From fc73bc9761ab17867060956a9c86cae396429708 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 1 Sep 2023 15:20:57 +0200 Subject: [PATCH 01/76] Add scripts for exporting music from PageLayout to MIDI + MusicXML files. --- pero_ocr/music/README.md | 6 + pero_ocr/music/__init__.py | 2 + pero_ocr/music/export_music.py | 406 +++++++++++++++++++++++ pero_ocr/music/music_structures.py | 503 +++++++++++++++++++++++++++++ pero_ocr/music/music_symbols.py | 396 +++++++++++++++++++++++ 5 files changed, 1313 insertions(+) create mode 100644 pero_ocr/music/README.md create mode 100644 pero_ocr/music/__init__.py create mode 100644 pero_ocr/music/export_music.py create mode 100644 pero_ocr/music/music_structures.py create mode 100644 pero_ocr/music/music_symbols.py diff --git a/pero_ocr/music/README.md b/pero_ocr/music/README.md new file mode 100644 index 0000000..3b4a0cb --- /dev/null +++ b/pero_ocr/music/README.md @@ -0,0 +1,6 @@ +# README.md + +This folder contains scripts for exporting transcribed musical pages. +Main functionality is in `export_music.py/ExportMusicPage`. + +For older versions of these files, see [github.com/vlachvojta/polyphonic-omr-by-sachindae](https://github.com/vlachvojta/polyphonic-omr-by-sachindae/tree/main/reverse_converter) diff --git a/pero_ocr/music/__init__.py b/pero_ocr/music/__init__.py new file mode 100644 index 0000000..60b154f --- /dev/null +++ b/pero_ocr/music/__init__.py @@ -0,0 +1,2 @@ +from export_music import ExportMusicPage +from export_music import ExportMusicLines diff --git a/pero_ocr/music/export_music.py b/pero_ocr/music/export_music.py new file mode 100644 index 0000000..93c7fe0 --- /dev/null +++ b/pero_ocr/music/export_music.py @@ -0,0 +1,406 @@ +#!/usr/bin/env python3.8 +"""Script to take output of pero-ocr with musical transcriptions and export it to musicxml and MIDI formats. + +INPUTS: +- XML PageLayout (exported directly from pero-ocr engine) using `--input-xml-path` argument + - Represents one whole page of musical notation transcribed by pero-ocr engine + - OUTPUTS one musicxml file for the page + - + MIDI file for page and for individual lines (named according to IDs in PageLayout) +- Text files with individual transcriptions and their IDs on each line using `--input-transcription-files` argument. + - OUTPUTS one musicxml file for each line with names corresponding to IDs in each line + +Author: Vojtěch Vlach +Contact: xvlach22@vutbr.cz +""" + +from __future__ import annotations +import sys +import argparse +import os +import re +import time +import logging +import json + +import music21 as music + +from music_structures import Measure +from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine + + +def parseargs(): + print(' '.join(sys.argv)) + print('----------------------------------------------------------------------') + parser = argparse.ArgumentParser() + + parser.add_argument( + "-i", "--input-xml-path", type=str, default='', + help="Path to input XML file with exported PageLayout.") + parser.add_argument( + '-f', '--input-transcription-files', nargs='*', default=None, + help='Input files with sequences as lines with IDs at the beginning.') + parser.add_argument( + "-t", "--translator-path", type=str, required=True, + help="JSON File containing translation dictionary from shorter encoding (exported by model) to longest.") + parser.add_argument( + "-o", "--output-folder", default='output_page', + help="Set output file with extension. Output format is JSON") + parser.add_argument( + "-m", "--export-midi", action='store_true', + help=("Enable exporting midi file to output_folder." + "Exports whole file and individual lines with names corresponding to them TextLine IDs.")) + parser.add_argument( + '-v', "--verbose", action='store_true', default=False, + help="Activate verbose logging.") + + return parser.parse_args() + + +def main(): + """Main function for simple testing""" + args = parseargs() + + start = time.time() + ExportMusicPage( + input_xml_path=args.input_xml_path, + input_transcription_files=args.input_transcription_files, + translator_path=args.translator_path, + output_folder=args.output_folder, + export_midi=args.export_midi, + verbose=args.verbose)() + + end = time.time() + print(f'Total time: {end - start:.2f} s') + + +class ExportMusicPage: + """Take pageLayout XML exported from pero-ocr with transcriptions and re-construct page of musical notation.""" + + def __init__(self, input_xml_path: str = '', translator_path: str = '', + input_transcription_files: list[str] = None, + output_folder: str = 'output_page', export_midi: bool = False, + verbose: bool = False): + self.translator_path = translator_path + if verbose: + logging.basicConfig(level=logging.DEBUG, format='[%(levelname)-s] \t- %(message)s') + else: + logging.basicConfig(level=logging.INFO, format='[%(levelname)-s]\t- %(message)s') + self.verbose = verbose + + if input_xml_path and not os.path.isfile(input_xml_path): + logging.error('No input file of this path was found') + self.input_xml_path = input_xml_path + + self.input_transcription_files = input_transcription_files if input_transcription_files else [] + + if not os.path.exists(output_folder): + os.makedirs(output_folder) + self.output_folder = output_folder + self.export_midi = export_midi + + self.translator = Translator(file_name=self.translator_path) + + def __call__(self) -> None: + if self.input_transcription_files: + ExportMusicLines(input_files=self.input_transcription_files, output_folder=self.output_folder, + translator=self.translator, verbose=self.verbose)() + + if self.input_xml_path: + self.export_xml() + + def export_xml(self) -> None: + page = PageLayout(file=self.input_xml_path) + print(f'Page {self.input_xml_path} loaded successfully.') + + parts = ExportMusicPage.regions_to_parts(page.regions, self.translator, self.export_midi) + music_parts = [] + for part in parts: + music_parts.append(part.encode_to_music21()) + + # Finalize score creation + metadata = music.metadata.Metadata() + metadata.title = metadata.composer = '' + score = music.stream.Score([metadata] + music_parts) + + # Export score to MusicXML or something + output_file = self.get_output_file('musicxml') + xml = music21_to_musicxml(score) + write_to_file(output_file, xml) + + if self.export_midi: + self.export_to_midi(score, parts) + + def get_output_file(self, extension: str = 'musicxml') -> str: + base = self.get_output_file_base() + return f'{base}.{extension}' + + def get_output_file_base(self) -> str: + input_file = os.path.basename(self.input_xml_path) + name, *_ = re.split(r'\.', input_file) + return os.path.join(self.output_folder, f'{name}') + + def export_to_midi(self, score, parts): + # Export whole score to midi + output_file = self.get_output_file('mid') + score.write("midi", output_file) + + for part in parts: + base = self.get_output_file_base() + part.export_to_midi(base) + + @staticmethod + def regions_to_parts(regions: list[RegionLayout], translator, export_midi: bool = False + ) -> list: # -> list[Part]: + """Takes a list of regions and splits them to parts.""" + max_parts = max([len(region.lines) for region in regions]) + + # TODO add empty measure padding to parts without textlines in multi-part scores. + + parts = [Part(translator) for _ in range(max_parts)] + for region in regions: + for part, line in zip(parts, region.lines): + part.add_textline(line) + + return parts + + +class ExportMusicLines: + """Takes text files with transcriptions as individual lines and exports musicxml file for each one""" + def __init__(self, translator: Translator, input_files: list[str] = None, + output_folder: str = 'output_musicxml', verbose: bool = False): + self.translator = translator + self.output_folder = output_folder + + if verbose: + logging.basicConfig(level=logging.DEBUG, format='[%(levelname)-s] \t- %(message)s') + else: + logging.basicConfig(level=logging.INFO, format='[%(levelname)-s]\t- %(message)s') + + logging.debug('Hello World! (from ReverseConverter)') + + self.input_files = ExportMusicLines.get_input_files(input_files) + ExportMusicLines.prepare_output_folder(output_folder) + + def __call__(self): + if not self.input_files: + logging.error('No input files provided. Exiting...') + sys.exit(1) + + # For every file, convert it to MusicXML + for input_file_name in self.input_files: + logging.info(f'Reading file {input_file_name}') + lines = ExportMusicLines.read_file_lines(input_file_name) + + for i, line in enumerate(lines): + match = re.fullmatch(r'([a-zA-Z0-9_\-]+)[a-zA-Z0-9_\.]+\s+([0-9]+\s+)?\"([\S\s]+)\"', line) + + if not match: + logging.debug(f'NOT MATCHING PATTERN. Skipping line {i} in file {input_file_name}: ' + f'({line[:min(50, len(line))]}...)') + continue + + stave_id = match.group(1) + labels = match.group(3) + labels = self.translator.convert_line(labels, to_shorter=False) + output_file_name = os.path.join(self.output_folder, f'{stave_id}.musicxml') + + parsed_labels = semantic_line_to_music21_score(labels) + if not isinstance(parsed_labels, music.stream.Stream): + logging.error(f'Labels could not be parsed. Skipping line {i} in file {input_file_name}: ' + f'({line[:min(50, len(line))]}...)') + continue + + logging.info(f'Parsing successfully completed.') + # parsed_labels.show() # Show parsed labels in some visual program (MuseScore by default) + + xml = music21_to_musicxml(parsed_labels) + write_to_file(output_file_name, xml) + + @staticmethod + def prepare_output_folder(output_folder: str): + if not os.path.exists(output_folder): + os.makedirs(output_folder) + + @staticmethod + def get_input_files(input_files: list[str] = None): + existing_files = [] + + if not input_files: + return [] + + for input_file in input_files: + if os.path.isfile(input_file): + existing_files.append(input_file) + + return existing_files + + @staticmethod + def read_file_lines(input_file: str) -> list[str]: + with open(input_file, 'r', encoding='utf-8') as f: + lines = f.read().splitlines() + + if not lines: + logging.warning(f'File {input_file} is empty!') + + return [line for line in lines if line] + + +class Part: + """Represent musical part (part of notation for one instrument/section)""" + + def __init__(self, translator): + self.translator = translator + + self.repr_music21 = music.stream.Part([music.instrument.Piano()]) + self.labels: list[str] = [] + self.textlines: list[TextLineWrapper] = [] + self.measures: list[Measure] = [] # List of measures in internal representation, NOT music21 + + def add_textline(self, line: TextLine) -> None: + labels = self.translator.convert_line(line.transcription, False) + self.labels.append(labels) + + new_measures = parse_semantic_to_measures(labels) + + # Delete first clef symbol of first measure in line if same as last clef in previous line + if len(self.measures) and new_measures[0].get_start_clef() == self.measures[-1].last_clef: + new_measures[0].delete_clef_symbol() + + new_measures_encoded = encode_measures(new_measures, len(self.measures) + 1) + new_measures_encoded_without_measure_ids = encode_measures(new_measures) + + self.measures += new_measures + self.repr_music21.append(new_measures_encoded) + + self.textlines.append(TextLineWrapper(line, new_measures_encoded_without_measure_ids)) + + def encode_to_music21(self) -> music.stream.Part: + if self.repr_music21 is None: + logging.info('Part empty') + + return self.repr_music21 + + def export_to_midi(self, file_base: str): + for text_line in self.textlines: + text_line.export_midi(file_base) + + +class TextLineWrapper: + """Class to wrap one TextLine for easier export etc.""" + def __init__(self, text_line: TextLine, measures: list[music.stream.Measure]): + self.text_line = text_line + print(f'len of measures: {len(measures)}') + self.repr_music21 = music.stream.Part([music.instrument.Piano()] + measures) + + def export_midi(self, file_base: str = 'out'): + filename = f'{file_base}_{self.text_line.id}.mid' + + xml = music21_to_musicxml(self.repr_music21) + parsed_xml = music.converter.parse(xml) + parsed_xml.write('mid', filename) + + +class Translator: + """Translator class for translating shorter SSemantic encoding to Semantic encoding using translator dictionary.""" + def __init__(self, file_name: str): + self.translator = Translator.read_json(file_name) + self.translator_reversed = {v: k for k, v in self.translator.items()} + self.n_existing_labels = set() + + def convert_line(self, line, to_shorter: bool = True): + line = line.strip('"').strip() + symbols = re.split(r'\s+', line) + converted_symbols = [self.convert_symbol(symbol, to_shorter) for symbol in symbols] + + return ' '.join(converted_symbols) + + def convert_symbol(self, symbol: str, to_shorter: bool = True): + dictionary = self.translator if to_shorter else self.translator_reversed + + try: + return dictionary[symbol] + except KeyError: + if symbol not in self.n_existing_labels: + self.n_existing_labels.add(symbol) + print(f'Not existing label: ({symbol})') + return '' + + @staticmethod + def read_json(filename) -> dict: + with open(filename) as f: + data = json.load(f) + return data + +def parse_semantic_to_measures(labels: str) -> list[Measure]: + """Convert line of semantic labels to list of measures. + + Args: + labels (str): one line of labels in semantic format without any prefixes. + """ + labels = labels.strip('"') + + measures_labels = re.split(r'barline', labels) + + stripped_measures_labels = [] + for measure_label in measures_labels: + stripped = measure_label.strip().strip('+').strip() + if stripped: + stripped_measures_labels.append(stripped) + + measures = [Measure(measure_label) for measure_label in stripped_measures_labels if measure_label] + + previous_measure_key = music.key.Key() # C Major as a default key (without accidentals) + for measure in measures: + previous_measure_key = measure.get_key(previous_measure_key) + + measures[0].new_system = True + + previous_measure_last_clef = measures[0].get_last_clef() + for measure in measures[1:]: + previous_measure_last_clef = measure.get_last_clef(previous_measure_last_clef) + + return measures + + +def encode_measures(measures: list, measure_id_start_from: int = 1) -> list[Measure]: + """Get list of measures and encode them to music21 encoded measures.""" + logging.debug('-------------------------------- -------------- --------------------------------') + logging.debug('-------------------------------- START ENCODING --------------------------------') + logging.debug('-------------------------------- -------------- --------------------------------') + + measures_encoded = [] + for measure_id, measure in enumerate(measures): + measures_encoded.append(measure.encode_to_music21()) + measures_encoded[-1].number = measure_id_start_from + measure_id + + return measures_encoded + + +def semantic_line_to_music21_score(labels: str) -> music.stream.Score: + """Get semantic line of labels, Return stream encoded in music21 score format.""" + measures = parse_semantic_to_measures(labels) + measures_encoded = encode_measures(measures) + + # stream = music.stream.Score(music.stream.Part([music.instrument.Piano()] + measures_encoded)) + metadata = music.metadata.Metadata() + metadata.title = metadata.composer = '' + stream = music.stream.Score([metadata, music.stream.Part([music.instrument.Piano()] + measures_encoded)]) + + return stream + + +def music21_to_musicxml(music_object): + out_bytes = music.musicxml.m21ToXml.GeneralObjectExporter(music_object).parse() + out_str = out_bytes.decode('utf-8') + return out_str.strip() + + +def write_to_file(output_file_name, xml): + with open(output_file_name, 'w', encoding='utf-8') as f: + f.write(xml) + + logging.info(f'File {output_file_name} successfully written.') + + +if __name__ == "__main__": + main() diff --git a/pero_ocr/music/music_structures.py b/pero_ocr/music/music_structures.py new file mode 100644 index 0000000..950f013 --- /dev/null +++ b/pero_ocr/music/music_structures.py @@ -0,0 +1,503 @@ +#!/usr/bin/python3.10 +"""Script for converting semantic sequential representation of music labels (produced by the model) to music21 stream +usable by music21 library to export to other formats. + +Author: Vojtěch Vlach +Contact: xvlach22@vutbr.cz +""" + +from __future__ import annotations +import re +from enum import Enum +import logging + +import music21 as music +from music_symbols import Symbol, SymbolType, AlteredPitches, LENGTH_TO_SYMBOL + + +class Measure: + _is_polyphonic = None + keysignature = None + repr = None + new_system = False + start_clef = None + last_clef = None + + def __init__(self, labels: str): + """Takes labels corresponding to a single measure.""" + self.labels = labels + + label_groups = re.split(r'\+', self.labels) + stripped_label_groups = [] + for measure_label in label_groups: + stripped = measure_label.strip().strip('+').strip() + if stripped: + stripped_label_groups.append(stripped) + + self.symbol_groups = [SymbolGroup(label_group) for label_group in stripped_label_groups] + self._is_polyphonic = self.is_polyphonic + + def __str__(self): + label_groups_str = '\n'.join([str(group) for group in self.symbol_groups]) + poly = 'polyphonic' if self.is_polyphonic else 'monophonic' + return (f'MEASURE: ({poly}) \n' + f'key signature: {self.keysignature}\n' + f'labels: {self.labels}\n' + f'{label_groups_str}') + + @property + def is_polyphonic(self) -> bool: + """Returns True if there are more than 1 notes in the same label_group with different lengths.""" + if self._is_polyphonic is not None: + return self._is_polyphonic + + self._is_polyphonic = any(group.type == SymbolGroupType.TUPLE for group in self.symbol_groups) + return self._is_polyphonic + + def get_key(self, previous_measure_key: music.key.Key) -> music.key.Key: + """Returns the key of the measure. + + Args: + previous_measure_key (music.key.Key): key of the previous measure. + + Returns: + music.key.Key: key of the current measure. + """ + if self.keysignature is not None: + return self.keysignature + + for symbol_group in self.symbol_groups: + key = symbol_group.get_key() + if key is not None: + self.set_key(key) + break + else: + self.set_key(previous_measure_key) + return self.keysignature + + def get_start_clef(self) -> music.clef.Clef | None: + if self.start_clef is not None: + return self.start_clef + else: + return self.symbol_groups[0].get_clef() + + def get_last_clef(self, previous_measure_last_clef: music.clef.Clef = music.clef.TrebleClef + ) -> music.clef.Clef | None: + if self.last_clef is not None: + return self.last_clef + + self.last_clef = previous_measure_last_clef + + for group in self.symbol_groups: + new_clef = group.get_clef() + if new_clef is not None: + self.last_clef = new_clef + + return self.last_clef + + def delete_clef_symbol(self, position: int = 0) -> None: + self.symbol_groups.pop(position) + + def set_key(self, key: music.key.Key): + """Sets the key of the measure. Send key to all symbols groups to represent notes in real height. + + Args: + key (music.key.Key): key of the current measure. + """ + self.keysignature = key + + altered_pitches = AlteredPitches(key) + for symbol_group in self.symbol_groups: + symbol_group.set_key(altered_pitches) + + def encode_to_music21(self) -> music.stream.Measure: + """Encodes the measure to music21 format. + + Returns: + music.stream.Measure: music21 representation of the measure. + """ + if self.repr is not None: + return self.repr + + self.repr = music.stream.Measure() + if not self._is_polyphonic: + for symbol_group in self.symbol_groups: + self.repr.append(symbol_group.encode_to_music21_monophonic()) + else: + self.repr = self.encode_to_music21_polyphonic() + + if self.new_system: + self.repr.insert(0, music.layout.SystemLayout(isNew=True)) + + logging.debug('Current measure:') + logging.debug(str(self)) + + logging.debug('Current measure ENCODED:') + return self.repr + + def encode_to_music21_polyphonic(self) -> music.stream.Measure: + """Encodes POLYPHONIC MEASURE to music21 format. + + Returns: + music.stream.Measure: music21 representation of the measure. + """ + voice_count = max([symbol_group.get_voice_count() for symbol_group in self.symbol_groups]) + voices = [Voice() for _ in range(voice_count)] + logging.debug('-------------------------------- NEW MEASURE --------------------------------') + logging.debug(f'voice_count: {voice_count}') + + zero_length_symbol_groups = Measure.find_zero_length_symbol_groups(self.symbol_groups) + remaining_symbol_groups = self.symbol_groups[len(zero_length_symbol_groups):] + + mono_start_symbol_groups = Measure.get_mono_start_symbol_groups(remaining_symbol_groups) + voices = Measure.create_mono_start(voices, mono_start_symbol_groups) + remaining_symbol_groups = remaining_symbol_groups[len(mono_start_symbol_groups):] + + # Groups to voices + for symbol_group in remaining_symbol_groups: + logging.debug('------------ NEW symbol_group ------------------------') + groups_to_add = symbol_group.get_groups_to_add() + shortest_voice_ids = Measure.pad_voices_to_n_shortest(voices, len(groups_to_add)) + + logging.debug( + f'Zipping {len(groups_to_add)} symbol groups to shortest voices ({len(shortest_voice_ids)}): {shortest_voice_ids}') + for voice_id, group in zip(shortest_voice_ids, groups_to_add): + logging.debug(f'Voice ({voice_id}) adding: {group}') + voices[voice_id].add_symbol_group(group) + + for voice_id, voice in enumerate(voices): + logging.debug(f'voice ({voice_id}) len: {voice.length}') + + zero_length_encoded = [group.encode_to_music21_monophonic() for group in zero_length_symbol_groups] + voices_repr = [voice.encode_to_music21_monophonic() for voice in voices] + return music.stream.Measure(zero_length_encoded + voices_repr) + + @staticmethod + def find_shortest_voices(voices: list, ignore: list = None) -> list[int]: + """Go through all voices and find the one with the current shortest duration. + + Args: + voices (list): list of voices. + ignore (list): indexes of voices to ignore. + + Returns: + list: indexes of voices with the current shortest duration. + """ + if ignore is None: + ignore = [] + + shortest_duration = 1_000_000 + shortest_voice_ids = [0] + for voice_id, voice in enumerate(voices): + if voice_id in ignore: + continue + if voice.length < shortest_duration: + shortest_duration = voice.length + shortest_voice_ids = [voice_id] + elif voice.length == shortest_duration: + shortest_voice_ids.append(voice_id) + + return shortest_voice_ids + + @staticmethod + def find_zero_length_symbol_groups(symbol_groups: list[SymbolGroup]) -> list[SymbolGroup]: + """Returns a list of zero-length symbol groups AT THE BEGGING OF THE MEASURE.""" + zero_length_symbol_groups = [] + for symbol_group in symbol_groups: + if symbol_group.type == SymbolGroupType.TUPLE or symbol_group.length > 0: + break + zero_length_symbol_groups.append(symbol_group) + return zero_length_symbol_groups + + @staticmethod + def pad_voices_to_n_shortest(voices: list[Voice], n: int = 1) -> list[int]: + """Pads voices (starting from the shortest) so there is n shortest voices with same length. + + Args: + voices (list): list of voices. + n (int): number of desired shortest voices. + + Returns: + list: list of voice IDS with the current shortest duration. + """ + shortest_voice_ids = Measure.find_shortest_voices(voices) + + while n > len(shortest_voice_ids): + logging.debug(f'Found {len(shortest_voice_ids)} shortest voices, desired voices: {n}.') + second_shortest_voice_ids = Measure.find_shortest_voices(voices, ignore=shortest_voice_ids) + second_shortest_len = voices[second_shortest_voice_ids[0]].length + for voice_id in shortest_voice_ids: + desired_padding_length = second_shortest_len - voices[voice_id].length + voices[voice_id].add_padding(desired_padding_length) + + shortest_voice_ids = Measure.find_shortest_voices(voices) + + return shortest_voice_ids + + @staticmethod + def get_mono_start_symbol_groups(symbol_groups: list[SymbolGroup]) -> list[SymbolGroup]: + """Get a list of monophonic symbol groups AT THE BEGINNING OF THE MEASURE. + + Returns: + list: list of monophonic symbol groups AT THE BEGINNING OF THE MEASURE + """ + mono_start_symbol_groups = [] + for symbol_group in symbol_groups: + if symbol_group.type == SymbolGroupType.TUPLE: + break + mono_start_symbol_groups.append(symbol_group) + return mono_start_symbol_groups + + @staticmethod + def create_mono_start(voices: list[Voice], mono_start_symbol_groups: list[SymbolGroup]) -> list[Voice]: + """Create monophonic start of measure in the first voice and add padding to the others. + + Args: + voices (list[Voices]): list of voices + mono_start_symbol_groups: list of monophonic symbol groups AT THE BEGINNING OF MEASURE. + + Returns: + list[Voice]: list of voices + """ + padding_length = 0 + for symbol_group in mono_start_symbol_groups: + voices[0].add_symbol_group(symbol_group) + padding_length += symbol_group.length + + for voice in voices[1:]: + voice.add_padding(padding_length) + + return voices + + +class SymbolGroupType(Enum): + SYMBOL = 0 + CHORD = 1 + TUPLE = 2 + EMPTY = 3 + UNKNOWN = 99 + + +class SymbolGroup: + """Represents one label group in a measure. Consisting of 1 to n labels/symbols.""" + tuple_data: list = None # Tuple data consists of a list of symbol groups where symbols have same lengths. + length: float = None # Length of the symbol group in quarter notes. + + def __init__(self, labels: str): + self.labels = labels + self.type = SymbolGroupType.UNKNOWN + + label_group_parsed = re.split(r'\s', self.labels.strip()) + self.symbols = [Symbol(label_group) for label_group in label_group_parsed if label_group] + + self.type = self.get_type() + if self.type == SymbolGroupType.TUPLE: + self.create_tuple_data() + + def __str__(self): + if not self.type == SymbolGroupType.TUPLE: + symbols_str = '\n'.join([str(symbol) for symbol in self.symbols]) + return (f'\t({self.type}) {self.labels} (len: {self.length}) =>\n' + f'{symbols_str}') + out = [] + for group in self.tuple_data: + out.append(str(group)) + out = '\n'.join(out) + + return (f'\tTUPLE BEGIN:\n' + f'{out}\n' + f'\tTUPLE END') + + def get_type(self): + if len(self.symbols) == 0: + logging.warning(f'No symbols found in label group: {self.labels}') + return SymbolGroupType.UNKNOWN + elif len(self.symbols) == 1: + self.length = self.symbols[0].get_length() + return SymbolGroupType.SYMBOL + else: + same_length_notes = all((symbol.get_length() == self.symbols[0].get_length() and + symbol.type in [SymbolType.NOTE, SymbolType.GRACENOTE]) + for symbol in self.symbols) + if same_length_notes: + self.length = self.symbols[0].get_length() + return SymbolGroupType.CHORD + else: + return SymbolGroupType.TUPLE + + def get_key(self) -> music.key.Key | None: + """Go through all labels and find key signature or return None. + + Returns: + music.key.Key: key signature of the label group or None. + """ + if not self.type == SymbolGroupType.SYMBOL: + return None + + for symbol in self.symbols: + if symbol.type == SymbolType.KEY_SIGNATURE: + return symbol.repr + + return None + + def set_key(self, altered_pitches: AlteredPitches): + if self.type == SymbolGroupType.TUPLE: + for group in self.tuple_data: + group.set_key(altered_pitches) + else: + for symbol in self.symbols: + symbol.set_key(altered_pitches) + + def encode_to_music21_monophonic(self): + """Encodes the label group to music21 format. + + Returns: + music.object: music21 representation of the label group. + """ + if self.type == SymbolGroupType.SYMBOL: + return self.symbols[0].repr + elif self.type == SymbolGroupType.CHORD: + notes = [symbol.repr for symbol in self.symbols] + logging.debug(f'notes: {notes}') + return music.chord.Chord(notes) + # return music.stream.Stream(music.chord.Chord(notes)) + elif self.type == SymbolGroupType.EMPTY: + return music.stream.Stream() + elif self.type == SymbolGroupType.TUPLE: + logging.info(f'Tuple label group not supported yet, returning empty stream.') + return music.stream.Stream() + else: + return music.stream.Stream() + + def create_tuple_data(self): + """Create tuple data for the label group. + + Tuple data consists of a list of symbol groups where symbols have same lengths. + """ + # logging.debug(f'Creating tuple data for label group: {self.labels}') + list_of_groups = [[self.symbols[0]]] + for symbol in self.symbols[1:]: + if symbol.type == SymbolType.REST: + list_of_groups.append([symbol]) + continue + symbol_length = symbol.get_length() + for group in list_of_groups: + # if symbol_length == group[0].get_length() and symbol.type in [SymbolType.NOTE, SymbolType.GRACENOTE]: + if group[0].type in [SymbolType.NOTE, SymbolType.GRACENOTE] and symbol_length == group[0].get_length(): + group.append(symbol) + break + else: + list_of_groups.append([symbol]) + + # logging.debug(list_of_groups) + + self.tuple_data = [] + for group in list_of_groups: + labels = [symbol.label for symbol in group] + labels = ' '.join(labels) + self.tuple_data.append(SymbolGroup(labels)) + + def get_voice_count(self): + """Returns the number of voices in the label group (count of groups in tuple group) + + Returns: + int: number of voices in the label group. + """ + if not self.type == SymbolGroupType.TUPLE: + return 1 + return len(self.tuple_data) + + def get_groups_to_add(self): + """Returns list of symbol groups. Either self in list of symbol groups in tuple data.""" + if self.type == SymbolGroupType.TUPLE: + groups_to_add = self.tuple_data.copy() + groups_to_add.reverse() + # return groups_to_add.reverse() + else: + groups_to_add = [self] + # return [self] + + logging.debug(f'groups_to_add:') + for group in groups_to_add: + logging.debug(f'{group}') + + return groups_to_add + + def get_clef(self) -> music.clef.Clef | None: + if self.type == SymbolGroupType.SYMBOL and self.symbols[0].type == SymbolType.CLEF: + return self.symbols[0].repr + return None + + +class Voice: + """Internal representation of voice (list of symbol groups symbolizing one musical line).""" + length: float = 0.0 # Accumulated length of symbol groups (in quarter notes). + symbol_groups: list = [] + repr = None + + def __init__(self): + self.length = 0.0 + self.symbol_groups = [] + self.repr = None + + def __str__(self): + out = [] + for group in self.symbol_groups: + out.append(str(group)) + out = '\n'.join(out) + + return (f'\tVOICE BEGIN:\n' + f'{out}\n' + f'\tVOICE END') + + def add_symbol_group(self, symbol_group: SymbolGroup) -> None: + if symbol_group.type == SymbolGroupType.TUPLE: + logging.warning(f'Can NOT add symbol group of type TUPLE to a voice.') + return + self.symbol_groups.append(symbol_group) + self.length += symbol_group.length + self.repr = None + + def encode_to_music21_monophonic(self) -> music.stream.Voice: + """Encodes the voice to music21 format. + + Returns: + music.Voice: music21 representation of the voice. + """ + if self.repr is not None: + return self.repr + + if len(self.symbol_groups) == 0: + return music.stream.Voice() + + self.repr = music.stream.Voice() + for group in self.symbol_groups: + self.repr.append(group.encode_to_music21_monophonic()) + return self.repr + + def add_padding(self, padding_length: float) -> None: + """Add padding symbols (rests) to the voice until it reaches the desired length. + + Args: + padding_length (float): desired length of the padding in quarter notes. + """ + lengths = list(LENGTH_TO_SYMBOL.values()) + min_length = min(LENGTH_TO_SYMBOL.keys()) + + while padding_length > 0: + if padding_length in LENGTH_TO_SYMBOL: + length_label = LENGTH_TO_SYMBOL[padding_length] + logging.debug(f'Completing padding with padding length {padding_length} to the voice.') + self.add_symbol_group(SymbolGroup(f'rest-{length_label}')) + padding_length -= padding_length + elif padding_length < min_length: + logging.error(f'Padding length {padding_length} is smaller than the minimum length {min}, breaking.') + break + else: + # Step is the biggest number lower than desired padding length. + step = lengths[lengths < padding_length].max() + logging.debug(f'Adding padding STEP {step} to the voice.') + + length_label = LENGTH_TO_SYMBOL[step] + self.add_symbol_group(SymbolGroup(f'rest-{length_label}')) + padding_length -= step diff --git a/pero_ocr/music/music_symbols.py b/pero_ocr/music/music_symbols.py new file mode 100644 index 0000000..c9cb40c --- /dev/null +++ b/pero_ocr/music/music_symbols.py @@ -0,0 +1,396 @@ +#!/usr/bin/python3.10 +"""Script containing classes for internal representation of Semantic labels. +Symbol class is default symbol for parsing and returning music21 representation. +Other classes are internal representations of different symbols. + +Primarily used for compatibility with music21 library. + +Author: Vojtěch Vlach +Contact: xvlach22@vutbr.cz +""" + +from __future__ import annotations +import logging +from enum import Enum +import re + +import music21 as music + + +class SymbolType(Enum): + CLEF = 0 + GRACENOTE = 1 + KEY_SIGNATURE = 2 + MULTI_REST = 3 + NOTE = 4 + REST = 5 + TIE = 6 + TIME_SIGNATURE = 7 + UNKNOWN = 99 + + +class Symbol: + """Represents one label in a label group.""" + + def __init__(self, label: str): + self.label = label + self.type, self.repr = Symbol.label_to_symbol(label) + self.length = self.get_length() + + def __str__(self): + return f'\t\t\t({self.type}) {self.repr}' + + def get_length(self) -> float: + """Returns the length of the symbol in quarter notes. + + (half note: 2 quarter notes, eighth note: 0.5 quarter notes, ...) + If the symbol does not have musical length, returns 0. + """ + if self.type in [SymbolType.REST, SymbolType.NOTE, SymbolType.GRACENOTE]: + return self.repr.duration.quarterLength + else: + return 0 + + def set_key(self, altered_pitches: AlteredPitches): + if self.type in [SymbolType.NOTE, SymbolType.GRACENOTE]: + self.repr = self.repr.get_real_height(altered_pitches) + + @staticmethod + def label_to_symbol(label: str): # -> (SymbolType, music.object): + """Converts one label to music21 format. + + Args: + label (str): one symbol in semantic format as string + """ + if label.startswith("clef-"): + label = label[len('clef-'):] + return SymbolType.CLEF, Symbol.clef_to_symbol(label) + elif label.startswith("gracenote-"): + label = label[len('gracenote-'):] + return SymbolType.GRACENOTE, Symbol.note_to_symbol(label, gracenote=True) + elif label.startswith("keySignature-"): + label = label[len('keySignature-'):] + return SymbolType.KEY_SIGNATURE, Symbol.keysignature_to_symbol(label) + elif label.startswith("multirest-"): + label = label[len('multirest-'):] + return SymbolType.MULTI_REST, Symbol.multirest_to_symbol(label) + elif label.startswith("note-"): + label = label[len('note-'):] + return SymbolType.NOTE, Symbol.note_to_symbol(label) + elif label.startswith("rest-"): + label = label[len('rest-'):] + return SymbolType.REST, Symbol.rest_to_symbol(label) + elif label.startswith("tie"): + label = label[len('tie'):] + return SymbolType.TIE, Symbol.tie_to_symbol(label) + elif label.startswith("timeSignature-"): + label = label[len('timeSignature-'):] + return SymbolType.TIME_SIGNATURE, Symbol.timesignature_to_symbol(label) + + logging.info(f'Unknown label: {label}, returning None.') + return SymbolType.UNKNOWN, None + + @staticmethod + def clef_to_symbol(clef) -> music.clef: + """Converts one clef label to music21 format. + + Args: + clef (str): one symbol in semantic format as string + + Returns: + music.clef: one clef in music21 format + """ + if len(clef) != 2: + logging.info(f'Unknown clef label: {clef}, returning default clef.') + return music.clef.Clef() + + return music.clef.clefFromString(clef) + + @staticmethod + def keysignature_to_symbol(keysignature) -> music.key.Key: + """Converts one key signature label to music21 format. + + Args: + keysignature (str): one symbol in semantic format as string + + Returns: + music.key.Key: one key in music21 format + """ + if not keysignature: + logging.info(f'Unknown key signature label: {keysignature}, returning default key.') + return music.key.Key() + + return music.key.Key(keysignature) + + @staticmethod + def multirest_to_symbol(multirest: str) -> MultiRest: + """Converts one multi rest label to internal MultiRest format. + + Args: + multirest (str): one symbol in semantic format as string + + Returns: + music.note.Rest: one rest in music21 format + """ + def return_default_multirest() -> MultiRest: + logging.info(f'Unknown multi rest label: {multirest}, returning default Multirest.') + return MultiRest() + + if not multirest: + return_default_multirest() + + try: + return MultiRest(int(multirest)) + except ValueError: + return return_default_multirest() + + @staticmethod + def note_to_symbol(note, gracenote: bool = False) -> Note: + """Converts one note label to internal note format. + + Args: + note (str): one symbol in semantic format as string + gracenote (bool, optional): if True, returns grace note. Defaults to False. + + Returns: + music.note.Note: one note in music21 format + """ + def return_default_note() -> music.note.Note: + logging.info(f'Unknown note label: {note}, returning default note.') + return Note(music.duration.Duration(1), 'C4', gracenote=gracenote) + + if not note: + return_default_note() + + note, fermata = Symbol.check_fermata(note) + + note_height, note_length = re.split('_', note, maxsplit=1) + + if not note_length or not note_height: + return_default_note() + + return Note(label_to_length(note_length), + note_height, fermata=fermata, gracenote=gracenote) + + @staticmethod + def rest_to_symbol(rest) -> music.note.Rest: + """Converts one rest label to music21 format. + + Args: + rest (str): one symbol in semantic format as string + + Returns: + music.note.Rest: one rest in music21 format + """ + if not rest: + logging.info(f'Unknown rest label: {rest}, returning default rest.') + return music.note.Rest() + + rest, fermata = Symbol.check_fermata(rest) + + duration = label_to_length(rest) + + rest = music.note.Rest() + rest.duration = duration + if fermata is not None: + rest.expressions.append(fermata) + + return rest + + @staticmethod + def tie_to_symbol(label): + return Tie + + @staticmethod + def timesignature_to_symbol(timesignature) -> music.meter.TimeSignature: + """Converts one time signature label to music21 format. + + Args: + timesignature (str): one symbol in semantic format as string + + Returns: + music.meter.TimeSignature: one time signature in music21 format + """ + if not timesignature: + logging.info(f'Unknown time signature label: {timesignature}, returning default time signature.') + return music.meter.TimeSignature() + + if timesignature == 'C/': + return music.meter.TimeSignature('cut') + else: + return music.meter.TimeSignature(timesignature) + + @staticmethod + def check_fermata(label: str) -> (str, music.expressions.Fermata): + """Check if note has fermata. + + Args: + label (str): one symbol in semantic format as string + + Returns: + str: note without fermata + music.expressions.Fermata: fermata + """ + fermata = None + if label.endswith('_fermata'): + label = label[:-len('_fermata')] + fermata = music.expressions.Fermata() + fermata.type = 'upright' + + return label, fermata + + +class Note: + """Represents one note in a label group. + + In the order which semantic labels are represented, the real height of note depends on key signature + of current measure. This class is used as an internal representation of a note before knowing its real height. + Real height is then stored directly in `self.note` as music.note.Note object. + """ + + def __init__(self, duration: music.duration.Duration, height: str, + fermata: music.expressions.Fermata = None, gracenote: bool = False): + self.duration = duration + self.height = height + self.fermata = fermata + self.note = None + self.gracenote = gracenote + self.note_ready = False + + def get_real_height(self, altered_pitches: AlteredPitches) -> music.note.Note | None: + """Returns the real height of the note. + + Args: + key signature of current measure + Returns: + Final music.note.Note object representing the real height and other info. + """ + if self.note_ready: + return self.note + + # pitches = [pitch.name[0] for pitch in key.alteredPitches] + + if not self.height[1:-1]: + # Note has no accidental on its own and takes accidental of the altered pitches. + note_str = self.height[0] + altered_pitches[self.height[0]] + self.height[-1] + self.note = music.note.Note(note_str, duration=self.duration) + else: + # Note has accidental which directly tells real note height. + note_str = self.height[0] + self.height[1:-1].replace('b', '-') + self.height[-1] + self.note = music.note.Note(note_str, duration=self.duration) + # Note sets new altered pitch for future notes. + altered_pitches[self.height[0]] = note_str[1:-1] + + if self.gracenote: + self.note = self.note.getGrace() + self.note_ready = True + return self.note + + def __str__(self): + return f'note {self.height} {self.duration}' + + +class MultiRest: + """Represents one multi rest in a label group.""" + + def __init__(self, duration: int = 0): + self.duration = duration + + +class Tie: + """Represents one tie in a label group.""" + + def __str__(self): + return 'tie' + + +class AlteredPitches: + def __init__(self, key: music.key.Key): + self.key = key + self.alteredPitches = {} + for pitch in self.key.alteredPitches: + self.alteredPitches[pitch.name[0]] = pitch.name[1] + + def __repr__(self): + return str(self.alteredPitches) + + def __str__(self): + return str(self.alteredPitches) + + def __getitem__(self, pitch_name: str): + """Gets name of pitch (e.g. 'C', 'G', ...) and returns its alternation.""" + if pitch_name not in self.alteredPitches: + return '' + return self.alteredPitches[pitch_name] + + def __setitem__(self, pitch_name: str, direction: str): + """Sets item. + + Args: + pitch_name (str): name of pitch (e.g. 'C', 'G',...) + direction (str): pitch alternation sign (#, ##, b, bb, 0, N) + """ + if not direction: + return + elif direction in ['0', 'N']: + if pitch_name in self.alteredPitches: + del self.alteredPitches[pitch_name] + # del self.alteredPitches[pitch_name] + return + else: + self.alteredPitches[pitch_name] = direction + + +SYMBOL_TO_LENGTH = { + 'hundred_twenty_eighth': 0.03125, + 'hundred_twenty_eighth.': 0.046875, + 'hundred_twenty_eighth..': 0.0546875, + 'sixty_fourth': 0.0625, + 'sixty_fourth.': 0.09375, + 'sixty_fourth..': 0.109375, + 'thirty_second': 0.125, + 'thirty_second.': 0.1875, + 'thirty_second..': 0.21875, + 'sixteenth': 0.25, + 'sixteenth.': 0.375, + 'sixteenth..': 0.4375, + 'eighth': 0.5, + 'eighth.': 0.75, + 'eighth..': 0.875, + 'quarter': 1.0, + 'quarter.': 1.5, + 'quarter..': 1.75, + 'half': 2.0, + 'half.': 3.0, + 'half..': 3.5, + 'whole': 4.0, + 'whole.': 6.0, + 'whole..': 7.0, + 'breve': 8.0, + 'breve.': 10.0, + 'breve..': 11.0, + 'double_whole': 8.0, + 'double_whole.': 12.0, + 'double_whole..': 14.0, + 'quadruple_whole': 16.0, + 'quadruple_whole.': 24.0, + 'quadruple_whole..': 28.0 +} + +LENGTH_TO_SYMBOL = {v: k for k, v in SYMBOL_TO_LENGTH.items()} # reverse dictionary + + +def label_to_length(length: str) -> music.duration.Duration: + """Return length of label as music21 duration. + + Args: + length (str): only length part of one label in semantic format as string + + Returns: + music.duration.Duration: one duration in music21 format + """ + if length in SYMBOL_TO_LENGTH: + return music.duration.Duration(SYMBOL_TO_LENGTH[length]) + else: + logging.info(f'Unknown duration label: {length}, returning default duration.') + return music.duration.Duration(1) From bda025aacf92c35b7e16d087033a61e717dae0c8 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 7 Sep 2023 10:18:49 +0200 Subject: [PATCH 02/76] Add translator dictionary, defining translation from internal shortened labels (model output) to more verbose format usable by `export_music.py`. --- pero_ocr/music/translator.Semantic_to_SSemantic.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 pero_ocr/music/translator.Semantic_to_SSemantic.json diff --git a/pero_ocr/music/translator.Semantic_to_SSemantic.json b/pero_ocr/music/translator.Semantic_to_SSemantic.json new file mode 100644 index 0000000..38c40ce --- /dev/null +++ b/pero_ocr/music/translator.Semantic_to_SSemantic.json @@ -0,0 +1,12 @@ +{ + "+": "+", + "barline": "|", + "clef-C1": "<1", "clef-C2": "<2", "clef-C3": "<3", "clef-C4": "<4", "clef-C5": "<5", "clef-F3": "=3", "clef-F4": "=4", "clef-F5": "=5", "clef-G1": ">1", "clef-G2": ">2", + "gracenote-A#3_eighth": "a#3z", "gracenote-A#3_sixteenth": "a#3S", "gracenote-A#4_eighth": "a#4z", "gracenote-A#4_half": "a#4H", "gracenote-A#4_quarter": "a#4q", "gracenote-A#4_sixteenth": "a#4S", "gracenote-A#4_thirty_second": "a#4T", "gracenote-A#5_eighth": "a#5z", "gracenote-A2_eighth": "a2z", "gracenote-A2_quarter": "a2q", "gracenote-A2_sixteenth": "a2S", "gracenote-A3_eighth": "a3z", "gracenote-A3_half": "a3H", "gracenote-A3_quarter": "a3q", "gracenote-A3_sixteenth": "a3S", "gracenote-A3_thirty_second": "a3T", "gracenote-A4_eighth": "a4z", "gracenote-A4_half": "a4H", "gracenote-A4_quarter": "a4q", "gracenote-A4_sixteenth": "a4S", "gracenote-A4_thirty_second": "a4T", "gracenote-A5_eighth": "a5z", "gracenote-A5_quarter": "a5q", "gracenote-A5_sixteenth": "a5S", "gracenote-A5_sixteenth.": "a5S.", "gracenote-A5_thirty_second": "a5T", "gracenote-Ab3_double_whole": "ab3w", "gracenote-Ab3_eighth": "ab3z", "gracenote-Ab3_sixteenth": "ab3S", "gracenote-Ab3_thirty_second": "ab3T", "gracenote-Ab4_eighth": "ab4z", "gracenote-Ab4_half": "ab4H", "gracenote-Ab4_quarter": "ab4q", "gracenote-Ab4_sixteenth": "ab4S", "gracenote-Ab4_thirty_second": "ab4T", "gracenote-Ab5_eighth": "ab5z", "gracenote-Ab5_quarter": "ab5q", "gracenote-Ab5_sixteenth": "ab5S", "gracenote-Ab5_thirty_second": "ab5T", "gracenote-B#3_sixteenth": "b#3S", "gracenote-B#4_eighth": "b#4z", "gracenote-B#4_quarter": "b#4q", "gracenote-B#4_sixteenth": "b#4S", "gracenote-B2_sixteenth": "b2S", "gracenote-B3_eighth": "b3z", "gracenote-B3_quarter": "b3q", "gracenote-B3_sixteenth": "b3S", "gracenote-B3_thirty_second": "b3T", "gracenote-B4_eighth": "b4z", "gracenote-B4_quarter": "b4q", "gracenote-B4_sixteenth": "b4S", "gracenote-B4_sixteenth.": "b4S.", "gracenote-B4_thirty_second": "b4T", "gracenote-B5_eighth": "b5z", "gracenote-Bb3_eighth": "bb3z", "gracenote-Bb3_quarter": "bb3q", "gracenote-Bb3_sixteenth": "bb3S", "gracenote-Bb3_thirty_second": "bb3T", "gracenote-Bb4_eighth": "bb4z", "gracenote-Bb4_eighth.": "bb4z.", "gracenote-Bb4_half": "bb4H", "gracenote-Bb4_quarter": "bb4q", "gracenote-Bb4_sixteenth": "bb4S", "gracenote-Bb4_thirty_second": "bb4T", "gracenote-Bb5_eighth": "bb5z", "gracenote-C#3_eighth": "c#3z", "gracenote-C#3_sixteenth": "c#3S", "gracenote-C#4_eighth": "c#4z", "gracenote-C#4_eighth.": "c#4z.", "gracenote-C#4_quarter": "c#4q", "gracenote-C#4_sixteenth": "c#4S", "gracenote-C#4_thirty_second": "c#4T", "gracenote-C#5_eighth": "c#5z", "gracenote-C#5_eighth.": "c#5z.", "gracenote-C#5_half": "c#5H", "gracenote-C#5_quarter": "c#5q", "gracenote-C#5_sixteenth": "c#5S", "gracenote-C#5_sixteenth.": "c#5S.", "gracenote-C#5_thirty_second": "c#5T", "gracenote-C3_eighth": "c3z", "gracenote-C3_quarter": "c3q", "gracenote-C3_sixteenth": "c3S", "gracenote-C4_eighth": "c4z", "gracenote-C4_quarter": "c4q", "gracenote-C4_sixteenth": "c4S", "gracenote-C4_thirty_second": "c4T", "gracenote-C5_eighth": "c5z", "gracenote-C5_half": "c5H", "gracenote-C5_quarter": "c5q", "gracenote-C5_sixteenth": "c5S", "gracenote-C5_thirty_second": "c5T", "gracenote-Cb5_eighth": "cb5z", "gracenote-Cb5_quarter": "cb5q", "gracenote-Cb5_thirty_second": "cb5T", "gracenote-D#3_quarter": "d#3q", "gracenote-D#3_sixteenth": "d#3S", "gracenote-D#4_eighth": "d#4z", "gracenote-D#4_quarter": "d#4q", "gracenote-D#4_sixteenth": "d#4S", "gracenote-D#4_thirty_second": "d#4T", "gracenote-D#5_eighth": "d#5z", "gracenote-D#5_quarter": "d#5q", "gracenote-D#5_sixteenth": "d#5S", "gracenote-D#5_thirty_second": "d#5T", "gracenote-D3_eighth": "d3z", "gracenote-D3_quarter": "d3q", "gracenote-D3_sixteenth": "d3S", "gracenote-D4_eighth": "d4z", "gracenote-D4_quarter": "d4q", "gracenote-D4_sixteenth": "d4S", "gracenote-D4_thirty_second": "d4T", "gracenote-D5_eighth": "d5z", "gracenote-D5_half": "d5H", "gracenote-D5_quarter": "d5q", "gracenote-D5_sixteenth": "d5S", "gracenote-D5_sixteenth.": "d5S.", "gracenote-D5_thirty_second": "d5T", "gracenote-Db4_eighth": "db4z", "gracenote-Db4_sixteenth": "db4S", "gracenote-Db5_eighth": "db5z", "gracenote-Db5_half": "db5H", "gracenote-Db5_quarter": "db5q", "gracenote-Db5_sixteenth": "db5S", "gracenote-Db5_thirty_second": "db5T", "gracenote-E#4_eighth": "e#4z", "gracenote-E#4_sixteenth": "e#4S", "gracenote-E#5_eighth": "e#5z", "gracenote-E#5_quarter": "e#5q", "gracenote-E#5_sixteenth": "e#5S", "gracenote-E3_eighth": "e3z", "gracenote-E3_quarter": "e3q", "gracenote-E3_sixteenth": "e3S", "gracenote-E4_eighth": "e4z", "gracenote-E4_quarter": "e4q", "gracenote-E4_sixteenth": "e4S", "gracenote-E4_thirty_second": "e4T", "gracenote-E5_eighth": "e5z", "gracenote-E5_half": "e5H", "gracenote-E5_quarter": "e5q", "gracenote-E5_sixteenth": "e5S", "gracenote-E5_thirty_second": "e5T", "gracenote-Eb3_eighth": "eb3z", "gracenote-Eb3_quarter": "eb3q", "gracenote-Eb3_sixteenth": "eb3S", "gracenote-Eb4_eighth": "eb4z", "gracenote-Eb4_quarter": "eb4q", "gracenote-Eb4_sixteenth": "eb4S", "gracenote-Eb4_thirty_second": "eb4T", "gracenote-Eb5_eighth": "eb5z", "gracenote-Eb5_quarter": "eb5q", "gracenote-Eb5_quarter.": "eb5q.", "gracenote-Eb5_sixteenth": "eb5S", "gracenote-Eb5_thirty_second": "eb5T", "gracenote-F#2_quarter": "f#2q", "gracenote-F#3_eighth": "f#3z", "gracenote-F#3_quarter": "f#3q", "gracenote-F#3_sixteenth": "f#3S", "gracenote-F#4_eighth": "f#4z", "gracenote-F#4_quarter": "f#4q", "gracenote-F#4_sixteenth": "f#4S", "gracenote-F#4_thirty_second": "f#4T", "gracenote-F#5_eighth": "f#5z", "gracenote-F#5_quarter": "f#5q", "gracenote-F#5_sixteenth": "f#5S", "gracenote-F#5_thirty_second": "f#5T", "gracenote-F2_eighth": "f2z", "gracenote-F3_eighth": "f3z", "gracenote-F3_quarter": "f3q", "gracenote-F3_sixteenth": "f3S", "gracenote-F3_thirty_second": "f3T", "gracenote-F4_eighth": "f4z", "gracenote-F4_quarter": "f4q", "gracenote-F4_sixteenth": "f4S", "gracenote-F4_thirty_second": "f4T", "gracenote-F5_eighth": "f5z", "gracenote-F5_half": "f5H", "gracenote-F5_quarter": "f5q", "gracenote-F5_sixteenth": "f5S", "gracenote-F5_sixteenth.": "f5S.", "gracenote-F5_thirty_second": "f5T", "gracenote-G#3_eighth": "g#3z", "gracenote-G#3_sixteenth": "g#3S", "gracenote-G#3_thirty_second": "g#3T", "gracenote-G#4_eighth": "g#4z", "gracenote-G#4_quarter": "g#4q", "gracenote-G#4_sixteenth": "g#4S", "gracenote-G#4_thirty_second": "g#4T", "gracenote-G#5_eighth": "g#5z", "gracenote-G#5_quarter": "g#5q", "gracenote-G#5_sixteenth": "g#5S", "gracenote-G#5_thirty_second": "g#5T", "gracenote-G3_eighth": "g3z", "gracenote-G3_quarter": "g3q", "gracenote-G3_sixteenth": "g3S", "gracenote-G3_thirty_second": "g3T", "gracenote-G4_eighth": "g4z", "gracenote-G4_eighth.": "g4z.", "gracenote-G4_half": "g4H", "gracenote-G4_quarter": "g4q", "gracenote-G4_sixteenth": "g4S", "gracenote-G4_thirty_second": "g4T", "gracenote-G5_eighth": "g5z", "gracenote-G5_half": "g5H", "gracenote-G5_quarter": "g5q", "gracenote-G5_sixteenth": "g5S", "gracenote-G5_sixteenth.": "g5S.", "gracenote-G5_thirty_second": "g5T", "gracenote-Gb4_eighth": "gb4z", "gracenote-Gb4_quarter": "gb4q", "gracenote-Gb5_thirty_second": "gb5T", + "keySignature-AM": "kAM", "keySignature-AbM": "kAbM", "keySignature-BM": "kBM", "keySignature-BbM": "kBbM", "keySignature-C#M": "kC#M", "keySignature-CM": "kCM", "keySignature-CbM": "kCbM", "keySignature-DM": "kDM", "keySignature-DbM": "kDbM", "keySignature-EM": "kEM", "keySignature-EbM": "kEbM", "keySignature-F#M": "kF#M", "keySignature-FM": "kFM", "keySignature-GM": "kGM", "keySignature-GbM": "kGbM", + "multirest-1": "-1", "multirest-10": "-10", "multirest-100": "-100", "multirest-105": "-105", "multirest-107": "-107", "multirest-11": "-11", "multirest-1111": "-1111", "multirest-112": "-112", "multirest-115": "-115", "multirest-119": "-119", "multirest-12": "-12", "multirest-123": "-123", "multirest-124": "-124", "multirest-126": "-126", "multirest-128": "-128", "multirest-13": "-13", "multirest-14": "-14", "multirest-143": "-143", "multirest-15": "-15", "multirest-16": "-16", "multirest-164": "-164", "multirest-17": "-17", "multirest-18": "-18", "multirest-19": "-19", "multirest-193": "-193", "multirest-2": "-2", "multirest-20": "-20", "multirest-21": "-21", "multirest-22": "-22", "multirest-225": "-225", "multirest-23": "-23", "multirest-24": "-24", "multirest-25": "-25", "multirest-26": "-26", "multirest-27": "-27", "multirest-28": "-28", "multirest-29": "-29", "multirest-3": "-3", "multirest-30": "-30", "multirest-31": "-31", "multirest-32": "-32", "multirest-33": "-33", "multirest-34": "-34", "multirest-35": "-35", "multirest-36": "-36", "multirest-37": "-37", "multirest-38": "-38", "multirest-39": "-39", "multirest-4": "-4", "multirest-40": "-40", "multirest-41": "-41", "multirest-42": "-42", "multirest-43": "-43", "multirest-44": "-44", "multirest-45": "-45", "multirest-46": "-46", "multirest-47": "-47", "multirest-48": "-48", "multirest-49": "-49", "multirest-5": "-5", "multirest-50": "-50", "multirest-51": "-51", "multirest-52": "-52", "multirest-53": "-53", "multirest-54": "-54", "multirest-55": "-55", "multirest-56": "-56", "multirest-57": "-57", "multirest-58": "-58", "multirest-59": "-59", "multirest-6": "-6", "multirest-60": "-60", "multirest-63": "-63", "multirest-64": "-64", "multirest-65": "-65", "multirest-66": "-66", "multirest-67": "-67", "multirest-68": "-68", "multirest-69": "-69", "multirest-7": "-7", "multirest-70": "-70", "multirest-71": "-71", "multirest-72": "-72", "multirest-73": "-73", "multirest-76": "-76", "multirest-77": "-77", "multirest-79": "-79", "multirest-8": "-8", "multirest-80": "-80", "multirest-81": "-81", "multirest-88": "-88", "multirest-89": "-89", "multirest-9": "-9", "multirest-91": "-91", "multirest-94": "-94", "multirest-96": "-96", "multirest-98": "-98", "multirest-99": "-99", + "note-A##1_eighth": "A##1z", "note-A##2_eighth": "A##2z", "note-A##2_sixteenth": "A##2S", "note-A##2_thirty_second": "A##2T", "note-A##3_sixteenth": "A##3S", "note-A##3_thirty_second": "A##3T", "note-A##4_eighth": "A##4z", "note-A##4_sixteenth": "A##4S", "note-A##5_eighth": "A##5z", "note-A##5_quarter": "A##5q", "note-A##6_eighth": "A##6z", "note-A##7_eighth": "A##7z", "note-A#0_eighth": "A#0z", "note-A#0_half": "A#0H", "note-A#0_quarter": "A#0q", "note-A#0_sixteenth": "A#0S", "note-A#1_eighth": "A#1z", "note-A#1_eighth.": "A#1z.", "note-A#1_half": "A#1H", "note-A#1_half.": "A#1H.", "note-A#1_quarter": "A#1q", "note-A#1_quarter.": "A#1q.", "note-A#1_sixteenth": "A#1S", "note-A#1_thirty_second": "A#1T", "note-A#1_whole": "A#1W", "note-A#1_whole.": "A#1W.", "note-A#2_eighth": "A#2z", "note-A#2_eighth.": "A#2z.", "note-A#2_half": "A#2H", "note-A#2_half.": "A#2H.", "note-A#2_quarter": "A#2q", "note-A#2_quarter.": "A#2q.", "note-A#2_sixteenth": "A#2S", "note-A#2_sixteenth.": "A#2S.", "note-A#2_sixty_fourth": "A#2s", "note-A#2_thirty_second": "A#2T", "note-A#2_whole": "A#2W", "note-A#2_whole.": "A#2W.", "note-A#3_eighth": "A#3z", "note-A#3_eighth.": "A#3z.", "note-A#3_half": "A#3H", "note-A#3_half.": "A#3H.", "note-A#3_half_fermata": "A#3H^", "note-A#3_hundred_twenty_eighth": "A#3h", "note-A#3_quarter": "A#3q", "note-A#3_quarter.": "A#3q.", "note-A#3_sixteenth": "A#3S", "note-A#3_sixteenth.": "A#3S.", "note-A#3_sixty_fourth": "A#3s", "note-A#3_thirty_second": "A#3T", "note-A#3_whole": "A#3W", "note-A#4_eighth": "A#4z", "note-A#4_eighth.": "A#4z.", "note-A#4_half": "A#4H", "note-A#4_half.": "A#4H.", "note-A#4_half_fermata": "A#4H^", "note-A#4_quarter": "A#4q", "note-A#4_quarter.": "A#4q.", "note-A#4_quarter_fermata": "A#4q^", "note-A#4_sixteenth": "A#4S", "note-A#4_sixteenth.": "A#4S.", "note-A#4_sixty_fourth": "A#4s", "note-A#4_thirty_second": "A#4T", "note-A#4_whole": "A#4W", "note-A#4_whole.": "A#4W.", "note-A#5_eighth": "A#5z", "note-A#5_eighth.": "A#5z.", "note-A#5_half": "A#5H", "note-A#5_half.": "A#5H.", "note-A#5_quarter": "A#5q", "note-A#5_quarter.": "A#5q.", "note-A#5_sixteenth": "A#5S", "note-A#5_sixteenth.": "A#5S.", "note-A#5_sixty_fourth": "A#5s", "note-A#5_thirty_second": "A#5T", "note-A#5_whole": "A#5W", "note-A#5_whole.": "A#5W.", "note-A#6_eighth": "A#6z", "note-A#6_eighth.": "A#6z.", "note-A#6_half": "A#6H", "note-A#6_quarter": "A#6q", "note-A#6_quarter.": "A#6q.", "note-A#6_sixteenth": "A#6S", "note-A#6_sixty_fourth": "A#6s", "note-A#6_thirty_second": "A#6T", "note-A#6_whole": "A#6W", "note-A#7_eighth": "A#7z", "note-A#7_eighth.": "A#7z.", "note-A#7_half": "A#7H", "note-A#7_quarter": "A#7q", "note-A#7_sixteenth": "A#7S", "note-A#7_sixty_fourth": "A#7s", "note-A#7_thirty_second": "A#7T", "note-A0_eighth": "A0z", "note-A0_eighth.": "A0z.", "note-A0_half": "A0H", "note-A0_half.": "A0H.", "note-A0_quarter": "A0q", "note-A0_quarter.": "A0q.", "note-A0_sixteenth": "A0S", "note-A0_sixteenth.": "A0S.", "note-A0_thirty_second": "A0T", "note-A0_whole": "A0W", "note-A1_breve": "A1Y", "note-A1_eighth": "A1z", "note-A1_eighth.": "A1z.", "note-A1_half": "A1H", "note-A1_half.": "A1H.", "note-A1_hundred_twenty_eighth": "A1h", "note-A1_quarter": "A1q", "note-A1_quarter.": "A1q.", "note-A1_sixteenth": "A1S", "note-A1_sixteenth.": "A1S.", "note-A1_sixty_fourth": "A1s", "note-A1_sixty_fourth.": "A1s.", "note-A1_thirty_second": "A1T", "note-A1_thirty_second.": "A1T.", "note-A1_whole": "A1W", "note-A1_whole.": "A1W.", "note-A2_breve": "A2Y", "note-A2_double_whole": "A2w", "note-A2_double_whole.": "A2w.", "note-A2_double_whole_fermata": "A2w^", "note-A2_eighth": "A2z", "note-A2_eighth.": "A2z.", "note-A2_half": "A2H", "note-A2_half.": "A2H.", "note-A2_half_fermata": "A2H^", "note-A2_hundred_twenty_eighth": "A2h", "note-A2_quadruple_whole": "A2Q", "note-A2_quadruple_whole_fermata": "A2Q^", "note-A2_quarter": "A2q", "note-A2_quarter.": "A2q.", "note-A2_quarter_fermata": "A2q^", "note-A2_sixteenth": "A2S", "note-A2_sixteenth.": "A2S.", "note-A2_sixty_fourth": "A2s", "note-A2_sixty_fourth.": "A2s.", "note-A2_thirty_second": "A2T", "note-A2_thirty_second.": "A2T.", "note-A2_whole": "A2W", "note-A2_whole.": "A2W.", "note-A2_whole_fermata": "A2W^", "note-A3_breve": "A3Y", "note-A3_breve.": "A3Y.", "note-A3_double_whole": "A3w", "note-A3_double_whole.": "A3w.", "note-A3_double_whole_fermata": "A3w^", "note-A3_eighth": "A3z", "note-A3_eighth.": "A3z.", "note-A3_eighth..": "A3z..", "note-A3_eighth._fermata": "A3z.^", "note-A3_eighth_fermata": "A3z^", "note-A3_half": "A3H", "note-A3_half.": "A3H.", "note-A3_half._fermata": "A3H.^", "note-A3_half_fermata": "A3H^", "note-A3_hundred_twenty_eighth": "A3h", "note-A3_quadruple_whole": "A3Q", "note-A3_quarter": "A3q", "note-A3_quarter.": "A3q.", "note-A3_quarter..": "A3q..", "note-A3_quarter_fermata": "A3q^", "note-A3_sixteenth": "A3S", "note-A3_sixteenth.": "A3S.", "note-A3_sixty_fourth": "A3s", "note-A3_sixty_fourth.": "A3s.", "note-A3_thirty_second": "A3T", "note-A3_thirty_second.": "A3T.", "note-A3_whole": "A3W", "note-A3_whole.": "A3W.", "note-A3_whole_fermata": "A3W^", "note-A4_breve": "A4Y", "note-A4_double_whole": "A4w", "note-A4_double_whole.": "A4w.", "note-A4_double_whole_fermata": "A4w^", "note-A4_eighth": "A4z", "note-A4_eighth.": "A4z.", "note-A4_eighth..": "A4z..", "note-A4_eighth_fermata": "A4z^", "note-A4_half": "A4H", "note-A4_half.": "A4H.", "note-A4_half..": "A4H..", "note-A4_half._fermata": "A4H.^", "note-A4_half_fermata": "A4H^", "note-A4_hundred_twenty_eighth": "A4h", "note-A4_long": "A4long", "note-A4_quadruple_whole": "A4Q", "note-A4_quadruple_whole.": "A4Q.", "note-A4_quarter": "A4q", "note-A4_quarter.": "A4q.", "note-A4_quarter..": "A4q..", "note-A4_quarter._fermata": "A4q.^", "note-A4_quarter_fermata": "A4q^", "note-A4_sixteenth": "A4S", "note-A4_sixteenth.": "A4S.", "note-A4_sixty_fourth": "A4s", "note-A4_sixty_fourth.": "A4s.", "note-A4_thirty_second": "A4T", "note-A4_thirty_second.": "A4T.", "note-A4_whole": "A4W", "note-A4_whole.": "A4W.", "note-A4_whole._fermata": "A4W.^", "note-A4_whole_fermata": "A4W^", "note-A5_breve": "A5Y", "note-A5_double_whole": "A5w", "note-A5_eighth": "A5z", "note-A5_eighth.": "A5z.", "note-A5_eighth..": "A5z..", "note-A5_eighth_fermata": "A5z^", "note-A5_half": "A5H", "note-A5_half.": "A5H.", "note-A5_half._fermata": "A5H.^", "note-A5_half_fermata": "A5H^", "note-A5_hundred_twenty_eighth": "A5h", "note-A5_quarter": "A5q", "note-A5_quarter.": "A5q.", "note-A5_quarter..": "A5q..", "note-A5_quarter._fermata": "A5q.^", "note-A5_quarter_fermata": "A5q^", "note-A5_sixteenth": "A5S", "note-A5_sixteenth.": "A5S.", "note-A5_sixty_fourth": "A5s", "note-A5_sixty_fourth.": "A5s.", "note-A5_thirty_second": "A5T", "note-A5_thirty_second.": "A5T.", "note-A5_whole": "A5W", "note-A5_whole.": "A5W.", "note-A5_whole_fermata": "A5W^", "note-A6_eighth": "A6z", "note-A6_eighth.": "A6z.", "note-A6_half": "A6H", "note-A6_half.": "A6H.", "note-A6_hundred_twenty_eighth": "A6h", "note-A6_quarter": "A6q", "note-A6_quarter.": "A6q.", "note-A6_sixteenth": "A6S", "note-A6_sixteenth.": "A6S.", "note-A6_sixty_fourth": "A6s", "note-A6_sixty_fourth.": "A6s.", "note-A6_thirty_second": "A6T", "note-A6_thirty_second.": "A6T.", "note-A6_whole": "A6W", "note-A6_whole.": "A6W.", "note-A7_eighth": "A7z", "note-A7_eighth.": "A7z.", "note-A7_half": "A7H", "note-A7_half.": "A7H.", "note-A7_hundred_twenty_eighth": "A7h", "note-A7_quarter": "A7q", "note-A7_quarter.": "A7q.", "note-A7_sixteenth": "A7S", "note-A7_sixteenth.": "A7S.", "note-A7_sixty_fourth": "A7s", "note-A7_thirty_second": "A7T", "note-A7_thirty_second.": "A7T.", "note-A7_whole": "A7W", "note-A8_eighth": "A8z", "note-A8_eighth.": "A8z.", "note-A8_hundred_twenty_eighth": "A8h", "note-A8_quarter": "A8q", "note-A8_sixteenth": "A8S", "note-A8_sixteenth.": "A8S.", "note-A8_sixty_fourth.": "A8s.", "note-A8_thirty_second": "A8T", "note-AN0_eighth": "AN0z", "note-AN0_half.": "AN0H.", "note-AN0_sixteenth": "AN0S", "note-AN0_whole": "AN0W", "note-AN1_eighth": "AN1z", "note-AN1_eighth.": "AN1z.", "note-AN1_half": "AN1H", "note-AN1_half.": "AN1H.", "note-AN1_hundred_twenty_eighth": "AN1h", "note-AN1_quarter": "AN1q", "note-AN1_quarter.": "AN1q.", "note-AN1_sixteenth": "AN1S", "note-AN1_sixty_fourth.": "AN1s.", "note-AN1_thirty_second": "AN1T", "note-AN1_whole": "AN1W", "note-AN2_eighth": "AN2z", "note-AN2_eighth.": "AN2z.", "note-AN2_half": "AN2H", "note-AN2_half.": "AN2H.", "note-AN2_quarter": "AN2q", "note-AN2_quarter.": "AN2q.", "note-AN2_sixteenth": "AN2S", "note-AN2_sixteenth.": "AN2S.", "note-AN2_sixty_fourth": "AN2s", "note-AN2_thirty_second": "AN2T", "note-AN2_whole": "AN2W", "note-AN2_whole.": "AN2W.", "note-AN3_eighth": "AN3z", "note-AN3_eighth.": "AN3z.", "note-AN3_half": "AN3H", "note-AN3_half.": "AN3H.", "note-AN3_quarter": "AN3q", "note-AN3_quarter.": "AN3q.", "note-AN3_sixteenth": "AN3S", "note-AN3_sixteenth.": "AN3S.", "note-AN3_sixty_fourth": "AN3s", "note-AN3_thirty_second": "AN3T", "note-AN3_thirty_second.": "AN3T.", "note-AN3_whole": "AN3W", "note-AN3_whole.": "AN3W.", "note-AN4_eighth": "AN4z", "note-AN4_eighth.": "AN4z.", "note-AN4_half": "AN4H", "note-AN4_half.": "AN4H.", "note-AN4_hundred_twenty_eighth": "AN4h", "note-AN4_quarter": "AN4q", "note-AN4_quarter.": "AN4q.", "note-AN4_sixteenth": "AN4S", "note-AN4_sixteenth.": "AN4S.", "note-AN4_sixty_fourth": "AN4s", "note-AN4_thirty_second": "AN4T", "note-AN4_thirty_second.": "AN4T.", "note-AN4_whole": "AN4W", "note-AN4_whole.": "AN4W.", "note-AN5_eighth": "AN5z", "note-AN5_eighth.": "AN5z.", "note-AN5_half": "AN5H", "note-AN5_half.": "AN5H.", "note-AN5_hundred_twenty_eighth": "AN5h", "note-AN5_quarter": "AN5q", "note-AN5_quarter.": "AN5q.", "note-AN5_sixteenth": "AN5S", "note-AN5_sixteenth.": "AN5S.", "note-AN5_sixty_fourth": "AN5s", "note-AN5_sixty_fourth.": "AN5s.", "note-AN5_thirty_second": "AN5T", "note-AN5_thirty_second.": "AN5T.", "note-AN5_whole": "AN5W", "note-AN5_whole.": "AN5W.", "note-AN6_eighth": "AN6z", "note-AN6_eighth.": "AN6z.", "note-AN6_half": "AN6H", "note-AN6_half.": "AN6H.", "note-AN6_hundred_twenty_eighth": "AN6h", "note-AN6_quarter": "AN6q", "note-AN6_quarter.": "AN6q.", "note-AN6_sixteenth": "AN6S", "note-AN6_sixteenth.": "AN6S.", "note-AN6_sixty_fourth": "AN6s", "note-AN6_sixty_fourth.": "AN6s.", "note-AN6_thirty_second": "AN6T", "note-AN6_thirty_second.": "AN6T.", "note-AN6_whole": "AN6W", "note-AN7_eighth": "AN7z", "note-AN7_quarter": "AN7q", "note-AN7_sixteenth": "AN7S", "note-AN7_thirty_second": "AN7T", "note-Ab0_half": "Ab0H", "note-Ab0_quarter.": "Ab0q.", "note-Ab1_eighth": "Ab1z", "note-Ab1_eighth.": "Ab1z.", "note-Ab1_half": "Ab1H", "note-Ab1_half.": "Ab1H.", "note-Ab1_quarter": "Ab1q", "note-Ab1_quarter.": "Ab1q.", "note-Ab1_sixteenth": "Ab1S", "note-Ab1_thirty_second": "Ab1T", "note-Ab1_thirty_second.": "Ab1T.", "note-Ab1_whole": "Ab1W", "note-Ab1_whole.": "Ab1W.", "note-Ab2_breve": "Ab2Y", "note-Ab2_eighth": "Ab2z", "note-Ab2_eighth.": "Ab2z.", "note-Ab2_half": "Ab2H", "note-Ab2_half.": "Ab2H.", "note-Ab2_half_fermata": "Ab2H^", "note-Ab2_quarter": "Ab2q", "note-Ab2_quarter.": "Ab2q.", "note-Ab2_sixteenth": "Ab2S", "note-Ab2_sixty_fourth": "Ab2s", "note-Ab2_thirty_second": "Ab2T", "note-Ab2_whole": "Ab2W", "note-Ab2_whole.": "Ab2W.", "note-Ab3_breve": "Ab3Y", "note-Ab3_eighth": "Ab3z", "note-Ab3_eighth.": "Ab3z.", "note-Ab3_half": "Ab3H", "note-Ab3_half.": "Ab3H.", "note-Ab3_quarter": "Ab3q", "note-Ab3_quarter.": "Ab3q.", "note-Ab3_quarter..": "Ab3q..", "note-Ab3_sixteenth": "Ab3S", "note-Ab3_sixteenth.": "Ab3S.", "note-Ab3_sixty_fourth": "Ab3s", "note-Ab3_thirty_second": "Ab3T", "note-Ab3_thirty_second.": "Ab3T.", "note-Ab3_whole": "Ab3W", "note-Ab3_whole.": "Ab3W.", "note-Ab4_breve": "Ab4Y", "note-Ab4_eighth": "Ab4z", "note-Ab4_eighth.": "Ab4z.", "note-Ab4_eighth..": "Ab4z..", "note-Ab4_half": "Ab4H", "note-Ab4_half.": "Ab4H.", "note-Ab4_half._fermata": "Ab4H.^", "note-Ab4_half_fermata": "Ab4H^", "note-Ab4_hundred_twenty_eighth": "Ab4h", "note-Ab4_quarter": "Ab4q", "note-Ab4_quarter.": "Ab4q.", "note-Ab4_quarter..": "Ab4q..", "note-Ab4_quarter_fermata": "Ab4q^", "note-Ab4_sixteenth": "Ab4S", "note-Ab4_sixteenth.": "Ab4S.", "note-Ab4_sixty_fourth": "Ab4s", "note-Ab4_thirty_second": "Ab4T", "note-Ab4_thirty_second.": "Ab4T.", "note-Ab4_whole": "Ab4W", "note-Ab4_whole.": "Ab4W.", "note-Ab4_whole_fermata": "Ab4W^", "note-Ab5_breve": "Ab5Y", "note-Ab5_eighth": "Ab5z", "note-Ab5_eighth.": "Ab5z.", "note-Ab5_eighth..": "Ab5z..", "note-Ab5_half": "Ab5H", "note-Ab5_half.": "Ab5H.", "note-Ab5_half._fermata": "Ab5H.^", "note-Ab5_hundred_twenty_eighth": "Ab5h", "note-Ab5_quarter": "Ab5q", "note-Ab5_quarter.": "Ab5q.", "note-Ab5_quarter_fermata": "Ab5q^", "note-Ab5_sixteenth": "Ab5S", "note-Ab5_sixteenth.": "Ab5S.", "note-Ab5_sixty_fourth": "Ab5s", "note-Ab5_sixty_fourth.": "Ab5s.", "note-Ab5_thirty_second": "Ab5T", "note-Ab5_thirty_second.": "Ab5T.", "note-Ab5_whole": "Ab5W", "note-Ab5_whole.": "Ab5W.", "note-Ab6_eighth": "Ab6z", "note-Ab6_eighth.": "Ab6z.", "note-Ab6_half": "Ab6H", "note-Ab6_half.": "Ab6H.", "note-Ab6_hundred_twenty_eighth": "Ab6h", "note-Ab6_quarter": "Ab6q", "note-Ab6_quarter.": "Ab6q.", "note-Ab6_sixteenth": "Ab6S", "note-Ab6_sixty_fourth": "Ab6s", "note-Ab6_thirty_second": "Ab6T", "note-Ab6_whole": "Ab6W", "note-Ab6_whole.": "Ab6W.", "note-Ab7_eighth": "Ab7z", "note-Ab7_hundred_twenty_eighth": "Ab7h", "note-Ab7_quarter": "Ab7q", "note-Ab7_sixteenth": "Ab7S", "note-Ab7_thirty_second": "Ab7T", "note-Abb1_half": "Abb1H", "note-Abb1_sixteenth": "Abb1S", "note-Abb1_whole": "Abb1W", "note-Abb2_eighth": "Abb2z", "note-Abb2_half": "Abb2H", "note-Abb2_quarter": "Abb2q", "note-Abb2_sixteenth": "Abb2S", "note-Abb2_whole": "Abb2W", "note-Abb3_eighth": "Abb3z", "note-Abb3_half": "Abb3H", "note-Abb3_quarter": "Abb3q", "note-Abb3_sixteenth": "Abb3S", "note-Abb4_eighth": "Abb4z", "note-Abb4_quarter": "Abb4q", "note-Abb4_sixteenth": "Abb4S", "note-Abb4_thirty_second": "Abb4T", "note-Abb5_eighth": "Abb5z", "note-Abb5_quarter": "Abb5q", "note-Abb5_sixteenth": "Abb5S", "note-Abb5_thirty_second": "Abb5T", "note-Abb6_eighth": "Abb6z", "note-Abb6_quarter": "Abb6q", "note-Abb6_sixteenth": "Abb6S", "note-Abb6_sixty_fourth": "Abb6s", "note-Abb6_thirty_second": "Abb6T", "note-B##2_eighth": "B##2z", "note-B##2_sixteenth": "B##2S", "note-B##4_eighth": "B##4z", "note-B##4_quarter": "B##4q", "note-B##4_sixteenth": "B##4S", "note-B#0_eighth": "B#0z", "note-B#0_whole": "B#0W", "note-B#1_eighth": "B#1z", "note-B#1_eighth.": "B#1z.", "note-B#1_half": "B#1H", "note-B#1_half.": "B#1H.", "note-B#1_quarter": "B#1q", "note-B#1_quarter.": "B#1q.", "note-B#1_sixteenth": "B#1S", "note-B#1_sixty_fourth": "B#1s", "note-B#1_thirty_second": "B#1T", "note-B#1_thirty_second.": "B#1T.", "note-B#1_whole": "B#1W", "note-B#1_whole.": "B#1W.", "note-B#2_eighth": "B#2z", "note-B#2_eighth.": "B#2z.", "note-B#2_half": "B#2H", "note-B#2_half.": "B#2H.", "note-B#2_quarter": "B#2q", "note-B#2_quarter.": "B#2q.", "note-B#2_sixteenth": "B#2S", "note-B#2_sixty_fourth": "B#2s", "note-B#2_thirty_second": "B#2T", "note-B#2_thirty_second.": "B#2T.", "note-B#2_whole": "B#2W", "note-B#2_whole.": "B#2W.", "note-B#3_double_whole": "B#3w", "note-B#3_double_whole.": "B#3w.", "note-B#3_eighth": "B#3z", "note-B#3_eighth.": "B#3z.", "note-B#3_half": "B#3H", "note-B#3_half.": "B#3H.", "note-B#3_quarter": "B#3q", "note-B#3_quarter.": "B#3q.", "note-B#3_sixteenth": "B#3S", "note-B#3_sixteenth.": "B#3S.", "note-B#3_sixty_fourth": "B#3s", "note-B#3_thirty_second": "B#3T", "note-B#3_whole": "B#3W", "note-B#4_double_whole": "B#4w", "note-B#4_double_whole_fermata": "B#4w^", "note-B#4_eighth": "B#4z", "note-B#4_eighth.": "B#4z.", "note-B#4_half": "B#4H", "note-B#4_half.": "B#4H.", "note-B#4_quarter": "B#4q", "note-B#4_quarter.": "B#4q.", "note-B#4_sixteenth": "B#4S", "note-B#4_sixteenth.": "B#4S.", "note-B#4_sixty_fourth": "B#4s", "note-B#4_thirty_second": "B#4T", "note-B#4_whole": "B#4W", "note-B#4_whole.": "B#4W.", "note-B#5_eighth": "B#5z", "note-B#5_eighth.": "B#5z.", "note-B#5_half": "B#5H", "note-B#5_half.": "B#5H.", "note-B#5_quarter": "B#5q", "note-B#5_quarter.": "B#5q.", "note-B#5_sixteenth": "B#5S", "note-B#5_sixteenth.": "B#5S.", "note-B#5_sixty_fourth": "B#5s", "note-B#5_thirty_second": "B#5T", "note-B#5_whole": "B#5W", "note-B#5_whole.": "B#5W.", "note-B#6_eighth": "B#6z", "note-B#6_quarter": "B#6q", "note-B#6_quarter.": "B#6q.", "note-B#6_sixteenth": "B#6S", "note-B#6_sixty_fourth": "B#6s", "note-B#6_thirty_second": "B#6T", "note-B#6_whole": "B#6W", "note-B#7_eighth": "B#7z", "note-B#7_sixteenth": "B#7S", "note-B#7_thirty_second": "B#7T", "note-B0_eighth": "B0z", "note-B0_eighth.": "B0z.", "note-B0_half": "B0H", "note-B0_half.": "B0H.", "note-B0_quarter": "B0q", "note-B0_quarter.": "B0q.", "note-B0_sixteenth": "B0S", "note-B0_sixteenth.": "B0S.", "note-B0_thirty_second": "B0T", "note-B0_whole": "B0W", "note-B0_whole.": "B0W.", "note-B1_breve": "B1Y", "note-B1_eighth": "B1z", "note-B1_eighth.": "B1z.", "note-B1_half": "B1H", "note-B1_half.": "B1H.", "note-B1_hundred_twenty_eighth": "B1h", "note-B1_quarter": "B1q", "note-B1_quarter.": "B1q.", "note-B1_sixteenth": "B1S", "note-B1_sixteenth.": "B1S.", "note-B1_sixty_fourth": "B1s", "note-B1_sixty_fourth.": "B1s.", "note-B1_thirty_second": "B1T", "note-B1_thirty_second.": "B1T.", "note-B1_whole": "B1W", "note-B1_whole.": "B1W.", "note-B2_breve": "B2Y", "note-B2_double_whole": "B2w", "note-B2_eighth": "B2z", "note-B2_eighth.": "B2z.", "note-B2_half": "B2H", "note-B2_half.": "B2H.", "note-B2_half_fermata": "B2H^", "note-B2_hundred_twenty_eighth": "B2h", "note-B2_quarter": "B2q", "note-B2_quarter.": "B2q.", "note-B2_sixteenth": "B2S", "note-B2_sixteenth.": "B2S.", "note-B2_sixty_fourth": "B2s", "note-B2_sixty_fourth.": "B2s.", "note-B2_thirty_second": "B2T", "note-B2_thirty_second.": "B2T.", "note-B2_whole": "B2W", "note-B2_whole.": "B2W.", "note-B3_breve": "B3Y", "note-B3_breve.": "B3Y.", "note-B3_double_whole": "B3w", "note-B3_double_whole.": "B3w.", "note-B3_double_whole_fermata": "B3w^", "note-B3_eighth": "B3z", "note-B3_eighth.": "B3z.", "note-B3_eighth_fermata": "B3z^", "note-B3_half": "B3H", "note-B3_half.": "B3H.", "note-B3_half_fermata": "B3H^", "note-B3_hundred_twenty_eighth": "B3h", "note-B3_quarter": "B3q", "note-B3_quarter.": "B3q.", "note-B3_quarter..": "B3q..", "note-B3_quarter_fermata": "B3q^", "note-B3_sixteenth": "B3S", "note-B3_sixteenth.": "B3S.", "note-B3_sixty_fourth": "B3s", "note-B3_sixty_fourth.": "B3s.", "note-B3_thirty_second": "B3T", "note-B3_thirty_second.": "B3T.", "note-B3_whole": "B3W", "note-B3_whole.": "B3W.", "note-B3_whole_fermata": "B3W^", "note-B4_breve": "B4Y", "note-B4_breve.": "B4Y.", "note-B4_double_whole": "B4w", "note-B4_double_whole.": "B4w.", "note-B4_double_whole_fermata": "B4w^", "note-B4_eighth": "B4z", "note-B4_eighth.": "B4z.", "note-B4_eighth..": "B4z..", "note-B4_eighth._fermata": "B4z.^", "note-B4_eighth_fermata": "B4z^", "note-B4_half": "B4H", "note-B4_half.": "B4H.", "note-B4_half._fermata": "B4H.^", "note-B4_half_fermata": "B4H^", "note-B4_hundred_twenty_eighth": "B4h", "note-B4_long": "B4long", "note-B4_quadruple_whole": "B4Q", "note-B4_quarter": "B4q", "note-B4_quarter.": "B4q.", "note-B4_quarter..": "B4q..", "note-B4_quarter._fermata": "B4q.^", "note-B4_quarter_fermata": "B4q^", "note-B4_sixteenth": "B4S", "note-B4_sixteenth.": "B4S.", "note-B4_sixteenth._fermata": "B4S.^", "note-B4_sixteenth_fermata": "B4S^", "note-B4_sixty_fourth": "B4s", "note-B4_sixty_fourth.": "B4s.", "note-B4_thirty_second": "B4T", "note-B4_thirty_second.": "B4T.", "note-B4_whole": "B4W", "note-B4_whole.": "B4W.", "note-B4_whole._fermata": "B4W.^", "note-B4_whole_fermata": "B4W^", "note-B5_breve": "B5Y", "note-B5_breve.": "B5Y.", "note-B5_double_whole": "B5w", "note-B5_eighth": "B5z", "note-B5_eighth.": "B5z.", "note-B5_eighth..": "B5z..", "note-B5_half": "B5H", "note-B5_half.": "B5H.", "note-B5_half_fermata": "B5H^", "note-B5_hundred_twenty_eighth": "B5h", "note-B5_quarter": "B5q", "note-B5_quarter.": "B5q.", "note-B5_quarter..": "B5q..", "note-B5_sixteenth": "B5S", "note-B5_sixteenth.": "B5S.", "note-B5_sixty_fourth": "B5s", "note-B5_sixty_fourth.": "B5s.", "note-B5_thirty_second": "B5T", "note-B5_thirty_second.": "B5T.", "note-B5_whole": "B5W", "note-B5_whole.": "B5W.", "note-B6_breve": "B6Y", "note-B6_breve.": "B6Y.", "note-B6_eighth": "B6z", "note-B6_eighth.": "B6z.", "note-B6_half": "B6H", "note-B6_half.": "B6H.", "note-B6_hundred_twenty_eighth": "B6h", "note-B6_quarter": "B6q", "note-B6_quarter.": "B6q.", "note-B6_sixteenth": "B6S", "note-B6_sixteenth.": "B6S.", "note-B6_sixty_fourth": "B6s", "note-B6_sixty_fourth.": "B6s.", "note-B6_thirty_second": "B6T", "note-B6_thirty_second.": "B6T.", "note-B6_whole": "B6W", "note-B6_whole.": "B6W.", "note-B7_eighth": "B7z", "note-B7_half": "B7H", "note-B7_half.": "B7H.", "note-B7_hundred_twenty_eighth": "B7h", "note-B7_quarter": "B7q", "note-B7_quarter.": "B7q.", "note-B7_sixteenth": "B7S", "note-B7_sixty_fourth": "B7s", "note-B7_thirty_second": "B7T", "note-B7_whole": "B7W", "note-B8_eighth": "B8z", "note-B8_eighth.": "B8z.", "note-B8_quarter": "B8q", "note-B8_sixteenth": "B8S", "note-B8_sixty_fourth.": "B8s.", "note-BN0_eighth": "BN0z", "note-BN0_eighth.": "BN0z.", "note-BN0_half": "BN0H", "note-BN0_quarter": "BN0q", "note-BN0_sixteenth": "BN0S", "note-BN0_whole": "BN0W", "note-BN1_eighth": "BN1z", "note-BN1_eighth.": "BN1z.", "note-BN1_half": "BN1H", "note-BN1_half.": "BN1H.", "note-BN1_hundred_twenty_eighth": "BN1h", "note-BN1_quarter": "BN1q", "note-BN1_quarter.": "BN1q.", "note-BN1_sixteenth": "BN1S", "note-BN1_sixty_fourth": "BN1s", "note-BN1_sixty_fourth.": "BN1s.", "note-BN1_thirty_second": "BN1T", "note-BN1_thirty_second.": "BN1T.", "note-BN1_whole": "BN1W", "note-BN1_whole.": "BN1W.", "note-BN2_eighth": "BN2z", "note-BN2_eighth.": "BN2z.", "note-BN2_half": "BN2H", "note-BN2_half.": "BN2H.", "note-BN2_hundred_twenty_eighth": "BN2h", "note-BN2_quarter": "BN2q", "note-BN2_quarter.": "BN2q.", "note-BN2_sixteenth": "BN2S", "note-BN2_sixteenth.": "BN2S.", "note-BN2_sixty_fourth": "BN2s", "note-BN2_sixty_fourth.": "BN2s.", "note-BN2_thirty_second": "BN2T", "note-BN2_thirty_second.": "BN2T.", "note-BN2_whole": "BN2W", "note-BN2_whole.": "BN2W.", "note-BN3_breve": "BN3Y", "note-BN3_eighth": "BN3z", "note-BN3_eighth.": "BN3z.", "note-BN3_half": "BN3H", "note-BN3_half.": "BN3H.", "note-BN3_quarter": "BN3q", "note-BN3_quarter.": "BN3q.", "note-BN3_sixteenth": "BN3S", "note-BN3_sixteenth.": "BN3S.", "note-BN3_sixty_fourth": "BN3s", "note-BN3_sixty_fourth.": "BN3s.", "note-BN3_thirty_second": "BN3T", "note-BN3_whole": "BN3W", "note-BN3_whole.": "BN3W.", "note-BN4_eighth": "BN4z", "note-BN4_eighth.": "BN4z.", "note-BN4_half": "BN4H", "note-BN4_half.": "BN4H.", "note-BN4_quarter": "BN4q", "note-BN4_quarter.": "BN4q.", "note-BN4_sixteenth": "BN4S", "note-BN4_sixteenth.": "BN4S.", "note-BN4_sixty_fourth": "BN4s", "note-BN4_sixty_fourth.": "BN4s.", "note-BN4_thirty_second": "BN4T", "note-BN4_thirty_second.": "BN4T.", "note-BN4_whole": "BN4W", "note-BN4_whole.": "BN4W.", "note-BN5_eighth": "BN5z", "note-BN5_eighth.": "BN5z.", "note-BN5_half": "BN5H", "note-BN5_half.": "BN5H.", "note-BN5_quarter": "BN5q", "note-BN5_quarter.": "BN5q.", "note-BN5_sixteenth": "BN5S", "note-BN5_sixteenth.": "BN5S.", "note-BN5_sixty_fourth": "BN5s", "note-BN5_thirty_second": "BN5T", "note-BN5_thirty_second.": "BN5T.", "note-BN5_whole": "BN5W", "note-BN5_whole.": "BN5W.", "note-BN6_eighth": "BN6z", "note-BN6_eighth.": "BN6z.", "note-BN6_half": "BN6H", "note-BN6_hundred_twenty_eighth": "BN6h", "note-BN6_quarter": "BN6q", "note-BN6_quarter.": "BN6q.", "note-BN6_sixteenth": "BN6S", "note-BN6_sixty_fourth": "BN6s", "note-BN6_sixty_fourth.": "BN6s.", "note-BN6_thirty_second": "BN6T", "note-BN6_whole": "BN6W", "note-BN7_sixteenth": "BN7S", "note-BN8_quarter": "BN8q", "note-Bb0_eighth": "Bb0z", "note-Bb0_eighth.": "Bb0z.", "note-Bb0_half": "Bb0H", "note-Bb0_half.": "Bb0H.", "note-Bb0_quarter": "Bb0q", "note-Bb0_quarter.": "Bb0q.", "note-Bb0_sixteenth": "Bb0S", "note-Bb0_thirty_second": "Bb0T", "note-Bb0_whole": "Bb0W", "note-Bb1_eighth": "Bb1z", "note-Bb1_eighth.": "Bb1z.", "note-Bb1_half": "Bb1H", "note-Bb1_half.": "Bb1H.", "note-Bb1_quarter": "Bb1q", "note-Bb1_quarter.": "Bb1q.", "note-Bb1_sixteenth": "Bb1S", "note-Bb1_sixteenth.": "Bb1S.", "note-Bb1_sixty_fourth": "Bb1s", "note-Bb1_thirty_second": "Bb1T", "note-Bb1_thirty_second.": "Bb1T.", "note-Bb1_whole": "Bb1W", "note-Bb1_whole.": "Bb1W.", "note-Bb2_double_whole": "Bb2w", "note-Bb2_eighth": "Bb2z", "note-Bb2_eighth.": "Bb2z.", "note-Bb2_half": "Bb2H", "note-Bb2_half.": "Bb2H.", "note-Bb2_quarter": "Bb2q", "note-Bb2_quarter.": "Bb2q.", "note-Bb2_quarter._fermata": "Bb2q.^", "note-Bb2_quarter_fermata": "Bb2q^", "note-Bb2_sixteenth": "Bb2S", "note-Bb2_sixteenth.": "Bb2S.", "note-Bb2_sixteenth_fermata": "Bb2S^", "note-Bb2_sixty_fourth": "Bb2s", "note-Bb2_sixty_fourth.": "Bb2s.", "note-Bb2_thirty_second": "Bb2T", "note-Bb2_thirty_second.": "Bb2T.", "note-Bb2_whole": "Bb2W", "note-Bb2_whole.": "Bb2W.", "note-Bb3_double_whole": "Bb3w", "note-Bb3_double_whole.": "Bb3w.", "note-Bb3_eighth": "Bb3z", "note-Bb3_eighth.": "Bb3z.", "note-Bb3_eighth..": "Bb3z..", "note-Bb3_half": "Bb3H", "note-Bb3_half.": "Bb3H.", "note-Bb3_half._fermata": "Bb3H.^", "note-Bb3_half_fermata": "Bb3H^", "note-Bb3_quadruple_whole": "Bb3Q", "note-Bb3_quarter": "Bb3q", "note-Bb3_quarter.": "Bb3q.", "note-Bb3_quarter..": "Bb3q..", "note-Bb3_quarter._fermata": "Bb3q.^", "note-Bb3_quarter_fermata": "Bb3q^", "note-Bb3_sixteenth": "Bb3S", "note-Bb3_sixteenth.": "Bb3S.", "note-Bb3_sixty_fourth": "Bb3s", "note-Bb3_sixty_fourth.": "Bb3s.", "note-Bb3_thirty_second": "Bb3T", "note-Bb3_thirty_second.": "Bb3T.", "note-Bb3_whole": "Bb3W", "note-Bb3_whole.": "Bb3W.", "note-Bb3_whole_fermata": "Bb3W^", "note-Bb4_double_whole": "Bb4w", "note-Bb4_double_whole.": "Bb4w.", "note-Bb4_eighth": "Bb4z", "note-Bb4_eighth.": "Bb4z.", "note-Bb4_eighth..": "Bb4z..", "note-Bb4_eighth_fermata": "Bb4z^", "note-Bb4_half": "Bb4H", "note-Bb4_half.": "Bb4H.", "note-Bb4_half._fermata": "Bb4H.^", "note-Bb4_half_fermata": "Bb4H^", "note-Bb4_hundred_twenty_eighth": "Bb4h", "note-Bb4_quadruple_whole": "Bb4Q", "note-Bb4_quarter": "Bb4q", "note-Bb4_quarter.": "Bb4q.", "note-Bb4_quarter..": "Bb4q..", "note-Bb4_quarter._fermata": "Bb4q.^", "note-Bb4_quarter_fermata": "Bb4q^", "note-Bb4_sixteenth": "Bb4S", "note-Bb4_sixteenth.": "Bb4S.", "note-Bb4_sixty_fourth": "Bb4s", "note-Bb4_sixty_fourth.": "Bb4s.", "note-Bb4_thirty_second": "Bb4T", "note-Bb4_thirty_second.": "Bb4T.", "note-Bb4_whole": "Bb4W", "note-Bb4_whole.": "Bb4W.", "note-Bb4_whole._fermata": "Bb4W.^", "note-Bb4_whole_fermata": "Bb4W^", "note-Bb5_double_whole": "Bb5w", "note-Bb5_eighth": "Bb5z", "note-Bb5_eighth.": "Bb5z.", "note-Bb5_eighth..": "Bb5z..", "note-Bb5_half": "Bb5H", "note-Bb5_half.": "Bb5H.", "note-Bb5_half_fermata": "Bb5H^", "note-Bb5_quarter": "Bb5q", "note-Bb5_quarter.": "Bb5q.", "note-Bb5_quarter..": "Bb5q..", "note-Bb5_quarter_fermata": "Bb5q^", "note-Bb5_sixteenth": "Bb5S", "note-Bb5_sixteenth.": "Bb5S.", "note-Bb5_sixty_fourth": "Bb5s", "note-Bb5_sixty_fourth.": "Bb5s.", "note-Bb5_thirty_second": "Bb5T", "note-Bb5_thirty_second.": "Bb5T.", "note-Bb5_whole": "Bb5W", "note-Bb5_whole.": "Bb5W.", "note-Bb5_whole_fermata": "Bb5W^", "note-Bb6_eighth": "Bb6z", "note-Bb6_eighth.": "Bb6z.", "note-Bb6_half": "Bb6H", "note-Bb6_half.": "Bb6H.", "note-Bb6_quarter": "Bb6q", "note-Bb6_quarter.": "Bb6q.", "note-Bb6_sixteenth": "Bb6S", "note-Bb6_sixty_fourth": "Bb6s", "note-Bb6_thirty_second": "Bb6T", "note-Bb6_whole": "Bb6W", "note-Bb7_eighth": "Bb7z", "note-Bb7_quarter": "Bb7q", "note-Bb7_sixteenth": "Bb7S", "note-Bb7_thirty_second": "Bb7T", "note-Bb8_eighth": "Bb8z", "note-Bb8_eighth.": "Bb8z.", "note-Bb8_quarter": "Bb8q", "note-Bbb0_sixteenth": "Bbb0S", "note-Bbb1_eighth": "Bbb1z", "note-Bbb1_half": "Bbb1H", "note-Bbb1_half.": "Bbb1H.", "note-Bbb1_quarter": "Bbb1q", "note-Bbb1_quarter.": "Bbb1q.", "note-Bbb1_sixteenth": "Bbb1S", "note-Bbb1_whole": "Bbb1W", "note-Bbb2_eighth": "Bbb2z", "note-Bbb2_eighth.": "Bbb2z.", "note-Bbb2_half": "Bbb2H", "note-Bbb2_half.": "Bbb2H.", "note-Bbb2_quarter": "Bbb2q", "note-Bbb2_quarter.": "Bbb2q.", "note-Bbb2_sixteenth": "Bbb2S", "note-Bbb2_thirty_second": "Bbb2T", "note-Bbb2_thirty_second.": "Bbb2T.", "note-Bbb2_whole": "Bbb2W", "note-Bbb3_eighth": "Bbb3z", "note-Bbb3_half": "Bbb3H", "note-Bbb3_half.": "Bbb3H.", "note-Bbb3_quarter": "Bbb3q", "note-Bbb3_quarter.": "Bbb3q.", "note-Bbb3_sixteenth": "Bbb3S", "note-Bbb3_sixteenth.": "Bbb3S.", "note-Bbb3_thirty_second": "Bbb3T", "note-Bbb3_whole": "Bbb3W", "note-Bbb4_eighth": "Bbb4z", "note-Bbb4_eighth.": "Bbb4z.", "note-Bbb4_half": "Bbb4H", "note-Bbb4_half.": "Bbb4H.", "note-Bbb4_quarter": "Bbb4q", "note-Bbb4_quarter.": "Bbb4q.", "note-Bbb4_sixteenth": "Bbb4S", "note-Bbb4_thirty_second": "Bbb4T", "note-Bbb4_whole": "Bbb4W", "note-Bbb5_eighth": "Bbb5z", "note-Bbb5_eighth.": "Bbb5z.", "note-Bbb5_half": "Bbb5H", "note-Bbb5_half.": "Bbb5H.", "note-Bbb5_quarter": "Bbb5q", "note-Bbb5_quarter.": "Bbb5q.", "note-Bbb5_sixteenth": "Bbb5S", "note-Bbb5_thirty_second": "Bbb5T", "note-Bbb5_whole": "Bbb5W", "note-Bbb6_eighth": "Bbb6z", "note-Bbb6_eighth.": "Bbb6z.", "note-Bbb6_sixteenth": "Bbb6S", "note-Bbb6_sixty_fourth": "Bbb6s", "note-Bbb6_thirty_second": "Bbb6T", "note-C##1_sixteenth": "C##1S", "note-C##2_eighth": "C##2z", "note-C##2_quarter": "C##2q", "note-C##2_quarter.": "C##2q.", "note-C##2_sixteenth": "C##2S", "note-C##2_whole": "C##2W", "note-C##3_eighth": "C##3z", "note-C##3_half": "C##3H", "note-C##3_quarter": "C##3q", "note-C##3_quarter.": "C##3q.", "note-C##3_sixteenth": "C##3S", "note-C##3_sixty_fourth": "C##3s", "note-C##4_eighth": "C##4z", "note-C##4_eighth.": "C##4z.", "note-C##4_half": "C##4H", "note-C##4_half.": "C##4H.", "note-C##4_quarter": "C##4q", "note-C##4_quarter.": "C##4q.", "note-C##4_sixteenth": "C##4S", "note-C##4_sixty_fourth": "C##4s", "note-C##4_thirty_second": "C##4T", "note-C##4_whole": "C##4W", "note-C##5_eighth": "C##5z", "note-C##5_eighth.": "C##5z.", "note-C##5_half.": "C##5H.", "note-C##5_quarter": "C##5q", "note-C##5_quarter.": "C##5q.", "note-C##5_sixteenth": "C##5S", "note-C##5_thirty_second": "C##5T", "note-C##5_whole": "C##5W", "note-C##6_eighth": "C##6z", "note-C##6_quarter": "C##6q", "note-C##6_quarter.": "C##6q.", "note-C##6_sixteenth": "C##6S", "note-C##6_thirty_second": "C##6T", "note-C##7_eighth": "C##7z", "note-C#1_eighth": "C#1z", "note-C#1_eighth.": "C#1z.", "note-C#1_half.": "C#1H.", "note-C#1_quarter": "C#1q", "note-C#1_sixteenth": "C#1S", "note-C#1_thirty_second": "C#1T", "note-C#1_whole": "C#1W", "note-C#2_eighth": "C#2z", "note-C#2_eighth.": "C#2z.", "note-C#2_half": "C#2H", "note-C#2_half.": "C#2H.", "note-C#2_quarter": "C#2q", "note-C#2_quarter.": "C#2q.", "note-C#2_sixteenth": "C#2S", "note-C#2_sixteenth.": "C#2S.", "note-C#2_sixty_fourth": "C#2s", "note-C#2_thirty_second": "C#2T", "note-C#2_whole": "C#2W", "note-C#2_whole.": "C#2W.", "note-C#3_double_whole": "C#3w", "note-C#3_eighth": "C#3z", "note-C#3_eighth.": "C#3z.", "note-C#3_half": "C#3H", "note-C#3_half.": "C#3H.", "note-C#3_quarter": "C#3q", "note-C#3_quarter.": "C#3q.", "note-C#3_sixteenth": "C#3S", "note-C#3_sixteenth.": "C#3S.", "note-C#3_sixty_fourth": "C#3s", "note-C#3_thirty_second": "C#3T", "note-C#3_whole": "C#3W", "note-C#4_eighth": "C#4z", "note-C#4_eighth.": "C#4z.", "note-C#4_eighth..": "C#4z..", "note-C#4_eighth_fermata": "C#4z^", "note-C#4_half": "C#4H", "note-C#4_half.": "C#4H.", "note-C#4_half_fermata": "C#4H^", "note-C#4_quadruple_whole_fermata": "C#4Q^", "note-C#4_quarter": "C#4q", "note-C#4_quarter.": "C#4q.", "note-C#4_quarter..": "C#4q..", "note-C#4_quarter._fermata": "C#4q.^", "note-C#4_quarter_fermata": "C#4q^", "note-C#4_sixteenth": "C#4S", "note-C#4_sixteenth.": "C#4S.", "note-C#4_sixty_fourth": "C#4s", "note-C#4_thirty_second": "C#4T", "note-C#4_thirty_second.": "C#4T.", "note-C#4_whole": "C#4W", "note-C#4_whole.": "C#4W.", "note-C#4_whole_fermata": "C#4W^", "note-C#5_double_whole": "C#5w", "note-C#5_eighth": "C#5z", "note-C#5_eighth.": "C#5z.", "note-C#5_eighth..": "C#5z..", "note-C#5_eighth._fermata": "C#5z.^", "note-C#5_eighth_fermata": "C#5z^", "note-C#5_half": "C#5H", "note-C#5_half.": "C#5H.", "note-C#5_half._fermata": "C#5H.^", "note-C#5_half_fermata": "C#5H^", "note-C#5_quarter": "C#5q", "note-C#5_quarter.": "C#5q.", "note-C#5_quarter..": "C#5q..", "note-C#5_quarter._fermata": "C#5q.^", "note-C#5_quarter_fermata": "C#5q^", "note-C#5_sixteenth": "C#5S", "note-C#5_sixteenth.": "C#5S.", "note-C#5_sixteenth._fermata": "C#5S.^", "note-C#5_sixty_fourth": "C#5s", "note-C#5_sixty_fourth.": "C#5s.", "note-C#5_thirty_second": "C#5T", "note-C#5_thirty_second.": "C#5T.", "note-C#5_whole": "C#5W", "note-C#5_whole.": "C#5W.", "note-C#5_whole_fermata": "C#5W^", "note-C#6_eighth": "C#6z", "note-C#6_eighth.": "C#6z.", "note-C#6_half": "C#6H", "note-C#6_half.": "C#6H.", "note-C#6_half_fermata": "C#6H^", "note-C#6_quarter": "C#6q", "note-C#6_quarter.": "C#6q.", "note-C#6_quarter..": "C#6q..", "note-C#6_sixteenth": "C#6S", "note-C#6_sixteenth.": "C#6S.", "note-C#6_sixty_fourth": "C#6s", "note-C#6_thirty_second": "C#6T", "note-C#6_whole": "C#6W", "note-C#6_whole_fermata": "C#6W^", "note-C#7_eighth": "C#7z", "note-C#7_eighth.": "C#7z.", "note-C#7_half": "C#7H", "note-C#7_half.": "C#7H.", "note-C#7_quarter": "C#7q", "note-C#7_quarter.": "C#7q.", "note-C#7_sixteenth": "C#7S", "note-C#7_sixty_fourth": "C#7s", "note-C#7_thirty_second": "C#7T", "note-C#7_whole": "C#7W", "note-C#8_sixteenth": "C#8S", "note-C1_eighth": "C1z", "note-C1_eighth.": "C1z.", "note-C1_half": "C1H", "note-C1_half.": "C1H.", "note-C1_quarter": "C1q", "note-C1_quarter.": "C1q.", "note-C1_sixteenth": "C1S", "note-C1_sixteenth.": "C1S.", "note-C1_thirty_second": "C1T", "note-C1_whole": "C1W", "note-C2_breve": "C2Y", "note-C2_double_whole.": "C2w.", "note-C2_eighth": "C2z", "note-C2_eighth.": "C2z.", "note-C2_half": "C2H", "note-C2_half.": "C2H.", "note-C2_half_fermata": "C2H^", "note-C2_hundred_twenty_eighth": "C2h", "note-C2_quarter": "C2q", "note-C2_quarter.": "C2q.", "note-C2_sixteenth": "C2S", "note-C2_sixteenth.": "C2S.", "note-C2_sixty_fourth": "C2s", "note-C2_sixty_fourth.": "C2s.", "note-C2_thirty_second": "C2T", "note-C2_thirty_second.": "C2T.", "note-C2_whole": "C2W", "note-C2_whole.": "C2W.", "note-C3_breve": "C3Y", "note-C3_double_whole": "C3w", "note-C3_double_whole.": "C3w.", "note-C3_double_whole_fermata": "C3w^", "note-C3_eighth": "C3z", "note-C3_eighth.": "C3z.", "note-C3_half": "C3H", "note-C3_half.": "C3H.", "note-C3_half_fermata": "C3H^", "note-C3_hundred_twenty_eighth": "C3h", "note-C3_quadruple_whole": "C3Q", "note-C3_quadruple_whole.": "C3Q.", "note-C3_quarter": "C3q", "note-C3_quarter.": "C3q.", "note-C3_quarter_fermata": "C3q^", "note-C3_sixteenth": "C3S", "note-C3_sixteenth.": "C3S.", "note-C3_sixty_fourth": "C3s", "note-C3_sixty_fourth.": "C3s.", "note-C3_thirty_second": "C3T", "note-C3_thirty_second.": "C3T.", "note-C3_whole": "C3W", "note-C3_whole.": "C3W.", "note-C3_whole_fermata": "C3W^", "note-C4_breve": "C4Y", "note-C4_double_whole": "C4w", "note-C4_double_whole.": "C4w.", "note-C4_double_whole_fermata": "C4w^", "note-C4_eighth": "C4z", "note-C4_eighth.": "C4z.", "note-C4_eighth..": "C4z..", "note-C4_eighth_fermata": "C4z^", "note-C4_half": "C4H", "note-C4_half.": "C4H.", "note-C4_half._fermata": "C4H.^", "note-C4_half_fermata": "C4H^", "note-C4_hundred_twenty_eighth": "C4h", "note-C4_quadruple_whole": "C4Q", "note-C4_quadruple_whole.": "C4Q.", "note-C4_quarter": "C4q", "note-C4_quarter.": "C4q.", "note-C4_quarter..": "C4q..", "note-C4_quarter._fermata": "C4q.^", "note-C4_quarter_fermata": "C4q^", "note-C4_sixteenth": "C4S", "note-C4_sixteenth.": "C4S.", "note-C4_sixty_fourth": "C4s", "note-C4_sixty_fourth.": "C4s.", "note-C4_thirty_second": "C4T", "note-C4_thirty_second.": "C4T.", "note-C4_whole": "C4W", "note-C4_whole.": "C4W.", "note-C4_whole_fermata": "C4W^", "note-C5_breve": "C5Y", "note-C5_double_whole": "C5w", "note-C5_double_whole.": "C5w.", "note-C5_double_whole._fermata": "C5w.^", "note-C5_double_whole_fermata": "C5w^", "note-C5_eighth": "C5z", "note-C5_eighth.": "C5z.", "note-C5_eighth..": "C5z..", "note-C5_eighth._fermata": "C5z.^", "note-C5_eighth_fermata": "C5z^", "note-C5_half": "C5H", "note-C5_half.": "C5H.", "note-C5_half._fermata": "C5H.^", "note-C5_half_fermata": "C5H^", "note-C5_hundred_twenty_eighth": "C5h", "note-C5_quadruple_whole": "C5Q", "note-C5_quadruple_whole.": "C5Q.", "note-C5_quadruple_whole_fermata": "C5Q^", "note-C5_quarter": "C5q", "note-C5_quarter.": "C5q.", "note-C5_quarter..": "C5q..", "note-C5_quarter._fermata": "C5q.^", "note-C5_quarter_fermata": "C5q^", "note-C5_sixteenth": "C5S", "note-C5_sixteenth.": "C5S.", "note-C5_sixteenth_fermata": "C5S^", "note-C5_sixty_fourth": "C5s", "note-C5_sixty_fourth.": "C5s.", "note-C5_thirty_second": "C5T", "note-C5_thirty_second.": "C5T.", "note-C5_whole": "C5W", "note-C5_whole.": "C5W.", "note-C5_whole._fermata": "C5W.^", "note-C5_whole_fermata": "C5W^", "note-C6_breve": "C6Y", "note-C6_eighth": "C6z", "note-C6_eighth.": "C6z.", "note-C6_eighth..": "C6z..", "note-C6_half": "C6H", "note-C6_half.": "C6H.", "note-C6_half..": "C6H..", "note-C6_half._fermata": "C6H.^", "note-C6_half_fermata": "C6H^", "note-C6_hundred_twenty_eighth": "C6h", "note-C6_quarter": "C6q", "note-C6_quarter.": "C6q.", "note-C6_quarter..": "C6q..", "note-C6_sixteenth": "C6S", "note-C6_sixteenth.": "C6S.", "note-C6_sixty_fourth": "C6s", "note-C6_sixty_fourth.": "C6s.", "note-C6_thirty_second": "C6T", "note-C6_thirty_second.": "C6T.", "note-C6_whole": "C6W", "note-C6_whole.": "C6W.", "note-C6_whole_fermata": "C6W^", "note-C7_eighth": "C7z", "note-C7_eighth.": "C7z.", "note-C7_half": "C7H", "note-C7_half.": "C7H.", "note-C7_hundred_twenty_eighth": "C7h", "note-C7_quarter": "C7q", "note-C7_quarter.": "C7q.", "note-C7_sixteenth": "C7S", "note-C7_sixteenth.": "C7S.", "note-C7_sixty_fourth": "C7s", "note-C7_sixty_fourth.": "C7s.", "note-C7_thirty_second": "C7T", "note-C7_thirty_second.": "C7T.", "note-C7_whole": "C7W", "note-C7_whole.": "C7W.", "note-C8_breve": "C8Y", "note-C8_eighth": "C8z", "note-C8_eighth.": "C8z.", "note-C8_half.": "C8H.", "note-C8_hundred_twenty_eighth": "C8h", "note-C8_quarter": "C8q", "note-C8_sixteenth": "C8S", "note-C8_sixteenth.": "C8S.", "note-C8_sixty_fourth": "C8s", "note-C8_thirty_second": "C8T", "note-C9_eighth": "C9z", "note-C9_quarter": "C9q", "note-C9_sixteenth.": "C9S.", "note-CN1_eighth": "CN1z", "note-CN1_half": "CN1H", "note-CN1_half.": "CN1H.", "note-CN1_quarter": "CN1q", "note-CN1_quarter.": "CN1q.", "note-CN1_sixteenth": "CN1S", "note-CN1_thirty_second": "CN1T", "note-CN1_whole": "CN1W", "note-CN2_eighth": "CN2z", "note-CN2_eighth.": "CN2z.", "note-CN2_half": "CN2H", "note-CN2_half.": "CN2H.", "note-CN2_quarter": "CN2q", "note-CN2_quarter.": "CN2q.", "note-CN2_sixteenth": "CN2S", "note-CN2_sixty_fourth": "CN2s", "note-CN2_thirty_second": "CN2T", "note-CN2_whole": "CN2W", "note-CN2_whole.": "CN2W.", "note-CN3_eighth": "CN3z", "note-CN3_eighth.": "CN3z.", "note-CN3_half": "CN3H", "note-CN3_half.": "CN3H.", "note-CN3_quarter": "CN3q", "note-CN3_quarter.": "CN3q.", "note-CN3_sixteenth": "CN3S", "note-CN3_sixteenth.": "CN3S.", "note-CN3_sixty_fourth": "CN3s", "note-CN3_thirty_second": "CN3T", "note-CN3_whole": "CN3W", "note-CN3_whole.": "CN3W.", "note-CN4_eighth": "CN4z", "note-CN4_eighth.": "CN4z.", "note-CN4_half": "CN4H", "note-CN4_half.": "CN4H.", "note-CN4_hundred_twenty_eighth": "CN4h", "note-CN4_quarter": "CN4q", "note-CN4_quarter.": "CN4q.", "note-CN4_sixteenth": "CN4S", "note-CN4_sixteenth.": "CN4S.", "note-CN4_sixty_fourth": "CN4s", "note-CN4_thirty_second": "CN4T", "note-CN4_whole": "CN4W", "note-CN5_eighth": "CN5z", "note-CN5_eighth.": "CN5z.", "note-CN5_half": "CN5H", "note-CN5_half.": "CN5H.", "note-CN5_quarter": "CN5q", "note-CN5_quarter.": "CN5q.", "note-CN5_sixteenth": "CN5S", "note-CN5_sixteenth.": "CN5S.", "note-CN5_sixty_fourth": "CN5s", "note-CN5_thirty_second": "CN5T", "note-CN5_whole": "CN5W", "note-CN5_whole.": "CN5W.", "note-CN6_eighth": "CN6z", "note-CN6_eighth.": "CN6z.", "note-CN6_half": "CN6H", "note-CN6_half.": "CN6H.", "note-CN6_quarter": "CN6q", "note-CN6_quarter.": "CN6q.", "note-CN6_sixteenth": "CN6S", "note-CN6_sixteenth.": "CN6S.", "note-CN6_sixty_fourth": "CN6s", "note-CN6_thirty_second": "CN6T", "note-CN6_whole": "CN6W", "note-CN7_eighth": "CN7z", "note-CN7_eighth.": "CN7z.", "note-CN7_half": "CN7H", "note-CN7_half.": "CN7H.", "note-CN7_quarter": "CN7q", "note-CN7_quarter.": "CN7q.", "note-CN7_sixteenth": "CN7S", "note-CN7_sixty_fourth": "CN7s", "note-CN7_thirty_second": "CN7T", "note-CN8_eighth": "CN8z", "note-CN8_quarter": "CN8q", "note-CN8_sixteenth": "CN8S", "note-CN8_thirty_second": "CN8T", "note-Cb1_half": "Cb1H", "note-Cb1_sixteenth": "Cb1S", "note-Cb1_whole": "Cb1W", "note-Cb2_eighth": "Cb2z", "note-Cb2_eighth.": "Cb2z.", "note-Cb2_half": "Cb2H", "note-Cb2_half.": "Cb2H.", "note-Cb2_quarter": "Cb2q", "note-Cb2_quarter.": "Cb2q.", "note-Cb2_sixteenth": "Cb2S", "note-Cb2_thirty_second": "Cb2T", "note-Cb2_thirty_second.": "Cb2T.", "note-Cb2_whole": "Cb2W", "note-Cb3_eighth": "Cb3z", "note-Cb3_eighth.": "Cb3z.", "note-Cb3_half": "Cb3H", "note-Cb3_half.": "Cb3H.", "note-Cb3_quarter": "Cb3q", "note-Cb3_quarter.": "Cb3q.", "note-Cb3_sixteenth": "Cb3S", "note-Cb3_sixteenth.": "Cb3S.", "note-Cb3_sixty_fourth": "Cb3s", "note-Cb3_sixty_fourth.": "Cb3s.", "note-Cb3_thirty_second": "Cb3T", "note-Cb3_thirty_second.": "Cb3T.", "note-Cb3_whole": "Cb3W", "note-Cb4_eighth": "Cb4z", "note-Cb4_eighth.": "Cb4z.", "note-Cb4_half": "Cb4H", "note-Cb4_half.": "Cb4H.", "note-Cb4_hundred_twenty_eighth": "Cb4h", "note-Cb4_quarter": "Cb4q", "note-Cb4_quarter.": "Cb4q.", "note-Cb4_sixteenth": "Cb4S", "note-Cb4_sixteenth.": "Cb4S.", "note-Cb4_sixty_fourth": "Cb4s", "note-Cb4_sixty_fourth.": "Cb4s.", "note-Cb4_thirty_second": "Cb4T", "note-Cb4_thirty_second.": "Cb4T.", "note-Cb4_whole": "Cb4W", "note-Cb4_whole.": "Cb4W.", "note-Cb5_eighth": "Cb5z", "note-Cb5_eighth.": "Cb5z.", "note-Cb5_half": "Cb5H", "note-Cb5_half.": "Cb5H.", "note-Cb5_quarter": "Cb5q", "note-Cb5_quarter.": "Cb5q.", "note-Cb5_sixteenth": "Cb5S", "note-Cb5_sixteenth.": "Cb5S.", "note-Cb5_sixty_fourth": "Cb5s", "note-Cb5_thirty_second": "Cb5T", "note-Cb5_whole": "Cb5W", "note-Cb5_whole.": "Cb5W.", "note-Cb6_eighth": "Cb6z", "note-Cb6_eighth.": "Cb6z.", "note-Cb6_half": "Cb6H", "note-Cb6_half.": "Cb6H.", "note-Cb6_quarter": "Cb6q", "note-Cb6_quarter.": "Cb6q.", "note-Cb6_sixteenth": "Cb6S", "note-Cb6_sixteenth.": "Cb6S.", "note-Cb6_sixty_fourth": "Cb6s", "note-Cb6_thirty_second": "Cb6T", "note-Cb6_thirty_second.": "Cb6T.", "note-Cb6_whole": "Cb6W", "note-Cb6_whole.": "Cb6W.", "note-Cb7_eighth": "Cb7z", "note-Cb7_half": "Cb7H", "note-Cb7_half.": "Cb7H.", "note-Cb7_quarter": "Cb7q", "note-Cb7_quarter.": "Cb7q.", "note-Cb7_sixteenth": "Cb7S", "note-Cb7_sixteenth.": "Cb7S.", "note-Cb7_sixty_fourth": "Cb7s", "note-Cb7_thirty_second": "Cb7T", "note-Cb8_eighth": "Cb8z", "note-Cbb3_quarter": "Cbb3q", "note-D##1_sixteenth": "D##1S", "note-D##2_sixteenth": "D##2S", "note-D##3_eighth": "D##3z", "note-D##3_quarter": "D##3q", "note-D##3_sixteenth": "D##3S", "note-D##4_eighth": "D##4z", "note-D##4_half": "D##4H", "note-D##4_sixteenth": "D##4S", "note-D##5_eighth": "D##5z", "note-D##5_half": "D##5H", "note-D##5_half.": "D##5H.", "note-D##5_quarter": "D##5q", "note-D##5_sixteenth": "D##5S", "note-D##6_eighth": "D##6z", "note-D##6_sixteenth": "D##6S", "note-D##7_eighth": "D##7z", "note-D#1_eighth": "D#1z", "note-D#1_half": "D#1H", "note-D#1_quarter": "D#1q", "note-D#1_sixteenth": "D#1S", "note-D#1_thirty_second": "D#1T", "note-D#1_whole": "D#1W", "note-D#2_eighth": "D#2z", "note-D#2_eighth.": "D#2z.", "note-D#2_half": "D#2H", "note-D#2_half.": "D#2H.", "note-D#2_quarter": "D#2q", "note-D#2_quarter.": "D#2q.", "note-D#2_sixteenth": "D#2S", "note-D#2_sixty_fourth": "D#2s", "note-D#2_thirty_second": "D#2T", "note-D#2_whole": "D#2W", "note-D#2_whole.": "D#2W.", "note-D#3_eighth": "D#3z", "note-D#3_eighth.": "D#3z.", "note-D#3_half": "D#3H", "note-D#3_half.": "D#3H.", "note-D#3_half_fermata": "D#3H^", "note-D#3_quarter": "D#3q", "note-D#3_quarter.": "D#3q.", "note-D#3_sixteenth": "D#3S", "note-D#3_sixteenth.": "D#3S.", "note-D#3_sixty_fourth": "D#3s", "note-D#3_thirty_second": "D#3T", "note-D#3_whole": "D#3W", "note-D#3_whole.": "D#3W.", "note-D#4_breve": "D#4Y", "note-D#4_eighth": "D#4z", "note-D#4_eighth.": "D#4z.", "note-D#4_half": "D#4H", "note-D#4_half.": "D#4H.", "note-D#4_hundred_twenty_eighth": "D#4h", "note-D#4_quarter": "D#4q", "note-D#4_quarter.": "D#4q.", "note-D#4_sixteenth": "D#4S", "note-D#4_sixteenth.": "D#4S.", "note-D#4_sixty_fourth": "D#4s", "note-D#4_thirty_second": "D#4T", "note-D#4_thirty_second.": "D#4T.", "note-D#4_whole": "D#4W", "note-D#4_whole.": "D#4W.", "note-D#5_double_whole": "D#5w", "note-D#5_eighth": "D#5z", "note-D#5_eighth.": "D#5z.", "note-D#5_eighth..": "D#5z..", "note-D#5_eighth_fermata": "D#5z^", "note-D#5_half": "D#5H", "note-D#5_half.": "D#5H.", "note-D#5_half_fermata": "D#5H^", "note-D#5_quarter": "D#5q", "note-D#5_quarter.": "D#5q.", "note-D#5_quarter..": "D#5q..", "note-D#5_quarter_fermata": "D#5q^", "note-D#5_sixteenth": "D#5S", "note-D#5_sixteenth.": "D#5S.", "note-D#5_sixty_fourth": "D#5s", "note-D#5_thirty_second": "D#5T", "note-D#5_thirty_second.": "D#5T.", "note-D#5_whole": "D#5W", "note-D#5_whole.": "D#5W.", "note-D#5_whole_fermata": "D#5W^", "note-D#6_eighth": "D#6z", "note-D#6_eighth.": "D#6z.", "note-D#6_eighth..": "D#6z..", "note-D#6_half": "D#6H", "note-D#6_half.": "D#6H.", "note-D#6_quarter": "D#6q", "note-D#6_quarter.": "D#6q.", "note-D#6_sixteenth": "D#6S", "note-D#6_sixty_fourth": "D#6s", "note-D#6_thirty_second": "D#6T", "note-D#6_thirty_second.": "D#6T.", "note-D#6_whole": "D#6W", "note-D#7_eighth": "D#7z", "note-D#7_eighth.": "D#7z.", "note-D#7_half": "D#7H", "note-D#7_half.": "D#7H.", "note-D#7_quarter": "D#7q", "note-D#7_quarter.": "D#7q.", "note-D#7_sixteenth": "D#7S", "note-D#7_sixty_fourth": "D#7s", "note-D#7_thirty_second": "D#7T", "note-D#7_whole": "D#7W", "note-D#8_eighth": "D#8z", "note-D#8_quarter": "D#8q", "note-D#8_sixteenth": "D#8S", "note-D#9_sixteenth": "D#9S", "note-D1_eighth": "D1z", "note-D1_eighth.": "D1z.", "note-D1_half": "D1H", "note-D1_half.": "D1H.", "note-D1_hundred_twenty_eighth": "D1h", "note-D1_quarter": "D1q", "note-D1_quarter.": "D1q.", "note-D1_sixteenth": "D1S", "note-D1_sixteenth.": "D1S.", "note-D1_sixty_fourth": "D1s", "note-D1_thirty_second": "D1T", "note-D1_whole": "D1W", "note-D1_whole.": "D1W.", "note-D2_breve.": "D2Y.", "note-D2_double_whole": "D2w", "note-D2_eighth": "D2z", "note-D2_eighth.": "D2z.", "note-D2_half": "D2H", "note-D2_half.": "D2H.", "note-D2_half_fermata": "D2H^", "note-D2_hundred_twenty_eighth": "D2h", "note-D2_quarter": "D2q", "note-D2_quarter.": "D2q.", "note-D2_quarter._fermata": "D2q.^", "note-D2_sixteenth": "D2S", "note-D2_sixteenth.": "D2S.", "note-D2_sixty_fourth": "D2s", "note-D2_sixty_fourth.": "D2s.", "note-D2_thirty_second": "D2T", "note-D2_thirty_second.": "D2T.", "note-D2_whole": "D2W", "note-D2_whole.": "D2W.", "note-D3_breve": "D3Y", "note-D3_breve.": "D3Y.", "note-D3_double_whole": "D3w", "note-D3_double_whole.": "D3w.", "note-D3_double_whole_fermata": "D3w^", "note-D3_eighth": "D3z", "note-D3_eighth.": "D3z.", "note-D3_eighth_fermata": "D3z^", "note-D3_half": "D3H", "note-D3_half.": "D3H.", "note-D3_half_fermata": "D3H^", "note-D3_hundred_twenty_eighth": "D3h", "note-D3_quadruple_whole": "D3Q", "note-D3_quadruple_whole_fermata": "D3Q^", "note-D3_quarter": "D3q", "note-D3_quarter.": "D3q.", "note-D3_quarter..": "D3q..", "note-D3_quarter._fermata": "D3q.^", "note-D3_quarter_fermata": "D3q^", "note-D3_sixteenth": "D3S", "note-D3_sixteenth.": "D3S.", "note-D3_sixty_fourth": "D3s", "note-D3_sixty_fourth.": "D3s.", "note-D3_thirty_second": "D3T", "note-D3_thirty_second.": "D3T.", "note-D3_whole": "D3W", "note-D3_whole.": "D3W.", "note-D3_whole_fermata": "D3W^", "note-D4_breve": "D4Y", "note-D4_double_whole": "D4w", "note-D4_double_whole.": "D4w.", "note-D4_double_whole_fermata": "D4w^", "note-D4_eighth": "D4z", "note-D4_eighth.": "D4z.", "note-D4_eighth..": "D4z..", "note-D4_eighth._fermata": "D4z.^", "note-D4_eighth_fermata": "D4z^", "note-D4_half": "D4H", "note-D4_half.": "D4H.", "note-D4_half._fermata": "D4H.^", "note-D4_half_fermata": "D4H^", "note-D4_hundred_twenty_eighth": "D4h", "note-D4_quadruple_whole": "D4Q", "note-D4_quadruple_whole_fermata": "D4Q^", "note-D4_quarter": "D4q", "note-D4_quarter.": "D4q.", "note-D4_quarter..": "D4q..", "note-D4_quarter.._fermata": "D4q..^", "note-D4_quarter._fermata": "D4q.^", "note-D4_quarter_fermata": "D4q^", "note-D4_sixteenth": "D4S", "note-D4_sixteenth.": "D4S.", "note-D4_sixteenth_fermata": "D4S^", "note-D4_sixty_fourth": "D4s", "note-D4_sixty_fourth.": "D4s.", "note-D4_thirty_second": "D4T", "note-D4_thirty_second.": "D4T.", "note-D4_whole": "D4W", "note-D4_whole.": "D4W.", "note-D4_whole_fermata": "D4W^", "note-D5_breve": "D5Y", "note-D5_double_whole": "D5w", "note-D5_double_whole.": "D5w.", "note-D5_double_whole_fermata": "D5w^", "note-D5_eighth": "D5z", "note-D5_eighth.": "D5z.", "note-D5_eighth..": "D5z..", "note-D5_eighth._fermata": "D5z.^", "note-D5_eighth_fermata": "D5z^", "note-D5_half": "D5H", "note-D5_half.": "D5H.", "note-D5_half._fermata": "D5H.^", "note-D5_half_fermata": "D5H^", "note-D5_hundred_twenty_eighth": "D5h", "note-D5_long": "D5long", "note-D5_quadruple_whole": "D5Q", "note-D5_quadruple_whole_fermata": "D5Q^", "note-D5_quarter": "D5q", "note-D5_quarter.": "D5q.", "note-D5_quarter..": "D5q..", "note-D5_quarter._fermata": "D5q.^", "note-D5_quarter_fermata": "D5q^", "note-D5_sixteenth": "D5S", "note-D5_sixteenth.": "D5S.", "note-D5_sixty_fourth": "D5s", "note-D5_sixty_fourth.": "D5s.", "note-D5_thirty_second": "D5T", "note-D5_thirty_second.": "D5T.", "note-D5_whole": "D5W", "note-D5_whole.": "D5W.", "note-D5_whole._fermata": "D5W.^", "note-D5_whole_fermata": "D5W^", "note-D6_eighth": "D6z", "note-D6_eighth.": "D6z.", "note-D6_eighth..": "D6z..", "note-D6_eighth_fermata": "D6z^", "note-D6_half": "D6H", "note-D6_half.": "D6H.", "note-D6_half..": "D6H..", "note-D6_half_fermata": "D6H^", "note-D6_hundred_twenty_eighth": "D6h", "note-D6_quarter": "D6q", "note-D6_quarter.": "D6q.", "note-D6_quarter..": "D6q..", "note-D6_quarter._fermata": "D6q.^", "note-D6_quarter_fermata": "D6q^", "note-D6_sixteenth": "D6S", "note-D6_sixteenth.": "D6S.", "note-D6_sixty_fourth": "D6s", "note-D6_sixty_fourth.": "D6s.", "note-D6_thirty_second": "D6T", "note-D6_thirty_second.": "D6T.", "note-D6_whole": "D6W", "note-D6_whole.": "D6W.", "note-D6_whole_fermata": "D6W^", "note-D7_eighth": "D7z", "note-D7_eighth.": "D7z.", "note-D7_half": "D7H", "note-D7_half.": "D7H.", "note-D7_hundred_twenty_eighth": "D7h", "note-D7_quarter": "D7q", "note-D7_quarter.": "D7q.", "note-D7_sixteenth": "D7S", "note-D7_sixteenth.": "D7S.", "note-D7_sixty_fourth": "D7s", "note-D7_sixty_fourth.": "D7s.", "note-D7_thirty_second": "D7T", "note-D7_thirty_second.": "D7T.", "note-D7_whole": "D7W", "note-D7_whole.": "D7W.", "note-D8_eighth": "D8z", "note-D8_hundred_twenty_eighth": "D8h", "note-D8_quarter": "D8q", "note-D8_sixteenth": "D8S", "note-D8_sixteenth.": "D8S.", "note-D8_sixty_fourth": "D8s", "note-D8_thirty_second": "D8T", "note-DN1_eighth": "DN1z", "note-DN1_eighth.": "DN1z.", "note-DN1_half": "DN1H", "note-DN1_half.": "DN1H.", "note-DN1_quarter": "DN1q", "note-DN1_quarter.": "DN1q.", "note-DN1_sixteenth": "DN1S", "note-DN1_thirty_second": "DN1T", "note-DN1_whole": "DN1W", "note-DN2_eighth": "DN2z", "note-DN2_eighth.": "DN2z.", "note-DN2_half": "DN2H", "note-DN2_half.": "DN2H.", "note-DN2_quarter": "DN2q", "note-DN2_quarter.": "DN2q.", "note-DN2_sixteenth": "DN2S", "note-DN2_sixty_fourth": "DN2s", "note-DN2_thirty_second": "DN2T", "note-DN2_thirty_second.": "DN2T.", "note-DN2_whole": "DN2W", "note-DN2_whole.": "DN2W.", "note-DN3_eighth": "DN3z", "note-DN3_eighth.": "DN3z.", "note-DN3_half": "DN3H", "note-DN3_half.": "DN3H.", "note-DN3_quarter": "DN3q", "note-DN3_quarter.": "DN3q.", "note-DN3_sixteenth": "DN3S", "note-DN3_sixteenth.": "DN3S.", "note-DN3_sixty_fourth": "DN3s", "note-DN3_sixty_fourth.": "DN3s.", "note-DN3_thirty_second": "DN3T", "note-DN3_thirty_second.": "DN3T.", "note-DN3_whole": "DN3W", "note-DN3_whole.": "DN3W.", "note-DN4_eighth": "DN4z", "note-DN4_eighth.": "DN4z.", "note-DN4_half": "DN4H", "note-DN4_half.": "DN4H.", "note-DN4_hundred_twenty_eighth": "DN4h", "note-DN4_quarter": "DN4q", "note-DN4_quarter.": "DN4q.", "note-DN4_sixteenth": "DN4S", "note-DN4_sixteenth.": "DN4S.", "note-DN4_sixty_fourth": "DN4s", "note-DN4_sixty_fourth.": "DN4s.", "note-DN4_thirty_second": "DN4T", "note-DN4_whole": "DN4W", "note-DN5_eighth": "DN5z", "note-DN5_eighth.": "DN5z.", "note-DN5_half": "DN5H", "note-DN5_half.": "DN5H.", "note-DN5_hundred_twenty_eighth": "DN5h", "note-DN5_quarter": "DN5q", "note-DN5_quarter.": "DN5q.", "note-DN5_sixteenth": "DN5S", "note-DN5_sixteenth.": "DN5S.", "note-DN5_sixty_fourth": "DN5s", "note-DN5_sixty_fourth.": "DN5s.", "note-DN5_thirty_second": "DN5T", "note-DN5_thirty_second.": "DN5T.", "note-DN5_whole": "DN5W", "note-DN5_whole.": "DN5W.", "note-DN6_eighth": "DN6z", "note-DN6_eighth.": "DN6z.", "note-DN6_half": "DN6H", "note-DN6_half.": "DN6H.", "note-DN6_quarter": "DN6q", "note-DN6_quarter.": "DN6q.", "note-DN6_sixteenth": "DN6S", "note-DN6_sixteenth.": "DN6S.", "note-DN6_sixty_fourth": "DN6s", "note-DN6_thirty_second": "DN6T", "note-DN6_thirty_second.": "DN6T.", "note-DN6_whole": "DN6W", "note-DN6_whole.": "DN6W.", "note-DN7_eighth": "DN7z", "note-DN7_eighth.": "DN7z.", "note-DN7_half": "DN7H", "note-DN7_half.": "DN7H.", "note-DN7_quarter": "DN7q", "note-DN7_sixteenth": "DN7S", "note-DN7_sixty_fourth": "DN7s", "note-DN7_thirty_second": "DN7T", "note-DN7_whole": "DN7W", "note-DN7_whole.": "DN7W.", "note-Db1_eighth": "Db1z", "note-Db1_eighth.": "Db1z.", "note-Db1_half": "Db1H", "note-Db1_quarter": "Db1q", "note-Db1_sixteenth": "Db1S", "note-Db1_whole": "Db1W", "note-Db2_eighth": "Db2z", "note-Db2_eighth.": "Db2z.", "note-Db2_half": "Db2H", "note-Db2_half.": "Db2H.", "note-Db2_hundred_twenty_eighth": "Db2h", "note-Db2_quarter": "Db2q", "note-Db2_quarter.": "Db2q.", "note-Db2_sixteenth": "Db2S", "note-Db2_sixteenth.": "Db2S.", "note-Db2_sixty_fourth": "Db2s", "note-Db2_sixty_fourth.": "Db2s.", "note-Db2_thirty_second": "Db2T", "note-Db2_thirty_second.": "Db2T.", "note-Db2_whole": "Db2W", "note-Db3_breve": "Db3Y", "note-Db3_eighth": "Db3z", "note-Db3_eighth.": "Db3z.", "note-Db3_half": "Db3H", "note-Db3_half.": "Db3H.", "note-Db3_quarter": "Db3q", "note-Db3_quarter.": "Db3q.", "note-Db3_sixteenth": "Db3S", "note-Db3_sixteenth.": "Db3S.", "note-Db3_sixty_fourth": "Db3s", "note-Db3_thirty_second": "Db3T", "note-Db3_whole": "Db3W", "note-Db3_whole.": "Db3W.", "note-Db4_breve": "Db4Y", "note-Db4_double_whole": "Db4w", "note-Db4_eighth": "Db4z", "note-Db4_eighth.": "Db4z.", "note-Db4_eighth..": "Db4z..", "note-Db4_half": "Db4H", "note-Db4_half.": "Db4H.", "note-Db4_hundred_twenty_eighth": "Db4h", "note-Db4_quarter": "Db4q", "note-Db4_quarter.": "Db4q.", "note-Db4_sixteenth": "Db4S", "note-Db4_sixteenth.": "Db4S.", "note-Db4_sixty_fourth": "Db4s", "note-Db4_sixty_fourth.": "Db4s.", "note-Db4_thirty_second": "Db4T", "note-Db4_thirty_second.": "Db4T.", "note-Db4_whole": "Db4W", "note-Db4_whole.": "Db4W.", "note-Db4_whole._fermata": "Db4W.^", "note-Db5_breve": "Db5Y", "note-Db5_double_whole": "Db5w", "note-Db5_eighth": "Db5z", "note-Db5_eighth.": "Db5z.", "note-Db5_eighth..": "Db5z..", "note-Db5_half": "Db5H", "note-Db5_half.": "Db5H.", "note-Db5_half_fermata": "Db5H^", "note-Db5_quarter": "Db5q", "note-Db5_quarter.": "Db5q.", "note-Db5_quarter..": "Db5q..", "note-Db5_sixteenth": "Db5S", "note-Db5_sixteenth.": "Db5S.", "note-Db5_sixty_fourth": "Db5s", "note-Db5_thirty_second": "Db5T", "note-Db5_thirty_second.": "Db5T.", "note-Db5_whole": "Db5W", "note-Db5_whole.": "Db5W.", "note-Db5_whole_fermata": "Db5W^", "note-Db6_eighth": "Db6z", "note-Db6_eighth.": "Db6z.", "note-Db6_half": "Db6H", "note-Db6_half.": "Db6H.", "note-Db6_hundred_twenty_eighth": "Db6h", "note-Db6_quarter": "Db6q", "note-Db6_quarter.": "Db6q.", "note-Db6_sixteenth": "Db6S", "note-Db6_sixteenth.": "Db6S.", "note-Db6_sixty_fourth": "Db6s", "note-Db6_thirty_second": "Db6T", "note-Db6_whole": "Db6W", "note-Db6_whole.": "Db6W.", "note-Db7_eighth": "Db7z", "note-Db7_eighth.": "Db7z.", "note-Db7_half": "Db7H", "note-Db7_hundred_twenty_eighth": "Db7h", "note-Db7_quarter": "Db7q", "note-Db7_quarter.": "Db7q.", "note-Db7_sixteenth": "Db7S", "note-Db7_sixteenth.": "Db7S.", "note-Db7_sixty_fourth.": "Db7s.", "note-Db7_thirty_second": "Db7T", "note-Db7_thirty_second.": "Db7T.", "note-Db7_whole": "Db7W", "note-Dbb2_half.": "Dbb2H.", "note-Dbb3_eighth": "Dbb3z", "note-Dbb3_half.": "Dbb3H.", "note-Dbb3_sixteenth": "Dbb3S", "note-Dbb4_eighth": "Dbb4z", "note-Dbb4_half": "Dbb4H", "note-Dbb4_quarter": "Dbb4q", "note-Dbb4_sixteenth": "Dbb4S", "note-Dbb4_thirty_second": "Dbb4T", "note-Dbb5_eighth": "Dbb5z", "note-Dbb5_half": "Dbb5H", "note-Dbb5_sixteenth": "Dbb5S", "note-Dbb5_thirty_second": "Dbb5T", "note-Dbb6_half.": "Dbb6H.", "note-Dbb6_thirty_second": "Dbb6T", "note-E##3_eighth": "E##3z", "note-E##3_quarter": "E##3q", "note-E#1_eighth": "E#1z", "note-E#1_eighth.": "E#1z.", "note-E#1_half": "E#1H", "note-E#1_half.": "E#1H.", "note-E#1_quarter": "E#1q", "note-E#1_quarter.": "E#1q.", "note-E#1_sixteenth": "E#1S", "note-E#1_thirty_second": "E#1T", "note-E#2_eighth": "E#2z", "note-E#2_eighth.": "E#2z.", "note-E#2_half": "E#2H", "note-E#2_half.": "E#2H.", "note-E#2_quarter": "E#2q", "note-E#2_quarter.": "E#2q.", "note-E#2_sixteenth": "E#2S", "note-E#2_sixteenth.": "E#2S.", "note-E#2_sixty_fourth": "E#2s", "note-E#2_thirty_second": "E#2T", "note-E#2_whole": "E#2W", "note-E#2_whole.": "E#2W.", "note-E#3_eighth": "E#3z", "note-E#3_eighth.": "E#3z.", "note-E#3_half": "E#3H", "note-E#3_half.": "E#3H.", "note-E#3_quarter": "E#3q", "note-E#3_quarter.": "E#3q.", "note-E#3_sixteenth": "E#3S", "note-E#3_sixty_fourth": "E#3s", "note-E#3_thirty_second": "E#3T", "note-E#3_whole": "E#3W", "note-E#3_whole.": "E#3W.", "note-E#4_eighth": "E#4z", "note-E#4_eighth.": "E#4z.", "note-E#4_eighth..": "E#4z..", "note-E#4_half": "E#4H", "note-E#4_half.": "E#4H.", "note-E#4_quarter": "E#4q", "note-E#4_quarter.": "E#4q.", "note-E#4_sixteenth": "E#4S", "note-E#4_sixteenth.": "E#4S.", "note-E#4_sixty_fourth": "E#4s", "note-E#4_thirty_second": "E#4T", "note-E#4_thirty_second.": "E#4T.", "note-E#4_whole": "E#4W", "note-E#4_whole.": "E#4W.", "note-E#5_breve": "E#5Y", "note-E#5_eighth": "E#5z", "note-E#5_eighth.": "E#5z.", "note-E#5_half": "E#5H", "note-E#5_half.": "E#5H.", "note-E#5_half_fermata": "E#5H^", "note-E#5_quarter": "E#5q", "note-E#5_quarter.": "E#5q.", "note-E#5_sixteenth": "E#5S", "note-E#5_sixteenth.": "E#5S.", "note-E#5_sixty_fourth": "E#5s", "note-E#5_thirty_second": "E#5T", "note-E#5_thirty_second.": "E#5T.", "note-E#5_whole": "E#5W", "note-E#5_whole.": "E#5W.", "note-E#6_eighth": "E#6z", "note-E#6_eighth.": "E#6z.", "note-E#6_half": "E#6H", "note-E#6_half.": "E#6H.", "note-E#6_quarter": "E#6q", "note-E#6_quarter.": "E#6q.", "note-E#6_sixteenth": "E#6S", "note-E#6_sixteenth.": "E#6S.", "note-E#6_sixty_fourth": "E#6s", "note-E#6_thirty_second": "E#6T", "note-E#6_whole": "E#6W", "note-E#6_whole.": "E#6W.", "note-E#7_eighth": "E#7z", "note-E#7_half.": "E#7H.", "note-E#7_quarter": "E#7q", "note-E#7_quarter.": "E#7q.", "note-E#7_sixteenth": "E#7S", "note-E#7_thirty_second": "E#7T", "note-E0_eighth.": "E0z.", "note-E0_quarter": "E0q", "note-E0_whole": "E0W", "note-E1_eighth": "E1z", "note-E1_eighth.": "E1z.", "note-E1_half": "E1H", "note-E1_half.": "E1H.", "note-E1_quarter": "E1q", "note-E1_quarter.": "E1q.", "note-E1_sixteenth": "E1S", "note-E1_sixteenth.": "E1S.", "note-E1_sixty_fourth": "E1s", "note-E1_thirty_second": "E1T", "note-E1_whole": "E1W", "note-E1_whole.": "E1W.", "note-E2_breve": "E2Y", "note-E2_breve.": "E2Y.", "note-E2_eighth": "E2z", "note-E2_eighth.": "E2z.", "note-E2_half": "E2H", "note-E2_half.": "E2H.", "note-E2_hundred_twenty_eighth": "E2h", "note-E2_quarter": "E2q", "note-E2_quarter.": "E2q.", "note-E2_quarter_fermata": "E2q^", "note-E2_sixteenth": "E2S", "note-E2_sixteenth.": "E2S.", "note-E2_sixty_fourth": "E2s", "note-E2_sixty_fourth.": "E2s.", "note-E2_thirty_second": "E2T", "note-E2_thirty_second.": "E2T.", "note-E2_whole": "E2W", "note-E2_whole.": "E2W.", "note-E2_whole_fermata": "E2W^", "note-E3_breve": "E3Y", "note-E3_breve.": "E3Y.", "note-E3_double_whole": "E3w", "note-E3_double_whole.": "E3w.", "note-E3_double_whole_fermata": "E3w^", "note-E3_eighth": "E3z", "note-E3_eighth.": "E3z.", "note-E3_half": "E3H", "note-E3_half.": "E3H.", "note-E3_half._fermata": "E3H.^", "note-E3_half_fermata": "E3H^", "note-E3_hundred_twenty_eighth": "E3h", "note-E3_quadruple_whole": "E3Q", "note-E3_quarter": "E3q", "note-E3_quarter.": "E3q.", "note-E3_quarter._fermata": "E3q.^", "note-E3_quarter_fermata": "E3q^", "note-E3_sixteenth": "E3S", "note-E3_sixteenth.": "E3S.", "note-E3_sixty_fourth": "E3s", "note-E3_sixty_fourth.": "E3s.", "note-E3_thirty_second": "E3T", "note-E3_thirty_second.": "E3T.", "note-E3_whole": "E3W", "note-E3_whole.": "E3W.", "note-E3_whole_fermata": "E3W^", "note-E4_breve": "E4Y", "note-E4_double_whole": "E4w", "note-E4_double_whole.": "E4w.", "note-E4_double_whole._fermata": "E4w.^", "note-E4_double_whole_fermata": "E4w^", "note-E4_eighth": "E4z", "note-E4_eighth.": "E4z.", "note-E4_eighth..": "E4z..", "note-E4_eighth_fermata": "E4z^", "note-E4_half": "E4H", "note-E4_half.": "E4H.", "note-E4_half._fermata": "E4H.^", "note-E4_half_fermata": "E4H^", "note-E4_hundred_twenty_eighth": "E4h", "note-E4_quadruple_whole": "E4Q", "note-E4_quadruple_whole.": "E4Q.", "note-E4_quadruple_whole_fermata": "E4Q^", "note-E4_quarter": "E4q", "note-E4_quarter.": "E4q.", "note-E4_quarter..": "E4q..", "note-E4_quarter._fermata": "E4q.^", "note-E4_quarter_fermata": "E4q^", "note-E4_sixteenth": "E4S", "note-E4_sixteenth.": "E4S.", "note-E4_sixty_fourth": "E4s", "note-E4_sixty_fourth.": "E4s.", "note-E4_thirty_second": "E4T", "note-E4_thirty_second.": "E4T.", "note-E4_whole": "E4W", "note-E4_whole.": "E4W.", "note-E4_whole_fermata": "E4W^", "note-E5_breve": "E5Y", "note-E5_breve.": "E5Y.", "note-E5_double_whole": "E5w", "note-E5_double_whole.": "E5w.", "note-E5_double_whole_fermata": "E5w^", "note-E5_eighth": "E5z", "note-E5_eighth.": "E5z.", "note-E5_eighth..": "E5z..", "note-E5_eighth_fermata": "E5z^", "note-E5_half": "E5H", "note-E5_half.": "E5H.", "note-E5_half..": "E5H..", "note-E5_half._fermata": "E5H.^", "note-E5_half_fermata": "E5H^", "note-E5_hundred_twenty_eighth": "E5h", "note-E5_quadruple_whole_fermata": "E5Q^", "note-E5_quarter": "E5q", "note-E5_quarter.": "E5q.", "note-E5_quarter..": "E5q..", "note-E5_quarter._fermata": "E5q.^", "note-E5_quarter_fermata": "E5q^", "note-E5_sixteenth": "E5S", "note-E5_sixteenth.": "E5S.", "note-E5_sixteenth_fermata": "E5S^", "note-E5_sixty_fourth": "E5s", "note-E5_sixty_fourth.": "E5s.", "note-E5_thirty_second": "E5T", "note-E5_thirty_second.": "E5T.", "note-E5_whole": "E5W", "note-E5_whole.": "E5W.", "note-E5_whole._fermata": "E5W.^", "note-E5_whole_fermata": "E5W^", "note-E6_breve": "E6Y", "note-E6_breve.": "E6Y.", "note-E6_eighth": "E6z", "note-E6_eighth.": "E6z.", "note-E6_eighth..": "E6z..", "note-E6_half": "E6H", "note-E6_half.": "E6H.", "note-E6_hundred_twenty_eighth": "E6h", "note-E6_quarter": "E6q", "note-E6_quarter.": "E6q.", "note-E6_sixteenth": "E6S", "note-E6_sixteenth.": "E6S.", "note-E6_sixty_fourth": "E6s", "note-E6_sixty_fourth.": "E6s.", "note-E6_thirty_second": "E6T", "note-E6_thirty_second.": "E6T.", "note-E6_whole": "E6W", "note-E6_whole.": "E6W.", "note-E7_eighth": "E7z", "note-E7_eighth.": "E7z.", "note-E7_half": "E7H", "note-E7_half.": "E7H.", "note-E7_hundred_twenty_eighth": "E7h", "note-E7_quarter": "E7q", "note-E7_quarter.": "E7q.", "note-E7_sixteenth": "E7S", "note-E7_sixteenth.": "E7S.", "note-E7_sixty_fourth": "E7s", "note-E7_thirty_second": "E7T", "note-E7_thirty_second.": "E7T.", "note-E7_whole": "E7W", "note-E7_whole.": "E7W.", "note-E8_eighth": "E8z", "note-E8_hundred_twenty_eighth": "E8h", "note-E8_quarter": "E8q", "note-E8_sixteenth": "E8S", "note-E8_sixteenth.": "E8S.", "note-E8_sixty_fourth": "E8s", "note-E8_thirty_second": "E8T", "note-E8_thirty_second.": "E8T.", "note-E8_whole": "E8W", "note-E9_eighth": "E9z", "note-E9_sixteenth": "E9S", "note-EN1_eighth": "EN1z", "note-EN1_half": "EN1H", "note-EN1_half.": "EN1H.", "note-EN1_quarter": "EN1q", "note-EN1_quarter.": "EN1q.", "note-EN1_sixteenth": "EN1S", "note-EN1_sixty_fourth": "EN1s", "note-EN1_thirty_second": "EN1T", "note-EN1_whole": "EN1W", "note-EN2_eighth": "EN2z", "note-EN2_eighth.": "EN2z.", "note-EN2_half": "EN2H", "note-EN2_half.": "EN2H.", "note-EN2_hundred_twenty_eighth": "EN2h", "note-EN2_quarter": "EN2q", "note-EN2_quarter.": "EN2q.", "note-EN2_sixteenth": "EN2S", "note-EN2_sixteenth.": "EN2S.", "note-EN2_sixty_fourth": "EN2s", "note-EN2_sixty_fourth.": "EN2s.", "note-EN2_thirty_second": "EN2T", "note-EN2_whole": "EN2W", "note-EN2_whole.": "EN2W.", "note-EN3_eighth": "EN3z", "note-EN3_eighth.": "EN3z.", "note-EN3_half": "EN3H", "note-EN3_half.": "EN3H.", "note-EN3_hundred_twenty_eighth": "EN3h", "note-EN3_quarter": "EN3q", "note-EN3_quarter.": "EN3q.", "note-EN3_sixteenth": "EN3S", "note-EN3_sixteenth.": "EN3S.", "note-EN3_sixty_fourth": "EN3s", "note-EN3_sixty_fourth.": "EN3s.", "note-EN3_thirty_second": "EN3T", "note-EN3_thirty_second.": "EN3T.", "note-EN3_whole": "EN3W", "note-EN3_whole.": "EN3W.", "note-EN4_eighth": "EN4z", "note-EN4_eighth.": "EN4z.", "note-EN4_half": "EN4H", "note-EN4_half.": "EN4H.", "note-EN4_hundred_twenty_eighth": "EN4h", "note-EN4_quarter": "EN4q", "note-EN4_quarter.": "EN4q.", "note-EN4_sixteenth": "EN4S", "note-EN4_sixteenth.": "EN4S.", "note-EN4_sixty_fourth": "EN4s", "note-EN4_sixty_fourth.": "EN4s.", "note-EN4_thirty_second": "EN4T", "note-EN4_thirty_second.": "EN4T.", "note-EN4_whole": "EN4W", "note-EN4_whole.": "EN4W.", "note-EN5_eighth": "EN5z", "note-EN5_eighth.": "EN5z.", "note-EN5_half": "EN5H", "note-EN5_half.": "EN5H.", "note-EN5_hundred_twenty_eighth": "EN5h", "note-EN5_quarter": "EN5q", "note-EN5_quarter.": "EN5q.", "note-EN5_sixteenth": "EN5S", "note-EN5_sixteenth.": "EN5S.", "note-EN5_sixty_fourth": "EN5s", "note-EN5_sixty_fourth.": "EN5s.", "note-EN5_thirty_second": "EN5T", "note-EN5_whole": "EN5W", "note-EN5_whole.": "EN5W.", "note-EN6_eighth": "EN6z", "note-EN6_eighth.": "EN6z.", "note-EN6_half": "EN6H", "note-EN6_half.": "EN6H.", "note-EN6_hundred_twenty_eighth": "EN6h", "note-EN6_quarter": "EN6q", "note-EN6_quarter.": "EN6q.", "note-EN6_sixteenth": "EN6S", "note-EN6_sixteenth.": "EN6S.", "note-EN6_sixty_fourth": "EN6s", "note-EN6_sixty_fourth.": "EN6s.", "note-EN6_thirty_second": "EN6T", "note-EN6_thirty_second.": "EN6T.", "note-EN6_whole": "EN6W", "note-EN6_whole.": "EN6W.", "note-EN7_eighth": "EN7z", "note-EN7_eighth.": "EN7z.", "note-EN7_half": "EN7H", "note-EN7_half.": "EN7H.", "note-EN7_quarter": "EN7q", "note-EN7_quarter.": "EN7q.", "note-EN7_sixteenth": "EN7S", "note-EN7_thirty_second": "EN7T", "note-EN7_thirty_second.": "EN7T.", "note-EN7_whole": "EN7W", "note-EN8_quarter": "EN8q", "note-Eb0_half": "Eb0H", "note-Eb1_eighth": "Eb1z", "note-Eb1_eighth.": "Eb1z.", "note-Eb1_half": "Eb1H", "note-Eb1_half.": "Eb1H.", "note-Eb1_quarter": "Eb1q", "note-Eb1_quarter.": "Eb1q.", "note-Eb1_sixteenth": "Eb1S", "note-Eb1_thirty_second": "Eb1T", "note-Eb1_whole": "Eb1W", "note-Eb2_eighth": "Eb2z", "note-Eb2_eighth.": "Eb2z.", "note-Eb2_half": "Eb2H", "note-Eb2_half.": "Eb2H.", "note-Eb2_quarter": "Eb2q", "note-Eb2_quarter.": "Eb2q.", "note-Eb2_quarter._fermata": "Eb2q.^", "note-Eb2_sixteenth": "Eb2S", "note-Eb2_sixteenth.": "Eb2S.", "note-Eb2_sixty_fourth": "Eb2s", "note-Eb2_thirty_second": "Eb2T", "note-Eb2_whole": "Eb2W", "note-Eb2_whole.": "Eb2W.", "note-Eb3_eighth": "Eb3z", "note-Eb3_eighth.": "Eb3z.", "note-Eb3_half": "Eb3H", "note-Eb3_half.": "Eb3H.", "note-Eb3_half._fermata": "Eb3H.^", "note-Eb3_half_fermata": "Eb3H^", "note-Eb3_quarter": "Eb3q", "note-Eb3_quarter.": "Eb3q.", "note-Eb3_quarter_fermata": "Eb3q^", "note-Eb3_sixteenth": "Eb3S", "note-Eb3_sixteenth.": "Eb3S.", "note-Eb3_sixty_fourth": "Eb3s", "note-Eb3_sixty_fourth.": "Eb3s.", "note-Eb3_thirty_second": "Eb3T", "note-Eb3_thirty_second.": "Eb3T.", "note-Eb3_whole": "Eb3W", "note-Eb3_whole.": "Eb3W.", "note-Eb4_double_whole": "Eb4w", "note-Eb4_eighth": "Eb4z", "note-Eb4_eighth.": "Eb4z.", "note-Eb4_eighth..": "Eb4z..", "note-Eb4_eighth._fermata": "Eb4z.^", "note-Eb4_eighth_fermata": "Eb4z^", "note-Eb4_half": "Eb4H", "note-Eb4_half.": "Eb4H.", "note-Eb4_half._fermata": "Eb4H.^", "note-Eb4_half_fermata": "Eb4H^", "note-Eb4_hundred_twenty_eighth": "Eb4h", "note-Eb4_quarter": "Eb4q", "note-Eb4_quarter.": "Eb4q.", "note-Eb4_quarter..": "Eb4q..", "note-Eb4_quarter._fermata": "Eb4q.^", "note-Eb4_quarter_fermata": "Eb4q^", "note-Eb4_sixteenth": "Eb4S", "note-Eb4_sixteenth.": "Eb4S.", "note-Eb4_sixty_fourth": "Eb4s", "note-Eb4_sixty_fourth.": "Eb4s.", "note-Eb4_thirty_second": "Eb4T", "note-Eb4_thirty_second.": "Eb4T.", "note-Eb4_whole": "Eb4W", "note-Eb4_whole.": "Eb4W.", "note-Eb4_whole_fermata": "Eb4W^", "note-Eb5_double_whole": "Eb5w", "note-Eb5_eighth": "Eb5z", "note-Eb5_eighth.": "Eb5z.", "note-Eb5_eighth..": "Eb5z..", "note-Eb5_eighth_fermata": "Eb5z^", "note-Eb5_half": "Eb5H", "note-Eb5_half.": "Eb5H.", "note-Eb5_half..": "Eb5H..", "note-Eb5_half._fermata": "Eb5H.^", "note-Eb5_half_fermata": "Eb5H^", "note-Eb5_hundred_twenty_eighth": "Eb5h", "note-Eb5_quarter": "Eb5q", "note-Eb5_quarter.": "Eb5q.", "note-Eb5_quarter..": "Eb5q..", "note-Eb5_quarter._fermata": "Eb5q.^", "note-Eb5_quarter_fermata": "Eb5q^", "note-Eb5_sixteenth": "Eb5S", "note-Eb5_sixteenth.": "Eb5S.", "note-Eb5_sixteenth_fermata": "Eb5S^", "note-Eb5_sixty_fourth": "Eb5s", "note-Eb5_sixty_fourth.": "Eb5s.", "note-Eb5_thirty_second": "Eb5T", "note-Eb5_thirty_second.": "Eb5T.", "note-Eb5_whole": "Eb5W", "note-Eb5_whole.": "Eb5W.", "note-Eb5_whole._fermata": "Eb5W.^", "note-Eb5_whole_fermata": "Eb5W^", "note-Eb6_eighth": "Eb6z", "note-Eb6_eighth.": "Eb6z.", "note-Eb6_eighth..": "Eb6z..", "note-Eb6_half": "Eb6H", "note-Eb6_half.": "Eb6H.", "note-Eb6_quarter": "Eb6q", "note-Eb6_quarter.": "Eb6q.", "note-Eb6_quarter..": "Eb6q..", "note-Eb6_sixteenth": "Eb6S", "note-Eb6_sixteenth.": "Eb6S.", "note-Eb6_thirty_second": "Eb6T", "note-Eb6_whole": "Eb6W", "note-Eb6_whole.": "Eb6W.", "note-Eb7_eighth": "Eb7z", "note-Eb7_eighth.": "Eb7z.", "note-Eb7_half": "Eb7H", "note-Eb7_hundred_twenty_eighth": "Eb7h", "note-Eb7_quarter": "Eb7q", "note-Eb7_sixteenth": "Eb7S", "note-Eb7_thirty_second": "Eb7T", "note-Eb7_whole": "Eb7W", "note-Eb8_quarter": "Eb8q", "note-Ebb1_half": "Ebb1H", "note-Ebb2_eighth": "Ebb2z", "note-Ebb2_half": "Ebb2H", "note-Ebb2_quarter": "Ebb2q", "note-Ebb2_quarter.": "Ebb2q.", "note-Ebb2_sixteenth": "Ebb2S", "note-Ebb2_whole": "Ebb2W", "note-Ebb3_eighth": "Ebb3z", "note-Ebb3_half": "Ebb3H", "note-Ebb3_half.": "Ebb3H.", "note-Ebb3_quarter": "Ebb3q", "note-Ebb3_quarter.": "Ebb3q.", "note-Ebb3_sixteenth": "Ebb3S", "note-Ebb3_whole": "Ebb3W", "note-Ebb4_eighth": "Ebb4z", "note-Ebb4_half": "Ebb4H", "note-Ebb4_quarter": "Ebb4q", "note-Ebb4_sixteenth": "Ebb4S", "note-Ebb4_thirty_second": "Ebb4T", "note-Ebb4_whole": "Ebb4W", "note-Ebb4_whole.": "Ebb4W.", "note-Ebb5_eighth": "Ebb5z", "note-Ebb5_half": "Ebb5H", "note-Ebb5_quarter": "Ebb5q", "note-Ebb5_sixteenth": "Ebb5S", "note-Ebb5_thirty_second": "Ebb5T", "note-Ebb5_whole.": "Ebb5W.", "note-Ebb6_eighth": "Ebb6z", "note-Ebb6_half.": "Ebb6H.", "note-Ebb6_quarter": "Ebb6q", "note-Ebb6_sixteenth": "Ebb6S", "note-Ebb6_sixty_fourth": "Ebb6s", "note-Ebb6_thirty_second": "Ebb6T", "note-Ebb6_whole.": "Ebb6W.", "note-Ebb7_eighth": "Ebb7z", "note-F##1_eighth": "F##1z", "note-F##1_half": "F##1H", "note-F##1_half.": "F##1H.", "note-F##1_quarter": "F##1q", "note-F##1_quarter.": "F##1q.", "note-F##1_sixteenth": "F##1S", "note-F##1_whole": "F##1W", "note-F##2_eighth": "F##2z", "note-F##2_half": "F##2H", "note-F##2_half.": "F##2H.", "note-F##2_quarter": "F##2q", "note-F##2_quarter.": "F##2q.", "note-F##2_sixteenth": "F##2S", "note-F##2_sixty_fourth": "F##2s", "note-F##2_thirty_second": "F##2T", "note-F##2_whole": "F##2W", "note-F##3_eighth": "F##3z", "note-F##3_half": "F##3H", "note-F##3_half.": "F##3H.", "note-F##3_hundred_twenty_eighth": "F##3h", "note-F##3_quarter": "F##3q", "note-F##3_quarter.": "F##3q.", "note-F##3_sixteenth": "F##3S", "note-F##3_sixty_fourth": "F##3s", "note-F##3_thirty_second": "F##3T", "note-F##3_whole": "F##3W", "note-F##4_eighth": "F##4z", "note-F##4_eighth.": "F##4z.", "note-F##4_half": "F##4H", "note-F##4_half.": "F##4H.", "note-F##4_quarter": "F##4q", "note-F##4_quarter.": "F##4q.", "note-F##4_sixteenth": "F##4S", "note-F##4_sixty_fourth": "F##4s", "note-F##4_thirty_second": "F##4T", "note-F##4_whole": "F##4W", "note-F##4_whole.": "F##4W.", "note-F##5_eighth": "F##5z", "note-F##5_eighth.": "F##5z.", "note-F##5_half": "F##5H", "note-F##5_half.": "F##5H.", "note-F##5_quarter": "F##5q", "note-F##5_quarter.": "F##5q.", "note-F##5_sixteenth": "F##5S", "note-F##5_sixty_fourth": "F##5s", "note-F##5_thirty_second": "F##5T", "note-F##5_whole": "F##5W", "note-F##5_whole.": "F##5W.", "note-F##6_eighth": "F##6z", "note-F##6_half": "F##6H", "note-F##6_half.": "F##6H.", "note-F##6_quarter": "F##6q", "note-F##6_quarter.": "F##6q.", "note-F##6_sixteenth": "F##6S", "note-F##6_sixty_fourth": "F##6s", "note-F##6_thirty_second": "F##6T", "note-F##6_whole": "F##6W", "note-F##7_eighth": "F##7z", "note-F##7_sixteenth": "F##7S", "note-F##7_thirty_second": "F##7T", "note-F#0_half": "F#0H", "note-F#1_eighth": "F#1z", "note-F#1_eighth.": "F#1z.", "note-F#1_half": "F#1H", "note-F#1_half.": "F#1H.", "note-F#1_quarter": "F#1q", "note-F#1_quarter.": "F#1q.", "note-F#1_sixteenth": "F#1S", "note-F#1_sixty_fourth": "F#1s", "note-F#1_thirty_second": "F#1T", "note-F#1_whole": "F#1W", "note-F#1_whole.": "F#1W.", "note-F#2_breve": "F#2Y", "note-F#2_eighth": "F#2z", "note-F#2_eighth.": "F#2z.", "note-F#2_half": "F#2H", "note-F#2_half.": "F#2H.", "note-F#2_quarter": "F#2q", "note-F#2_quarter.": "F#2q.", "note-F#2_sixteenth": "F#2S", "note-F#2_sixteenth.": "F#2S.", "note-F#2_sixty_fourth": "F#2s", "note-F#2_thirty_second": "F#2T", "note-F#2_whole": "F#2W", "note-F#2_whole.": "F#2W.", "note-F#3_breve": "F#3Y", "note-F#3_double_whole": "F#3w", "note-F#3_eighth": "F#3z", "note-F#3_eighth.": "F#3z.", "note-F#3_half": "F#3H", "note-F#3_half.": "F#3H.", "note-F#3_half_fermata": "F#3H^", "note-F#3_quarter": "F#3q", "note-F#3_quarter.": "F#3q.", "note-F#3_quarter_fermata": "F#3q^", "note-F#3_sixteenth": "F#3S", "note-F#3_sixteenth.": "F#3S.", "note-F#3_sixty_fourth": "F#3s", "note-F#3_sixty_fourth.": "F#3s.", "note-F#3_thirty_second": "F#3T", "note-F#3_whole": "F#3W", "note-F#3_whole.": "F#3W.", "note-F#4_double_whole": "F#4w", "note-F#4_double_whole_fermata": "F#4w^", "note-F#4_eighth": "F#4z", "note-F#4_eighth.": "F#4z.", "note-F#4_eighth..": "F#4z..", "note-F#4_eighth_fermata": "F#4z^", "note-F#4_half": "F#4H", "note-F#4_half.": "F#4H.", "note-F#4_half_fermata": "F#4H^", "note-F#4_quadruple_whole_fermata": "F#4Q^", "note-F#4_quarter": "F#4q", "note-F#4_quarter.": "F#4q.", "note-F#4_quarter..": "F#4q..", "note-F#4_quarter._fermata": "F#4q.^", "note-F#4_quarter_fermata": "F#4q^", "note-F#4_sixteenth": "F#4S", "note-F#4_sixteenth.": "F#4S.", "note-F#4_sixty_fourth": "F#4s", "note-F#4_sixty_fourth.": "F#4s.", "note-F#4_thirty_second": "F#4T", "note-F#4_thirty_second.": "F#4T.", "note-F#4_whole": "F#4W", "note-F#4_whole.": "F#4W.", "note-F#4_whole._fermata": "F#4W.^", "note-F#4_whole_fermata": "F#4W^", "note-F#5_double_whole": "F#5w", "note-F#5_eighth": "F#5z", "note-F#5_eighth.": "F#5z.", "note-F#5_eighth..": "F#5z..", "note-F#5_eighth._fermata": "F#5z.^", "note-F#5_eighth_fermata": "F#5z^", "note-F#5_half": "F#5H", "note-F#5_half.": "F#5H.", "note-F#5_half._fermata": "F#5H.^", "note-F#5_half_fermata": "F#5H^", "note-F#5_quarter": "F#5q", "note-F#5_quarter.": "F#5q.", "note-F#5_quarter..": "F#5q..", "note-F#5_quarter._fermata": "F#5q.^", "note-F#5_quarter_fermata": "F#5q^", "note-F#5_sixteenth": "F#5S", "note-F#5_sixteenth.": "F#5S.", "note-F#5_sixty_fourth": "F#5s", "note-F#5_thirty_second": "F#5T", "note-F#5_thirty_second.": "F#5T.", "note-F#5_whole": "F#5W", "note-F#5_whole.": "F#5W.", "note-F#5_whole_fermata": "F#5W^", "note-F#6_eighth": "F#6z", "note-F#6_eighth.": "F#6z.", "note-F#6_half": "F#6H", "note-F#6_half.": "F#6H.", "note-F#6_quarter": "F#6q", "note-F#6_quarter.": "F#6q.", "note-F#6_sixteenth": "F#6S", "note-F#6_sixteenth.": "F#6S.", "note-F#6_sixty_fourth": "F#6s", "note-F#6_thirty_second": "F#6T", "note-F#6_thirty_second.": "F#6T.", "note-F#6_whole": "F#6W", "note-F#7_eighth": "F#7z", "note-F#7_half": "F#7H", "note-F#7_quarter": "F#7q", "note-F#7_quarter.": "F#7q.", "note-F#7_sixteenth": "F#7S", "note-F#7_sixty_fourth": "F#7s", "note-F#7_thirty_second": "F#7T", "note-F#7_whole": "F#7W", "note-F#8_quarter": "F#8q", "note-F0_eighth": "F0z", "note-F0_eighth.": "F0z.", "note-F0_half": "F0H", "note-F0_quarter": "F0q", "note-F0_quarter.": "F0q.", "note-F0_whole": "F0W", "note-F1_eighth": "F1z", "note-F1_eighth.": "F1z.", "note-F1_half": "F1H", "note-F1_half.": "F1H.", "note-F1_hundred_twenty_eighth": "F1h", "note-F1_quarter": "F1q", "note-F1_quarter.": "F1q.", "note-F1_sixteenth": "F1S", "note-F1_sixteenth.": "F1S.", "note-F1_sixty_fourth": "F1s", "note-F1_thirty_second": "F1T", "note-F1_whole": "F1W", "note-F1_whole.": "F1W.", "note-F2_breve": "F2Y", "note-F2_double_whole": "F2w", "note-F2_double_whole.": "F2w.", "note-F2_double_whole_fermata": "F2w^", "note-F2_eighth": "F2z", "note-F2_eighth.": "F2z.", "note-F2_half": "F2H", "note-F2_half.": "F2H.", "note-F2_half_fermata": "F2H^", "note-F2_hundred_twenty_eighth": "F2h", "note-F2_quadruple_whole": "F2Q", "note-F2_quarter": "F2q", "note-F2_quarter.": "F2q.", "note-F2_quarter..": "F2q..", "note-F2_sixteenth": "F2S", "note-F2_sixteenth.": "F2S.", "note-F2_sixty_fourth": "F2s", "note-F2_sixty_fourth.": "F2s.", "note-F2_thirty_second": "F2T", "note-F2_thirty_second.": "F2T.", "note-F2_whole": "F2W", "note-F2_whole.": "F2W.", "note-F3_breve": "F3Y", "note-F3_breve.": "F3Y.", "note-F3_double_whole": "F3w", "note-F3_double_whole.": "F3w.", "note-F3_double_whole_fermata": "F3w^", "note-F3_eighth": "F3z", "note-F3_eighth.": "F3z.", "note-F3_half": "F3H", "note-F3_half.": "F3H.", "note-F3_half_fermata": "F3H^", "note-F3_hundred_twenty_eighth": "F3h", "note-F3_quadruple_whole": "F3Q", "note-F3_quarter": "F3q", "note-F3_quarter.": "F3q.", "note-F3_quarter..": "F3q..", "note-F3_sixteenth": "F3S", "note-F3_sixteenth.": "F3S.", "note-F3_sixty_fourth": "F3s", "note-F3_sixty_fourth.": "F3s.", "note-F3_thirty_second": "F3T", "note-F3_thirty_second.": "F3T.", "note-F3_whole": "F3W", "note-F3_whole.": "F3W.", "note-F3_whole_fermata": "F3W^", "note-F4_breve": "F4Y", "note-F4_double_whole": "F4w", "note-F4_double_whole.": "F4w.", "note-F4_double_whole_fermata": "F4w^", "note-F4_eighth": "F4z", "note-F4_eighth.": "F4z.", "note-F4_eighth..": "F4z..", "note-F4_eighth_fermata": "F4z^", "note-F4_half": "F4H", "note-F4_half.": "F4H.", "note-F4_half..": "F4H..", "note-F4_half._fermata": "F4H.^", "note-F4_half_fermata": "F4H^", "note-F4_hundred_twenty_eighth": "F4h", "note-F4_quadruple_whole": "F4Q", "note-F4_quadruple_whole.": "F4Q.", "note-F4_quadruple_whole_fermata": "F4Q^", "note-F4_quarter": "F4q", "note-F4_quarter.": "F4q.", "note-F4_quarter..": "F4q..", "note-F4_quarter._fermata": "F4q.^", "note-F4_quarter_fermata": "F4q^", "note-F4_sixteenth": "F4S", "note-F4_sixteenth.": "F4S.", "note-F4_sixty_fourth": "F4s", "note-F4_sixty_fourth.": "F4s.", "note-F4_thirty_second": "F4T", "note-F4_thirty_second.": "F4T.", "note-F4_whole": "F4W", "note-F4_whole.": "F4W.", "note-F4_whole_fermata": "F4W^", "note-F5_breve": "F5Y", "note-F5_double_whole": "F5w", "note-F5_eighth": "F5z", "note-F5_eighth.": "F5z.", "note-F5_eighth..": "F5z..", "note-F5_half": "F5H", "note-F5_half.": "F5H.", "note-F5_half._fermata": "F5H.^", "note-F5_half_fermata": "F5H^", "note-F5_hundred_twenty_eighth": "F5h", "note-F5_quarter": "F5q", "note-F5_quarter.": "F5q.", "note-F5_quarter..": "F5q..", "note-F5_quarter._fermata": "F5q.^", "note-F5_quarter_fermata": "F5q^", "note-F5_sixteenth": "F5S", "note-F5_sixteenth.": "F5S.", "note-F5_sixty_fourth": "F5s", "note-F5_sixty_fourth.": "F5s.", "note-F5_thirty_second": "F5T", "note-F5_thirty_second.": "F5T.", "note-F5_whole": "F5W", "note-F5_whole.": "F5W.", "note-F5_whole_fermata": "F5W^", "note-F6_eighth": "F6z", "note-F6_eighth.": "F6z.", "note-F6_half": "F6H", "note-F6_half.": "F6H.", "note-F6_hundred_twenty_eighth": "F6h", "note-F6_quarter": "F6q", "note-F6_quarter.": "F6q.", "note-F6_sixteenth": "F6S", "note-F6_sixteenth.": "F6S.", "note-F6_sixty_fourth": "F6s", "note-F6_sixty_fourth.": "F6s.", "note-F6_thirty_second": "F6T", "note-F6_thirty_second.": "F6T.", "note-F6_whole": "F6W", "note-F6_whole.": "F6W.", "note-F7_eighth": "F7z", "note-F7_eighth.": "F7z.", "note-F7_half": "F7H", "note-F7_half.": "F7H.", "note-F7_hundred_twenty_eighth": "F7h", "note-F7_quarter": "F7q", "note-F7_quarter.": "F7q.", "note-F7_sixteenth": "F7S", "note-F7_sixteenth.": "F7S.", "note-F7_sixty_fourth": "F7s", "note-F7_thirty_second": "F7T", "note-F7_whole": "F7W", "note-F7_whole.": "F7W.", "note-F8_eighth": "F8z", "note-F8_eighth.": "F8z.", "note-F8_hundred_twenty_eighth": "F8h", "note-F8_quarter": "F8q", "note-F8_sixteenth": "F8S", "note-F8_sixteenth.": "F8S.", "note-F8_thirty_second": "F8T", "note-FN1_eighth": "FN1z", "note-FN1_eighth.": "FN1z.", "note-FN1_half": "FN1H", "note-FN1_half.": "FN1H.", "note-FN1_quarter": "FN1q", "note-FN1_quarter.": "FN1q.", "note-FN1_sixteenth": "FN1S", "note-FN1_whole": "FN1W", "note-FN1_whole.": "FN1W.", "note-FN2_eighth": "FN2z", "note-FN2_eighth.": "FN2z.", "note-FN2_half": "FN2H", "note-FN2_half.": "FN2H.", "note-FN2_quarter": "FN2q", "note-FN2_quarter.": "FN2q.", "note-FN2_sixteenth": "FN2S", "note-FN2_sixty_fourth": "FN2s", "note-FN2_thirty_second": "FN2T", "note-FN2_whole": "FN2W", "note-FN2_whole.": "FN2W.", "note-FN3_eighth": "FN3z", "note-FN3_eighth.": "FN3z.", "note-FN3_half": "FN3H", "note-FN3_half.": "FN3H.", "note-FN3_quarter": "FN3q", "note-FN3_quarter.": "FN3q.", "note-FN3_sixteenth": "FN3S", "note-FN3_sixty_fourth": "FN3s", "note-FN3_thirty_second": "FN3T", "note-FN3_whole": "FN3W", "note-FN4_eighth": "FN4z", "note-FN4_eighth.": "FN4z.", "note-FN4_half": "FN4H", "note-FN4_half.": "FN4H.", "note-FN4_quarter": "FN4q", "note-FN4_quarter.": "FN4q.", "note-FN4_sixteenth": "FN4S", "note-FN4_sixteenth.": "FN4S.", "note-FN4_sixty_fourth": "FN4s", "note-FN4_thirty_second": "FN4T", "note-FN4_whole": "FN4W", "note-FN4_whole.": "FN4W.", "note-FN5_breve": "FN5Y", "note-FN5_eighth": "FN5z", "note-FN5_eighth.": "FN5z.", "note-FN5_half": "FN5H", "note-FN5_half.": "FN5H.", "note-FN5_quarter": "FN5q", "note-FN5_quarter.": "FN5q.", "note-FN5_sixteenth": "FN5S", "note-FN5_sixteenth.": "FN5S.", "note-FN5_sixty_fourth": "FN5s", "note-FN5_thirty_second": "FN5T", "note-FN5_whole": "FN5W", "note-FN5_whole.": "FN5W.", "note-FN6_breve": "FN6Y", "note-FN6_eighth": "FN6z", "note-FN6_eighth.": "FN6z.", "note-FN6_half": "FN6H", "note-FN6_half.": "FN6H.", "note-FN6_quarter": "FN6q", "note-FN6_quarter.": "FN6q.", "note-FN6_sixteenth": "FN6S", "note-FN6_sixteenth.": "FN6S.", "note-FN6_sixty_fourth": "FN6s", "note-FN6_thirty_second": "FN6T", "note-FN6_whole": "FN6W", "note-FN7_eighth": "FN7z", "note-FN7_half.": "FN7H.", "note-FN7_quarter": "FN7q", "note-FN7_sixteenth": "FN7S", "note-FN7_sixty_fourth": "FN7s", "note-FN7_thirty_second": "FN7T", "note-FN7_whole": "FN7W", "note-FN8_eighth": "FN8z", "note-FN8_eighth.": "FN8z.", "note-FN8_quarter": "FN8q", "note-Fb1_eighth": "Fb1z", "note-Fb1_half.": "Fb1H.", "note-Fb1_quarter": "Fb1q", "note-Fb1_quarter.": "Fb1q.", "note-Fb1_sixteenth": "Fb1S", "note-Fb1_sixty_fourth": "Fb1s", "note-Fb1_thirty_second": "Fb1T", "note-Fb2_eighth": "Fb2z", "note-Fb2_eighth.": "Fb2z.", "note-Fb2_half": "Fb2H", "note-Fb2_half.": "Fb2H.", "note-Fb2_quarter": "Fb2q", "note-Fb2_quarter.": "Fb2q.", "note-Fb2_sixteenth": "Fb2S", "note-Fb2_thirty_second": "Fb2T", "note-Fb2_whole": "Fb2W", "note-Fb2_whole.": "Fb2W.", "note-Fb3_eighth": "Fb3z", "note-Fb3_eighth.": "Fb3z.", "note-Fb3_half": "Fb3H", "note-Fb3_half.": "Fb3H.", "note-Fb3_quarter": "Fb3q", "note-Fb3_quarter.": "Fb3q.", "note-Fb3_sixteenth": "Fb3S", "note-Fb3_thirty_second": "Fb3T", "note-Fb3_whole": "Fb3W", "note-Fb3_whole.": "Fb3W.", "note-Fb4_eighth": "Fb4z", "note-Fb4_eighth.": "Fb4z.", "note-Fb4_half": "Fb4H", "note-Fb4_half.": "Fb4H.", "note-Fb4_hundred_twenty_eighth": "Fb4h", "note-Fb4_quarter": "Fb4q", "note-Fb4_quarter.": "Fb4q.", "note-Fb4_sixteenth": "Fb4S", "note-Fb4_sixteenth.": "Fb4S.", "note-Fb4_thirty_second": "Fb4T", "note-Fb4_whole": "Fb4W", "note-Fb5_eighth": "Fb5z", "note-Fb5_eighth.": "Fb5z.", "note-Fb5_half": "Fb5H", "note-Fb5_half.": "Fb5H.", "note-Fb5_hundred_twenty_eighth": "Fb5h", "note-Fb5_quarter": "Fb5q", "note-Fb5_quarter.": "Fb5q.", "note-Fb5_sixteenth": "Fb5S", "note-Fb5_sixty_fourth": "Fb5s", "note-Fb5_thirty_second": "Fb5T", "note-Fb5_whole": "Fb5W", "note-Fb6_eighth": "Fb6z", "note-Fb6_half": "Fb6H", "note-Fb6_half.": "Fb6H.", "note-Fb6_quarter": "Fb6q", "note-Fb6_quarter.": "Fb6q.", "note-Fb6_sixteenth": "Fb6S", "note-Fb6_sixty_fourth": "Fb6s", "note-Fb6_thirty_second": "Fb6T", "note-Fb6_whole": "Fb6W", "note-Fb6_whole.": "Fb6W.", "note-Fb7_eighth": "Fb7z", "note-Fb7_half.": "Fb7H.", "note-Fb7_thirty_second": "Fb7T", "note-Fbb2_sixteenth": "Fbb2S", "note-Fbb4_eighth": "Fbb4z", "note-G##1_eighth": "G##1z", "note-G##1_quarter": "G##1q", "note-G##1_sixteenth": "G##1S", "note-G##2_eighth": "G##2z", "note-G##2_quarter.": "G##2q.", "note-G##2_sixteenth": "G##2S", "note-G##3_eighth": "G##3z", "note-G##3_half": "G##3H", "note-G##3_quarter": "G##3q", "note-G##3_quarter.": "G##3q.", "note-G##3_sixteenth": "G##3S", "note-G##3_sixty_fourth": "G##3s", "note-G##3_thirty_second": "G##3T", "note-G##3_whole": "G##3W", "note-G##4_eighth": "G##4z", "note-G##4_half": "G##4H", "note-G##4_quarter": "G##4q", "note-G##4_quarter.": "G##4q.", "note-G##4_sixteenth": "G##4S", "note-G##4_thirty_second": "G##4T", "note-G##4_whole": "G##4W", "note-G##5_eighth": "G##5z", "note-G##5_eighth.": "G##5z.", "note-G##5_quarter": "G##5q", "note-G##5_quarter.": "G##5q.", "note-G##5_sixteenth": "G##5S", "note-G##5_thirty_second": "G##5T", "note-G##5_whole": "G##5W", "note-G##6_eighth": "G##6z", "note-G##6_sixteenth": "G##6S", "note-G##7_sixteenth": "G##7S", "note-G#0_eighth": "G#0z", "note-G#0_sixteenth": "G#0S", "note-G#1_eighth": "G#1z", "note-G#1_eighth.": "G#1z.", "note-G#1_half": "G#1H", "note-G#1_half.": "G#1H.", "note-G#1_quarter": "G#1q", "note-G#1_quarter.": "G#1q.", "note-G#1_sixteenth": "G#1S", "note-G#1_thirty_second": "G#1T", "note-G#1_whole": "G#1W", "note-G#1_whole.": "G#1W.", "note-G#2_eighth": "G#2z", "note-G#2_eighth.": "G#2z.", "note-G#2_half": "G#2H", "note-G#2_half.": "G#2H.", "note-G#2_quarter": "G#2q", "note-G#2_quarter.": "G#2q.", "note-G#2_sixteenth": "G#2S", "note-G#2_sixteenth.": "G#2S.", "note-G#2_sixty_fourth": "G#2s", "note-G#2_thirty_second": "G#2T", "note-G#2_whole": "G#2W", "note-G#2_whole.": "G#2W.", "note-G#3_breve": "G#3Y", "note-G#3_eighth": "G#3z", "note-G#3_eighth.": "G#3z.", "note-G#3_eighth..": "G#3z..", "note-G#3_half": "G#3H", "note-G#3_half.": "G#3H.", "note-G#3_half_fermata": "G#3H^", "note-G#3_hundred_twenty_eighth": "G#3h", "note-G#3_quarter": "G#3q", "note-G#3_quarter.": "G#3q.", "note-G#3_sixteenth": "G#3S", "note-G#3_sixteenth.": "G#3S.", "note-G#3_sixty_fourth": "G#3s", "note-G#3_sixty_fourth.": "G#3s.", "note-G#3_thirty_second": "G#3T", "note-G#3_thirty_second.": "G#3T.", "note-G#3_whole": "G#3W", "note-G#4_double_whole": "G#4w", "note-G#4_double_whole_fermata": "G#4w^", "note-G#4_eighth": "G#4z", "note-G#4_eighth.": "G#4z.", "note-G#4_eighth..": "G#4z..", "note-G#4_eighth._fermata": "G#4z.^", "note-G#4_half": "G#4H", "note-G#4_half.": "G#4H.", "note-G#4_half._fermata": "G#4H.^", "note-G#4_half_fermata": "G#4H^", "note-G#4_quarter": "G#4q", "note-G#4_quarter.": "G#4q.", "note-G#4_quarter_fermata": "G#4q^", "note-G#4_sixteenth": "G#4S", "note-G#4_sixteenth.": "G#4S.", "note-G#4_sixty_fourth": "G#4s", "note-G#4_thirty_second": "G#4T", "note-G#4_thirty_second.": "G#4T.", "note-G#4_whole": "G#4W", "note-G#4_whole.": "G#4W.", "note-G#4_whole_fermata": "G#4W^", "note-G#5_eighth": "G#5z", "note-G#5_eighth.": "G#5z.", "note-G#5_eighth_fermata": "G#5z^", "note-G#5_half": "G#5H", "note-G#5_half.": "G#5H.", "note-G#5_half_fermata": "G#5H^", "note-G#5_hundred_twenty_eighth": "G#5h", "note-G#5_quarter": "G#5q", "note-G#5_quarter.": "G#5q.", "note-G#5_quarter..": "G#5q..", "note-G#5_quarter_fermata": "G#5q^", "note-G#5_sixteenth": "G#5S", "note-G#5_sixteenth.": "G#5S.", "note-G#5_sixty_fourth": "G#5s", "note-G#5_sixty_fourth.": "G#5s.", "note-G#5_thirty_second": "G#5T", "note-G#5_thirty_second.": "G#5T.", "note-G#5_whole": "G#5W", "note-G#5_whole.": "G#5W.", "note-G#6_eighth": "G#6z", "note-G#6_eighth.": "G#6z.", "note-G#6_half": "G#6H", "note-G#6_half.": "G#6H.", "note-G#6_quarter": "G#6q", "note-G#6_quarter.": "G#6q.", "note-G#6_sixteenth": "G#6S", "note-G#6_sixteenth.": "G#6S.", "note-G#6_sixty_fourth": "G#6s", "note-G#6_sixty_fourth.": "G#6s.", "note-G#6_thirty_second": "G#6T", "note-G#6_whole": "G#6W", "note-G#6_whole.": "G#6W.", "note-G#7_eighth": "G#7z", "note-G#7_eighth.": "G#7z.", "note-G#7_half": "G#7H", "note-G#7_quarter": "G#7q", "note-G#7_quarter.": "G#7q.", "note-G#7_sixteenth": "G#7S", "note-G#7_sixty_fourth": "G#7s", "note-G#7_thirty_second": "G#7T", "note-G#8_eighth": "G#8z", "note-G0_eighth": "G0z", "note-G0_half": "G0H", "note-G0_quarter": "G0q", "note-G0_quarter.": "G0q.", "note-G0_sixteenth": "G0S", "note-G0_whole": "G0W", "note-G1_eighth": "G1z", "note-G1_eighth.": "G1z.", "note-G1_half": "G1H", "note-G1_half.": "G1H.", "note-G1_hundred_twenty_eighth": "G1h", "note-G1_quarter": "G1q", "note-G1_quarter.": "G1q.", "note-G1_sixteenth": "G1S", "note-G1_sixteenth.": "G1S.", "note-G1_sixty_fourth": "G1s", "note-G1_sixty_fourth.": "G1s.", "note-G1_thirty_second": "G1T", "note-G1_thirty_second.": "G1T.", "note-G1_whole": "G1W", "note-G1_whole.": "G1W.", "note-G2_breve": "G2Y", "note-G2_breve.": "G2Y.", "note-G2_double_whole": "G2w", "note-G2_double_whole.": "G2w.", "note-G2_double_whole_fermata": "G2w^", "note-G2_eighth": "G2z", "note-G2_eighth.": "G2z.", "note-G2_half": "G2H", "note-G2_half.": "G2H.", "note-G2_half_fermata": "G2H^", "note-G2_hundred_twenty_eighth": "G2h", "note-G2_quadruple_whole": "G2Q", "note-G2_quarter": "G2q", "note-G2_quarter.": "G2q.", "note-G2_quarter_fermata": "G2q^", "note-G2_sixteenth": "G2S", "note-G2_sixteenth.": "G2S.", "note-G2_sixty_fourth": "G2s", "note-G2_sixty_fourth.": "G2s.", "note-G2_thirty_second": "G2T", "note-G2_thirty_second.": "G2T.", "note-G2_whole": "G2W", "note-G2_whole.": "G2W.", "note-G2_whole_fermata": "G2W^", "note-G3_breve": "G3Y", "note-G3_breve.": "G3Y.", "note-G3_double_whole": "G3w", "note-G3_double_whole.": "G3w.", "note-G3_eighth": "G3z", "note-G3_eighth.": "G3z.", "note-G3_eighth..": "G3z..", "note-G3_eighth._fermata": "G3z.^", "note-G3_eighth_fermata": "G3z^", "note-G3_half": "G3H", "note-G3_half.": "G3H.", "note-G3_half._fermata": "G3H.^", "note-G3_half_fermata": "G3H^", "note-G3_hundred_twenty_eighth": "G3h", "note-G3_quadruple_whole": "G3Q", "note-G3_quarter": "G3q", "note-G3_quarter.": "G3q.", "note-G3_quarter..": "G3q..", "note-G3_quarter._fermata": "G3q.^", "note-G3_quarter_fermata": "G3q^", "note-G3_sixteenth": "G3S", "note-G3_sixteenth.": "G3S.", "note-G3_sixty_fourth": "G3s", "note-G3_sixty_fourth.": "G3s.", "note-G3_thirty_second": "G3T", "note-G3_thirty_second.": "G3T.", "note-G3_whole": "G3W", "note-G3_whole.": "G3W.", "note-G3_whole_fermata": "G3W^", "note-G4_breve": "G4Y", "note-G4_double_whole": "G4w", "note-G4_double_whole.": "G4w.", "note-G4_double_whole_fermata": "G4w^", "note-G4_eighth": "G4z", "note-G4_eighth.": "G4z.", "note-G4_eighth..": "G4z..", "note-G4_eighth_fermata": "G4z^", "note-G4_half": "G4H", "note-G4_half.": "G4H.", "note-G4_half._fermata": "G4H.^", "note-G4_half_fermata": "G4H^", "note-G4_hundred_twenty_eighth": "G4h", "note-G4_long": "G4long", "note-G4_quadruple_whole": "G4Q", "note-G4_quadruple_whole.": "G4Q.", "note-G4_quadruple_whole_fermata": "G4Q^", "note-G4_quarter": "G4q", "note-G4_quarter.": "G4q.", "note-G4_quarter..": "G4q..", "note-G4_quarter._fermata": "G4q.^", "note-G4_quarter_fermata": "G4q^", "note-G4_sixteenth": "G4S", "note-G4_sixteenth.": "G4S.", "note-G4_sixteenth..": "G4S..", "note-G4_sixteenth._fermata": "G4S.^", "note-G4_sixty_fourth": "G4s", "note-G4_sixty_fourth.": "G4s.", "note-G4_thirty_second": "G4T", "note-G4_thirty_second.": "G4T.", "note-G4_whole": "G4W", "note-G4_whole.": "G4W.", "note-G4_whole._fermata": "G4W.^", "note-G4_whole_fermata": "G4W^", "note-G5_breve": "G5Y", "note-G5_breve.": "G5Y.", "note-G5_double_whole": "G5w", "note-G5_double_whole.": "G5w.", "note-G5_eighth": "G5z", "note-G5_eighth.": "G5z.", "note-G5_eighth..": "G5z..", "note-G5_eighth_fermata": "G5z^", "note-G5_half": "G5H", "note-G5_half.": "G5H.", "note-G5_half..": "G5H..", "note-G5_half._fermata": "G5H.^", "note-G5_half_fermata": "G5H^", "note-G5_hundred_twenty_eighth": "G5h", "note-G5_quadruple_whole.": "G5Q.", "note-G5_quarter": "G5q", "note-G5_quarter.": "G5q.", "note-G5_quarter..": "G5q..", "note-G5_quarter._fermata": "G5q.^", "note-G5_quarter_fermata": "G5q^", "note-G5_sixteenth": "G5S", "note-G5_sixteenth.": "G5S.", "note-G5_sixty_fourth": "G5s", "note-G5_sixty_fourth.": "G5s.", "note-G5_thirty_second": "G5T", "note-G5_thirty_second.": "G5T.", "note-G5_whole": "G5W", "note-G5_whole.": "G5W.", "note-G5_whole_fermata": "G5W^", "note-G6_breve": "G6Y", "note-G6_breve.": "G6Y.", "note-G6_eighth": "G6z", "note-G6_eighth.": "G6z.", "note-G6_half": "G6H", "note-G6_half.": "G6H.", "note-G6_hundred_twenty_eighth": "G6h", "note-G6_quarter": "G6q", "note-G6_quarter.": "G6q.", "note-G6_sixteenth": "G6S", "note-G6_sixteenth.": "G6S.", "note-G6_sixty_fourth": "G6s", "note-G6_sixty_fourth.": "G6s.", "note-G6_thirty_second": "G6T", "note-G6_thirty_second.": "G6T.", "note-G6_whole": "G6W", "note-G6_whole.": "G6W.", "note-G7_eighth": "G7z", "note-G7_eighth.": "G7z.", "note-G7_half": "G7H", "note-G7_half.": "G7H.", "note-G7_hundred_twenty_eighth": "G7h", "note-G7_quarter": "G7q", "note-G7_quarter.": "G7q.", "note-G7_sixteenth": "G7S", "note-G7_sixteenth.": "G7S.", "note-G7_sixty_fourth": "G7s", "note-G7_thirty_second": "G7T", "note-G7_thirty_second.": "G7T.", "note-G7_whole": "G7W", "note-G7_whole.": "G7W.", "note-G8_eighth": "G8z", "note-G8_eighth.": "G8z.", "note-G8_hundred_twenty_eighth": "G8h", "note-G8_quarter": "G8q", "note-G8_sixteenth": "G8S", "note-G8_sixteenth.": "G8S.", "note-G8_sixty_fourth.": "G8s.", "note-G8_thirty_second": "G8T", "note-G9_quarter": "G9q", "note-GN0_eighth": "GN0z", "note-GN1_eighth": "GN1z", "note-GN1_half": "GN1H", "note-GN1_half.": "GN1H.", "note-GN1_quarter": "GN1q", "note-GN1_quarter.": "GN1q.", "note-GN1_sixteenth": "GN1S", "note-GN1_sixteenth.": "GN1S.", "note-GN1_thirty_second": "GN1T", "note-GN1_whole": "GN1W", "note-GN2_eighth": "GN2z", "note-GN2_eighth.": "GN2z.", "note-GN2_half": "GN2H", "note-GN2_half.": "GN2H.", "note-GN2_quarter": "GN2q", "note-GN2_quarter.": "GN2q.", "note-GN2_sixteenth": "GN2S", "note-GN2_sixty_fourth": "GN2s", "note-GN2_thirty_second": "GN2T", "note-GN2_whole": "GN2W", "note-GN3_eighth": "GN3z", "note-GN3_eighth.": "GN3z.", "note-GN3_half": "GN3H", "note-GN3_half.": "GN3H.", "note-GN3_hundred_twenty_eighth": "GN3h", "note-GN3_quarter": "GN3q", "note-GN3_quarter.": "GN3q.", "note-GN3_sixteenth": "GN3S", "note-GN3_sixteenth.": "GN3S.", "note-GN3_sixty_fourth": "GN3s", "note-GN3_thirty_second": "GN3T", "note-GN3_whole": "GN3W", "note-GN3_whole.": "GN3W.", "note-GN4_eighth": "GN4z", "note-GN4_eighth.": "GN4z.", "note-GN4_half": "GN4H", "note-GN4_half.": "GN4H.", "note-GN4_hundred_twenty_eighth": "GN4h", "note-GN4_quarter": "GN4q", "note-GN4_quarter.": "GN4q.", "note-GN4_sixteenth": "GN4S", "note-GN4_sixteenth.": "GN4S.", "note-GN4_sixty_fourth": "GN4s", "note-GN4_thirty_second": "GN4T", "note-GN4_thirty_second.": "GN4T.", "note-GN4_whole": "GN4W", "note-GN4_whole.": "GN4W.", "note-GN5_eighth": "GN5z", "note-GN5_eighth.": "GN5z.", "note-GN5_half": "GN5H", "note-GN5_half.": "GN5H.", "note-GN5_quarter": "GN5q", "note-GN5_quarter.": "GN5q.", "note-GN5_sixteenth": "GN5S", "note-GN5_sixteenth.": "GN5S.", "note-GN5_sixty_fourth": "GN5s", "note-GN5_thirty_second": "GN5T", "note-GN5_whole": "GN5W", "note-GN5_whole.": "GN5W.", "note-GN6_eighth": "GN6z", "note-GN6_eighth.": "GN6z.", "note-GN6_half": "GN6H", "note-GN6_half.": "GN6H.", "note-GN6_quarter": "GN6q", "note-GN6_quarter.": "GN6q.", "note-GN6_sixteenth": "GN6S", "note-GN6_sixteenth.": "GN6S.", "note-GN6_sixty_fourth": "GN6s", "note-GN6_thirty_second": "GN6T", "note-GN6_whole": "GN6W", "note-GN7_eighth": "GN7z", "note-GN7_sixteenth": "GN7S", "note-GN7_sixty_fourth": "GN7s", "note-GN7_thirty_second": "GN7T", "note-Gb1_eighth": "Gb1z", "note-Gb1_eighth.": "Gb1z.", "note-Gb1_half": "Gb1H", "note-Gb1_half.": "Gb1H.", "note-Gb1_quarter": "Gb1q", "note-Gb1_quarter.": "Gb1q.", "note-Gb1_sixteenth": "Gb1S", "note-Gb1_thirty_second": "Gb1T", "note-Gb1_thirty_second.": "Gb1T.", "note-Gb1_whole": "Gb1W", "note-Gb2_eighth": "Gb2z", "note-Gb2_eighth.": "Gb2z.", "note-Gb2_half": "Gb2H", "note-Gb2_half.": "Gb2H.", "note-Gb2_quarter": "Gb2q", "note-Gb2_quarter.": "Gb2q.", "note-Gb2_sixteenth": "Gb2S", "note-Gb2_sixteenth.": "Gb2S.", "note-Gb2_sixty_fourth": "Gb2s", "note-Gb2_sixty_fourth.": "Gb2s.", "note-Gb2_thirty_second": "Gb2T", "note-Gb2_thirty_second.": "Gb2T.", "note-Gb2_whole": "Gb2W", "note-Gb2_whole.": "Gb2W.", "note-Gb3_eighth": "Gb3z", "note-Gb3_eighth.": "Gb3z.", "note-Gb3_half": "Gb3H", "note-Gb3_half.": "Gb3H.", "note-Gb3_quarter": "Gb3q", "note-Gb3_quarter.": "Gb3q.", "note-Gb3_quarter..": "Gb3q..", "note-Gb3_sixteenth": "Gb3S", "note-Gb3_sixteenth.": "Gb3S.", "note-Gb3_sixty_fourth": "Gb3s", "note-Gb3_sixty_fourth.": "Gb3s.", "note-Gb3_thirty_second": "Gb3T", "note-Gb3_thirty_second.": "Gb3T.", "note-Gb3_whole": "Gb3W", "note-Gb3_whole.": "Gb3W.", "note-Gb4_eighth": "Gb4z", "note-Gb4_eighth.": "Gb4z.", "note-Gb4_eighth..": "Gb4z..", "note-Gb4_half": "Gb4H", "note-Gb4_half.": "Gb4H.", "note-Gb4_quarter": "Gb4q", "note-Gb4_quarter.": "Gb4q.", "note-Gb4_sixteenth": "Gb4S", "note-Gb4_sixteenth.": "Gb4S.", "note-Gb4_sixty_fourth": "Gb4s", "note-Gb4_sixty_fourth.": "Gb4s.", "note-Gb4_thirty_second": "Gb4T", "note-Gb4_thirty_second.": "Gb4T.", "note-Gb4_whole": "Gb4W", "note-Gb4_whole.": "Gb4W.", "note-Gb5_eighth": "Gb5z", "note-Gb5_eighth.": "Gb5z.", "note-Gb5_half": "Gb5H", "note-Gb5_half.": "Gb5H.", "note-Gb5_hundred_twenty_eighth": "Gb5h", "note-Gb5_quarter": "Gb5q", "note-Gb5_quarter.": "Gb5q.", "note-Gb5_quarter..": "Gb5q..", "note-Gb5_sixteenth": "Gb5S", "note-Gb5_sixteenth.": "Gb5S.", "note-Gb5_sixty_fourth": "Gb5s", "note-Gb5_thirty_second": "Gb5T", "note-Gb5_thirty_second.": "Gb5T.", "note-Gb5_whole": "Gb5W", "note-Gb5_whole.": "Gb5W.", "note-Gb6_eighth": "Gb6z", "note-Gb6_eighth.": "Gb6z.", "note-Gb6_half": "Gb6H", "note-Gb6_half.": "Gb6H.", "note-Gb6_quarter": "Gb6q", "note-Gb6_quarter.": "Gb6q.", "note-Gb6_sixteenth": "Gb6S", "note-Gb6_sixteenth.": "Gb6S.", "note-Gb6_sixty_fourth": "Gb6s", "note-Gb6_sixty_fourth.": "Gb6s.", "note-Gb6_thirty_second": "Gb6T", "note-Gb6_whole": "Gb6W", "note-Gb6_whole.": "Gb6W.", "note-Gb7_eighth": "Gb7z", "note-Gb7_half": "Gb7H", "note-Gb7_hundred_twenty_eighth": "Gb7h", "note-Gb7_quarter": "Gb7q", "note-Gb7_sixteenth": "Gb7S", "note-Gb7_sixty_fourth": "Gb7s", "note-Gb7_sixty_fourth.": "Gb7s.", "note-Gb7_thirty_second": "Gb7T", "note-Gbb2_eighth": "Gbb2z", "note-Gbb3_eighth": "Gbb3z", "note-Gbb3_half": "Gbb3H", "note-Gbb3_sixteenth": "Gbb3S", "note-Gbb4_half": "Gbb4H", "note-Gbb4_sixteenth": "Gbb4S", "note-Gbb5_eighth": "Gbb5z", "note-Gbb5_half": "Gbb5H", "note-Gbb5_sixteenth": "Gbb5S", "note-Gbb6_sixteenth": "Gbb6S", + "rest-breve": "rY", "rest-breve.": "rY.", "rest-eighth": "rz", "rest-eighth.": "rz.", "rest-eighth..": "rz..", "rest-eighth._fermata": "rz.^", "rest-eighth_fermata": "rz^", "rest-half": "rH", "rest-half.": "rH.", "rest-half._fermata": "rH.^", "rest-half_fermata": "rH^", "rest-hundred_twenty_eighth": "rh", "rest-long": "rlong", "rest-quadruple_whole": "rQ", "rest-quarter": "rq", "rest-quarter.": "rq.", "rest-quarter..": "rq..", "rest-quarter.._fermata": "rq..^", "rest-quarter._fermata": "rq.^", "rest-quarter_fermata": "rq^", "rest-sixteenth": "rS", "rest-sixteenth.": "rS.", "rest-sixteenth_fermata": "rS^", "rest-sixty_fourth": "rs", "rest-thirty_second": "rT", "rest-thirty_second.": "rT.", "rest-whole": "rW", "rest-whole.": "rW.", "rest-whole_fermata": "rW^", + "tie": "t", + "timeSignature-1/16": "s1/16", "timeSignature-1/2": "s1/2", "timeSignature-1/4": "s1/4", "timeSignature-1/8": "s1/8", "timeSignature-10/16": "s10/16", "timeSignature-10/4": "s10/4", "timeSignature-10/8": "s10/8", "timeSignature-11/16": "s11/16", "timeSignature-11/4": "s11/4", "timeSignature-11/8": "s11/8", "timeSignature-12/16": "s12/16", "timeSignature-12/32": "s12/32", "timeSignature-12/4": "s12/4", "timeSignature-12/8": "s12/8", "timeSignature-13/16": "s13/16", "timeSignature-13/8": "s13/8", "timeSignature-14/4": "s14/4", "timeSignature-14/8": "s14/8", "timeSignature-15/16": "s15/16", "timeSignature-15/4": "s15/4", "timeSignature-16/4": "s16/4", "timeSignature-17/16": "s17/16", "timeSignature-18/4": "s18/4", "timeSignature-19/16": "s19/16", "timeSignature-2/1": "s2/1", "timeSignature-2/2": "s2/2", "timeSignature-2/3": "s2/3", "timeSignature-2/32": "s2/32", "timeSignature-2/4": "s2/4", "timeSignature-2/48": "s2/48", "timeSignature-2/8": "s2/8", "timeSignature-20/8": "s20/8", "timeSignature-21/16": "s21/16", "timeSignature-22/8": "s22/8", "timeSignature-23/4": "s23/4", "timeSignature-23/8": "s23/8", "timeSignature-24/16": "s24/16", "timeSignature-27/16": "s27/16", "timeSignature-27/8": "s27/8", "timeSignature-28/8": "s28/8", "timeSignature-3/1": "s3/1", "timeSignature-3/16": "s3/16", "timeSignature-3/2": "s3/2", "timeSignature-3/4": "s3/4", "timeSignature-3/6": "s3/6", "timeSignature-3/8": "s3/8", "timeSignature-32/32": "s32/32", "timeSignature-33/32": "s33/32", "timeSignature-4/1": "s4/1", "timeSignature-4/16": "s4/16", "timeSignature-4/2": "s4/2", "timeSignature-4/4": "s4/4", "timeSignature-4/8": "s4/8", "timeSignature-5/16": "s5/16", "timeSignature-5/4": "s5/4", "timeSignature-5/8": "s5/8", "timeSignature-6/16": "s6/16", "timeSignature-6/2": "s6/2", "timeSignature-6/4": "s6/4", "timeSignature-6/8": "s6/8", "timeSignature-7/16": "s7/16", "timeSignature-7/2": "s7/2", "timeSignature-7/4": "s7/4", "timeSignature-7/8": "s7/8", "timeSignature-8/12": "s8/12", "timeSignature-8/16": "s8/16", "timeSignature-8/2": "s8/2", "timeSignature-8/4": "s8/4", "timeSignature-8/8": "s8/8", "timeSignature-9/16": "s9/16", "timeSignature-9/32": "s9/32", "timeSignature-9/4": "s9/4", "timeSignature-9/8": "s9/8", "timeSignature-C": "[", "timeSignature-C/": "[/" +} \ No newline at end of file From fe9acebf5495bd45dba5bb67085711737f5eb819 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Tue, 12 Sep 2023 12:51:54 +0200 Subject: [PATCH 03/76] Add base for LayoutEngineYolo using ultralytics YOLO. With conversion to PageLayout. --- pero_ocr/core/layout.py | 3 +- pero_ocr/document_ocr/page_parser.py | 101 ++++++++++++++++++- pero_ocr/layout_engines/cnn_layout_engine.py | 68 +++++++++++++ 3 files changed, 170 insertions(+), 2 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index cda62bc..a5b5f87 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -33,7 +33,7 @@ def export_id(id, validate_change_id): class TextLine(object): def __init__(self, id=None, baseline=None, polygon=None, heights=None, transcription=None, logits=None, crop=None, - characters=None, logit_coords=None, transcription_confidence=None, index=None): + characters=None, logit_coords=None, transcription_confidence=None, index=None, line_type=None): self.id = id self.index = index self.baseline = baseline @@ -45,6 +45,7 @@ def __init__(self, id=None, baseline=None, polygon=None, heights=None, transcrip self.characters = characters self.logit_coords = logit_coords self.transcription_confidence = transcription_confidence + self.line_type = line_type def get_dense_logits(self, zero_logit_value=-80): dense_logits = self.logits.toarray() diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index ca5132b..732cfe8 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -4,8 +4,10 @@ from multiprocessing import Pool import math import time +import re import torch.cuda +from PIL import Image from pero_ocr.utils import compose_path from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine @@ -14,7 +16,7 @@ from pero_ocr.ocr_engine.transformer_ocr_engine import TransformerEngineLineOCR from pero_ocr.layout_engines.simple_region_engine import SimpleThresholdRegion from pero_ocr.layout_engines.simple_baseline_engine import EngineLineDetectorSimple -from pero_ocr.layout_engines.cnn_layout_engine import LayoutEngine, LineFilterEngine +from pero_ocr.layout_engines.cnn_layout_engine import LayoutEngine, LineFilterEngine, LayoutEngineYolo from pero_ocr.layout_engines.line_postprocessing_engine import PostprocessingEngine from pero_ocr.layout_engines.naive_sorter import NaiveRegionSorter from pero_ocr.layout_engines.smart_sorter import SmartRegionSorter @@ -34,6 +36,8 @@ def layout_parser_factory(config, device, config_path='', order=1): layout_parser = SimpleThresholdRegion(config, config_path=config_path) elif config['METHOD'] == 'LAYOUT_CNN': layout_parser = LayoutExtractor(config, device, config_path=config_path) + elif config['METHOD'] == 'LAYOUT_YOLO': + layout_parser = LayoutExtractorYolo(config, device, config_path=config_path) elif config['METHOD'] == 'LINES_SIMPLE_THRESHOLD': layout_parser = TextlineExtractorSimple(config, config_path=config_path) elif config['METHOD'] == 'LINE_FILTER': @@ -299,6 +303,101 @@ def process_page(self, img, page_layout: PageLayout): return page_layout +class LayoutExtractorYolo(object): + def __init__(self, config, device, config_path=''): + self.detect_regions = config.getboolean('DETECT_REGIONS') + self.detect_lines = config.getboolean('DETECT_LINES') + # self.detect_straight_lines_in_regions = config.getboolean('DETECT_STRAIGHT_LINES_IN_REGIONS') + # self.merge_lines = config.getboolean('MERGE_LINES') + # self.adjust_heights = config.getboolean('ADJUST_HEIGHTS') + # self.multi_orientation = config.getboolean('MULTI_ORIENTATION') + # self.adjust_baselines = config.getboolean('ADJUST_BASELINES') + + use_cpu = config.getboolean('USE_CPU') + self.device = device if not use_cpu else torch.device("cpu") + + self.engine = LayoutEngineYolo( + model_path=compose_path(config['MODEL_PATH'], config_path), + device=self.device, + detection_threshold=config.getfloat('DETECTION_THRESHOLD'), + # downsample=config.getint('DOWNSAMPLE'), + # adaptive_downsample=config.getboolean('ADAPTIVE_DOWNSAMPLE', fallback=True), + # max_mp=config.getfloat('MAX_MEGAPIXELS'), + # line_end_weight=config.getfloat('LINE_END_WEIGHT', fallback=1.0), + # vertical_line_connection_range=config.getint('VERTICAL_LINE_CONNECTION_RANGE', fallback=5), + # smooth_line_predictions=config.getboolean('SMOOTH_LINE_PREDICTIONS', fallback=True), + # paragraph_line_threshold=config.getfloat('PARAGRAPH_LINE_THRESHOLD', fallback=0.3), + ) + # self.pool = Pool(1) + + def process_page(self, img, page_layout: PageLayout): + result = self.engine.detect(img) + # Show the result + # im_array = result.plot() # plot a BGR numpy array of predictions + # im = Image.fromarray(im_array[..., ::-1]) # RGB PIL image + # im.show() # show image + + polygons, baselines, heights = self.boxes_to_polygons(result.boxes.data) + + start_id = self.get_start_id(page_layout) + + # Add music regions to page layout + for id, (polygon, baseline, height) in enumerate(zip(polygons, baselines, heights)): + id_str = 'r{:03d}'.format(start_id + id) + region = RegionLayout(id_str, polygon, region_type='music notation') + + line = TextLine( + id=f'{id_str}-l000', + polygon=polygon, + baseline=baseline, + heights=height, + line_type='music notation' + ) + + region.lines.append(line) + page_layout.regions.append(region) + + return page_layout + + @staticmethod + def boxes_to_polygons(boxes: torch.Tensor) -> (list[np.ndarray], list[np.ndarray], list[np.ndarray]): + NOTE_LABEL = 10 + boxes = boxes[(boxes[:, 5] == NOTE_LABEL)].to(torch.int) + + polygons = [] + heights = [] + baselines = [] + for box in boxes: + x_min, y_min, x_max, y_max, *_ = box.cpu().tolist() + polygons.append( + np.array([[x_min, y_min], [x_min, y_max], [x_max, y_max], [x_max, y_min], [x_min, y_min]])) + + baseline_y = y_min + (y_max - y_min) / 2 + baselines.append(np.array([[x_min, baseline_y], [x_max, baseline_y]])) + + heights.append(np.array([baseline_y - y_min, y_max - baseline_y])) + return polygons, baselines, heights + + @staticmethod + def get_start_id(page_layout: PageLayout) -> int | None: + used_region_ids = sorted([region.id for region in page_layout.regions]) + if not used_region_ids: + return 0 + else: + start_id = self.get_last_region_id(used_region_ids) + 1 + + ids = [] + for id in used_region_ids: + id = re.match(r'r(\d+)', id).group(1) + try: + ids.append(int(id)) + except ValueError: + pass + + last_used_id = sorted(ids)[-1] + return last_used_id + 1 + + class LineFilter(object): def __init__(self, config, device, config_path): self.filter_directions = config.getboolean('FILTER_DIRECTIONS') diff --git a/pero_ocr/layout_engines/cnn_layout_engine.py b/pero_ocr/layout_engines/cnn_layout_engine.py index 08a57f6..c932ceb 100644 --- a/pero_ocr/layout_engines/cnn_layout_engine.py +++ b/pero_ocr/layout_engines/cnn_layout_engine.py @@ -8,6 +8,8 @@ from scipy.sparse.csgraph import connected_components import skimage.draw import shapely.geometry as sg +from ultralytics import YOLO +import torch from pero_ocr.layout_engines import layout_helpers as helpers from pero_ocr.layout_engines.torch_parsenet import TorchParseNet, TorchOrientationNet @@ -367,6 +369,72 @@ def make_clusters(self, b_list, h_list, t_list, layout_separator_map, ds): else: return [0] + +class LayoutEngineYolo(object): + def __init__(self, model_path, device, detection_threshold=0.2): + # downsample=4, max_mp=5, , adaptive_downsample=True, + # line_end_weight=1.0, vertical_line_connection_range=5, smooth_line_predictions=True, + # paragraph_line_threshold=0.3): + self.yolo_net = YoloNet( + model_path, + device=device, + detection_threshold=detection_threshold + # downsample=downsample, + # adaptive_downsample=adaptive_downsample, + # max_mp=max_mp, + ) + + self.line_detection_threshold = detection_threshold + # self.line_end_weight = line_end_weight + # self.vertical_line_connection_range = vertical_line_connection_range + # self.smooth_line_predictions = smooth_line_predictions + # self.adaptive_downsample = adaptive_downsample + # self.paragraph_line_threshold = paragraph_line_threshold + + params = ' '.join([f'{name}:{str(getattr(self, name))}' + for name in ['line_detection_threshold']]) + # for name in ['line_end_weight', 'vertical_line_connection_range', 'smooth_line_predictions', 'line_detection_threshold', 'adaptive_downsample']]) + print(f'LayoutEngine params are {params}') + + def detect(self, image): + # , rot=0): + """Uses parsenet to find lines and region separators, clusters vertically + close lines by computing penalties and postprocesses the resulting + regions. + :param image: input image + :param rot: number of counter-clockwise 90degree rotations (0 <= n <= 3) + """ + return self.yolo_net.detect_using_yolo(image) + + +class YoloNet: + + def __init__(self, model_path, device, detection_threshold=0.2): + # downsample=4, max_mp=5, adaptive_downsample=True): + + self.detection_threshold = detection_threshold + # self.min_line_processing_height = 9 + # self.max_line_processing_height = 15 + # self.optimal_line_processing_height = 12 + + self.yolo_net = YOLO(model_path).to(device) + self.net = torch.load(model_path) # .eval().to(device) + + # self.adaptive_downsample = adaptive_downsample + # self.init_downsample = downsample + # self.last_downsample = downsample + # self.downsample_line_pixel_adapt_threshold = 100 + # self.min_downsample = 1 + # self.max_downsample = 8 + + def detect_using_yolo(self, img): + return self.yolo_net(img, conf=self.detection_threshold)[0] + + def detect(self, img): + print('Detect NOT implemented yet, use detect_using_yolo instead.') + return self.net(img) + + def nonmaxima_suppression(input, element_size=(7, 1)): """Vertical non-maxima suppression. :param input: input array From 2da0f24cd5a7bd5e9d42522d1498a188b2ccc1fa Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Wed, 13 Sep 2023 15:21:18 +0200 Subject: [PATCH 04/76] Junk-code cleanup and docu. --- pero_ocr/document_ocr/page_parser.py | 19 ----------- pero_ocr/layout_engines/cnn_layout_engine.py | 35 ++------------------ 2 files changed, 3 insertions(+), 51 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 732cfe8..a7cbd13 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -307,11 +307,6 @@ class LayoutExtractorYolo(object): def __init__(self, config, device, config_path=''): self.detect_regions = config.getboolean('DETECT_REGIONS') self.detect_lines = config.getboolean('DETECT_LINES') - # self.detect_straight_lines_in_regions = config.getboolean('DETECT_STRAIGHT_LINES_IN_REGIONS') - # self.merge_lines = config.getboolean('MERGE_LINES') - # self.adjust_heights = config.getboolean('ADJUST_HEIGHTS') - # self.multi_orientation = config.getboolean('MULTI_ORIENTATION') - # self.adjust_baselines = config.getboolean('ADJUST_BASELINES') use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") @@ -320,25 +315,11 @@ def __init__(self, config, device, config_path=''): model_path=compose_path(config['MODEL_PATH'], config_path), device=self.device, detection_threshold=config.getfloat('DETECTION_THRESHOLD'), - # downsample=config.getint('DOWNSAMPLE'), - # adaptive_downsample=config.getboolean('ADAPTIVE_DOWNSAMPLE', fallback=True), - # max_mp=config.getfloat('MAX_MEGAPIXELS'), - # line_end_weight=config.getfloat('LINE_END_WEIGHT', fallback=1.0), - # vertical_line_connection_range=config.getint('VERTICAL_LINE_CONNECTION_RANGE', fallback=5), - # smooth_line_predictions=config.getboolean('SMOOTH_LINE_PREDICTIONS', fallback=True), - # paragraph_line_threshold=config.getfloat('PARAGRAPH_LINE_THRESHOLD', fallback=0.3), ) - # self.pool = Pool(1) def process_page(self, img, page_layout: PageLayout): result = self.engine.detect(img) - # Show the result - # im_array = result.plot() # plot a BGR numpy array of predictions - # im = Image.fromarray(im_array[..., ::-1]) # RGB PIL image - # im.show() # show image - polygons, baselines, heights = self.boxes_to_polygons(result.boxes.data) - start_id = self.get_start_id(page_layout) # Add music regions to page layout diff --git a/pero_ocr/layout_engines/cnn_layout_engine.py b/pero_ocr/layout_engines/cnn_layout_engine.py index c932ceb..2393612 100644 --- a/pero_ocr/layout_engines/cnn_layout_engine.py +++ b/pero_ocr/layout_engines/cnn_layout_engine.py @@ -372,37 +372,21 @@ def make_clusters(self, b_list, h_list, t_list, layout_separator_map, ds): class LayoutEngineYolo(object): def __init__(self, model_path, device, detection_threshold=0.2): - # downsample=4, max_mp=5, , adaptive_downsample=True, - # line_end_weight=1.0, vertical_line_connection_range=5, smooth_line_predictions=True, - # paragraph_line_threshold=0.3): self.yolo_net = YoloNet( model_path, device=device, detection_threshold=detection_threshold - # downsample=downsample, - # adaptive_downsample=adaptive_downsample, - # max_mp=max_mp, ) self.line_detection_threshold = detection_threshold - # self.line_end_weight = line_end_weight - # self.vertical_line_connection_range = vertical_line_connection_range - # self.smooth_line_predictions = smooth_line_predictions - # self.adaptive_downsample = adaptive_downsample - # self.paragraph_line_threshold = paragraph_line_threshold params = ' '.join([f'{name}:{str(getattr(self, name))}' for name in ['line_detection_threshold']]) - # for name in ['line_end_weight', 'vertical_line_connection_range', 'smooth_line_predictions', 'line_detection_threshold', 'adaptive_downsample']]) - print(f'LayoutEngine params are {params}') + print(f'LayoutEngineYolo params are {params}') def detect(self, image): - # , rot=0): - """Uses parsenet to find lines and region separators, clusters vertically - close lines by computing penalties and postprocesses the resulting - regions. + """Uses yolo_net to find bounding boxes. :param image: input image - :param rot: number of counter-clockwise 90degree rotations (0 <= n <= 3) """ return self.yolo_net.detect_using_yolo(image) @@ -410,28 +394,15 @@ def detect(self, image): class YoloNet: def __init__(self, model_path, device, detection_threshold=0.2): - # downsample=4, max_mp=5, adaptive_downsample=True): - self.detection_threshold = detection_threshold - # self.min_line_processing_height = 9 - # self.max_line_processing_height = 15 - # self.optimal_line_processing_height = 12 - self.yolo_net = YOLO(model_path).to(device) self.net = torch.load(model_path) # .eval().to(device) - # self.adaptive_downsample = adaptive_downsample - # self.init_downsample = downsample - # self.last_downsample = downsample - # self.downsample_line_pixel_adapt_threshold = 100 - # self.min_downsample = 1 - # self.max_downsample = 8 - def detect_using_yolo(self, img): return self.yolo_net(img, conf=self.detection_threshold)[0] def detect(self, img): - print('Detect NOT implemented yet, use detect_using_yolo instead.') + print('ERR: Detect NOT implemented yet, use detect_using_yolo instead.') return self.net(img) From 2f661d2b3b39c2d1ff6f104c4fb68409f56ecc97 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Wed, 13 Sep 2023 18:16:28 +0200 Subject: [PATCH 05/76] Prepare attributes for music-text distinction. WITHOUT exports to PageXML or ALTO. --- pero_ocr/core/layout.py | 18 +++++++++++++++--- pero_ocr/document_ocr/page_parser.py | 11 +++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index a5b5f87..6cac173 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -33,7 +33,7 @@ def export_id(id, validate_change_id): class TextLine(object): def __init__(self, id=None, baseline=None, polygon=None, heights=None, transcription=None, logits=None, crop=None, - characters=None, logit_coords=None, transcription_confidence=None, index=None, line_type=None): + characters=None, logit_coords=None, transcription_confidence=None, index=None): self.id = id self.index = index self.baseline = baseline @@ -45,7 +45,6 @@ def __init__(self, id=None, baseline=None, polygon=None, heights=None, transcrip self.characters = characters self.logit_coords = logit_coords self.transcription_confidence = transcription_confidence - self.line_type = line_type def get_dense_logits(self, zero_logit_value=-80): dense_logits = self.logits.toarray() @@ -58,12 +57,13 @@ def get_full_logprobs(self, zero_logit_value=-80): class RegionLayout(object): - def __init__(self, id, polygon, region_type=None): + def __init__(self, id, polygon, region_type=None, music_region=False): self.id = id # ID string self.polygon = polygon # bounding polygon self.region_type = region_type self.lines = [] self.transcription = None + self.music_region = music_region def to_page_xml(self, page_element, validate_id=False): region_element = ET.SubElement(page_element, "TextRegion") @@ -821,6 +821,18 @@ def get_quality(self, x=None, y=None, width=None, height=None, power=6): else: return -1 + def delete_regions_of_type(self, type=None): + self.regions = [region for region in self.regions if region.type == type] + + def regions_of_type_iterator(self, type=None): + if type is None: + for region in self.regions: + yield region + else: + for region in self.regions: + if region.type == type: + yield region + def draw_lines(img, lines, color=(255, 0, 0), circles=(False, False, False), close=False, thickness=2): """Draw a line into image. diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index a7cbd13..6cae042 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -304,6 +304,7 @@ def process_page(self, img, page_layout: PageLayout): class LayoutExtractorYolo(object): + REGION_TYPE = 'music' def __init__(self, config, device, config_path=''): self.detect_regions = config.getboolean('DETECT_REGIONS') self.detect_lines = config.getboolean('DETECT_LINES') @@ -318,6 +319,8 @@ def __init__(self, config, device, config_path=''): ) def process_page(self, img, page_layout: PageLayout): + page_layout.delete_regions_of_type(self.REGION_TYPE) + result = self.engine.detect(img) polygons, baselines, heights = self.boxes_to_polygons(result.boxes.data) start_id = self.get_start_id(page_layout) @@ -325,14 +328,14 @@ def process_page(self, img, page_layout: PageLayout): # Add music regions to page layout for id, (polygon, baseline, height) in enumerate(zip(polygons, baselines, heights)): id_str = 'r{:03d}'.format(start_id + id) - region = RegionLayout(id_str, polygon, region_type='music notation') + region = RegionLayout(id_str, polygon, music_region=True) line = TextLine( id=f'{id_str}-l000', + index=0, polygon=polygon, baseline=baseline, - heights=height, - line_type='music notation' + heights=height ) region.lines.append(line) @@ -356,7 +359,7 @@ def boxes_to_polygons(boxes: torch.Tensor) -> (list[np.ndarray], list[np.ndarray baseline_y = y_min + (y_max - y_min) / 2 baselines.append(np.array([[x_min, baseline_y], [x_max, baseline_y]])) - heights.append(np.array([baseline_y - y_min, y_max - baseline_y])) + heights.append(np.floor(np.array([baseline_y - y_min, y_max - baseline_y]))) return polygons, baselines, heights @staticmethod From 53732832f720afce2f9804606171619acbc36911 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 14 Sep 2023 12:55:47 +0200 Subject: [PATCH 06/76] Add support for `region.music_region`. WITHOUT exports to PageXML or ALTO. --- pero_ocr/core/layout.py | 14 ++++---------- pero_ocr/document_ocr/page_parser.py | 10 ++++------ 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 6cac173..5f1e4a8 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -821,17 +821,11 @@ def get_quality(self, x=None, y=None, width=None, height=None, power=6): else: return -1 - def delete_regions_of_type(self, type=None): - self.regions = [region for region in self.regions if region.type == type] + def delete_text_regions(self): + self.regions = [region for region in self.regions if region.music_regions] - def regions_of_type_iterator(self, type=None): - if type is None: - for region in self.regions: - yield region - else: - for region in self.regions: - if region.type == type: - yield region + def delete_music_regions(self): + self.regions = [region for region in self.regions if not region.music_regions] def draw_lines(img, lines, color=(255, 0, 0), circles=(False, False, False), close=False, thickness=2): diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 6cae042..509511d 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -233,10 +233,11 @@ def __init__(self, config, device, config_path=''): def process_page(self, img, page_layout: PageLayout): if self.detect_regions or self.detect_lines: if self.detect_regions: - page_layout.regions = [] + page_layout.delete_text_regions() if self.detect_lines: for region in page_layout.regions: - region.lines = [] + if not region.music_region: + region.lines = [] if self.multi_orientation: orientations = [0, 1, 3] @@ -306,9 +307,6 @@ def process_page(self, img, page_layout: PageLayout): class LayoutExtractorYolo(object): REGION_TYPE = 'music' def __init__(self, config, device, config_path=''): - self.detect_regions = config.getboolean('DETECT_REGIONS') - self.detect_lines = config.getboolean('DETECT_LINES') - use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") @@ -319,7 +317,7 @@ def __init__(self, config, device, config_path=''): ) def process_page(self, img, page_layout: PageLayout): - page_layout.delete_regions_of_type(self.REGION_TYPE) + page_layout.delete_music_regions() result = self.engine.detect(img) polygons, baselines, heights = self.boxes_to_polygons(result.boxes.data) From 1b9b67639fa947aef67aa46f8120f149d630bd7c Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 14 Sep 2023 16:26:37 +0200 Subject: [PATCH 07/76] Add category attribute to region. WITH saving to pageXML custom tag + loading. --- pero_ocr/core/layout.py | 41 ++++++++++++++++--- pero_ocr/document_ocr/page_parser.py | 43 +++++++------------- pero_ocr/layout_engines/cnn_layout_engine.py | 31 ++------------ 3 files changed, 52 insertions(+), 63 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 5f1e4a8..ca4b9eb 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -56,14 +56,32 @@ def get_full_logprobs(self, zero_logit_value=-80): return log_softmax(dense_logits) +class RegionCategory(Enum): + chemical_formula = 0 + logo_symbol = 1 + ex_libris = 2 + photo = 3 + geometric_drawing = 4 + initial = 5 + music = 10 + # ... and other label classes of layout detector + text = 42 + unknown = 99 + + @classmethod + def _missing_(cls, value): + return cls.unknown + + class RegionLayout(object): - def __init__(self, id, polygon, region_type=None, music_region=False): + def __init__(self, id, polygon, region_type=None, category=RegionCategory.unknown): self.id = id # ID string self.polygon = polygon # bounding polygon self.region_type = region_type + self.category = category + self.lines = [] self.transcription = None - self.music_region = music_region def to_page_xml(self, page_element, validate_id=False): region_element = ET.SubElement(page_element, "TextRegion") @@ -73,6 +91,10 @@ def to_page_xml(self, page_element, validate_id=False): if self.region_type is not None: region_element.set("type", self.region_type) + if self.category is not RegionCategory.unknown: + custom = json.dumps({"category": self.category.name}) + region_element.set("custom", custom) + points = ["{},{}".format(int(np.round(coord[0])), int(np.round(coord[1]))) for coord in self.polygon] points = " ".join(points) coords.set("points", points) @@ -103,7 +125,12 @@ def get_region_from_page_xml(region_element, schema): if "type" in region_element.attrib: region_type = region_element.attrib["type"] - layout_region = RegionLayout(region_element.attrib['id'], region_coords, region_type) + category = RegionCategory.unknown + if "custom" in region_element.attrib: + custom = json.loads(region_element.attrib["custom"]) + category = RegionCategory[custom.get('category', 'unknown')] + + layout_region = RegionLayout(region_element.attrib['id'], region_coords, region_type, category=category) transcription = region_element.find(schema + 'TextEquiv') if transcription is not None: @@ -822,10 +849,12 @@ def get_quality(self, x=None, y=None, width=None, height=None, power=6): return -1 def delete_text_regions(self): - self.regions = [region for region in self.regions if region.music_regions] + self.regions = [region for region in self.regions + if region.category not in [RegionCategory.text, RegionCategory.unknown]] - def delete_music_regions(self): - self.regions = [region for region in self.regions if not region.music_regions] + def delete_yolo_regions(self): + self.regions = [region for region in self.regions + if region.category in [RegionCategory.text, RegionCategory.unknown]] def draw_lines(img, lines, color=(255, 0, 0), circles=(False, False, False), close=False, thickness=2): diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 509511d..25ebad8 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -10,7 +10,7 @@ from PIL import Image from pero_ocr.utils import compose_path -from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine +from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine, RegionCategory import pero_ocr.core.crop_engine as cropper from pero_ocr.ocr_engine.pytorch_ocr_engine import PytorchEngineLineOCR from pero_ocr.ocr_engine.transformer_ocr_engine import TransformerEngineLineOCR @@ -236,7 +236,7 @@ def process_page(self, img, page_layout: PageLayout): page_layout.delete_text_regions() if self.detect_lines: for region in page_layout.regions: - if not region.music_region: + if region.category == RegionCategory.text: region.lines = [] if self.multi_orientation: @@ -253,7 +253,7 @@ def process_page(self, img, page_layout: PageLayout): id = 'r{:03d}_{}'.format(id, rot) else: id = 'r{:03d}'.format(id) - region = RegionLayout(id, polygon) + region = RegionLayout(id, polygon, category=RegionCategory.text) regions.append(region) if self.detect_lines: if not self.detect_regions: @@ -305,7 +305,6 @@ def process_page(self, img, page_layout: PageLayout): class LayoutExtractorYolo(object): - REGION_TYPE = 'music' def __init__(self, config, device, config_path=''): use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") @@ -317,16 +316,21 @@ def __init__(self, config, device, config_path=''): ) def process_page(self, img, page_layout: PageLayout): - page_layout.delete_music_regions() - + page_layout.delete_yolo_regions() result = self.engine.detect(img) - polygons, baselines, heights = self.boxes_to_polygons(result.boxes.data) start_id = self.get_start_id(page_layout) - # Add music regions to page layout - for id, (polygon, baseline, height) in enumerate(zip(polygons, baselines, heights)): + boxes = result.boxes.data.cpu() + for id, box in enumerate(boxes): id_str = 'r{:03d}'.format(start_id + id) - region = RegionLayout(id_str, polygon, music_region=True) + + x_min, y_min, x_max, y_max, _, class_label = box.tolist() + polygon = np.array([[x_min, y_min], [x_min, y_max], [x_max, y_max], [x_max, y_min], [x_min, y_min]]) + baseline_y = y_min + (y_max - y_min) / 2 + baseline = np.array([[x_min, baseline_y], [x_max, baseline_y]]) + height = np.floor(np.array([baseline_y - y_min, y_max - baseline_y])) + + region = RegionLayout(id_str, polygon, category=RegionCategory(class_label)) line = TextLine( id=f'{id_str}-l000', @@ -341,25 +345,6 @@ def process_page(self, img, page_layout: PageLayout): return page_layout - @staticmethod - def boxes_to_polygons(boxes: torch.Tensor) -> (list[np.ndarray], list[np.ndarray], list[np.ndarray]): - NOTE_LABEL = 10 - boxes = boxes[(boxes[:, 5] == NOTE_LABEL)].to(torch.int) - - polygons = [] - heights = [] - baselines = [] - for box in boxes: - x_min, y_min, x_max, y_max, *_ = box.cpu().tolist() - polygons.append( - np.array([[x_min, y_min], [x_min, y_max], [x_max, y_max], [x_max, y_min], [x_min, y_min]])) - - baseline_y = y_min + (y_max - y_min) / 2 - baselines.append(np.array([[x_min, baseline_y], [x_max, baseline_y]])) - - heights.append(np.floor(np.array([baseline_y - y_min, y_max - baseline_y]))) - return polygons, baselines, heights - @staticmethod def get_start_id(page_layout: PageLayout) -> int | None: used_region_ids = sorted([region.id for region in page_layout.regions]) diff --git a/pero_ocr/layout_engines/cnn_layout_engine.py b/pero_ocr/layout_engines/cnn_layout_engine.py index 2393612..0971fe7 100644 --- a/pero_ocr/layout_engines/cnn_layout_engine.py +++ b/pero_ocr/layout_engines/cnn_layout_engine.py @@ -372,39 +372,14 @@ def make_clusters(self, b_list, h_list, t_list, layout_separator_map, ds): class LayoutEngineYolo(object): def __init__(self, model_path, device, detection_threshold=0.2): - self.yolo_net = YoloNet( - model_path, - device=device, - detection_threshold=detection_threshold - ) - - self.line_detection_threshold = detection_threshold - - params = ' '.join([f'{name}:{str(getattr(self, name))}' - for name in ['line_detection_threshold']]) - print(f'LayoutEngineYolo params are {params}') + self.yolo_net = YOLO(model_path).to(device) + self.detection_threshold = detection_threshold def detect(self, image): """Uses yolo_net to find bounding boxes. :param image: input image """ - return self.yolo_net.detect_using_yolo(image) - - -class YoloNet: - - def __init__(self, model_path, device, detection_threshold=0.2): - self.detection_threshold = detection_threshold - self.yolo_net = YOLO(model_path).to(device) - self.net = torch.load(model_path) # .eval().to(device) - - def detect_using_yolo(self, img): - return self.yolo_net(img, conf=self.detection_threshold)[0] - - def detect(self, img): - print('ERR: Detect NOT implemented yet, use detect_using_yolo instead.') - return self.net(img) - + return self.yolo_net(image, conf=self.detection_threshold)[0] def nonmaxima_suppression(input, element_size=(7, 1)): """Vertical non-maxima suppression. From 7cc5293cc1ea33bf7f3184edf733e34fc2758bde Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 14 Sep 2023 17:23:26 +0200 Subject: [PATCH 08/76] Add category attribute to TextLine. WITH saving to pageXML custom tag + loading. --- pero_ocr/core/layout.py | 64 ++++++++++++++++++---------- pero_ocr/document_ocr/page_parser.py | 24 ++++++----- 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index ca4b9eb..add8fb0 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -31,9 +31,19 @@ def export_id(id, validate_change_id): return 'id_' + id if validate_change_id else id +class LineCategory(Enum): + music = 10 + text = 42 + unknown = 99 + + @classmethod + def _missing_(cls, value): + return cls.unknown + class TextLine(object): def __init__(self, id=None, baseline=None, polygon=None, heights=None, transcription=None, logits=None, crop=None, - characters=None, logit_coords=None, transcription_confidence=None, index=None): + characters=None, logit_coords=None, transcription_confidence=None, index=None, + category=LineCategory.unknown): self.id = id self.index = index self.baseline = baseline @@ -45,6 +55,7 @@ def __init__(self, id=None, baseline=None, polygon=None, heights=None, transcrip self.characters = characters self.logit_coords = logit_coords self.transcription_confidence = transcription_confidence + self.category = category def get_dense_logits(self, zero_logit_value=-80): dense_logits = self.logits.toarray() @@ -273,26 +284,31 @@ def from_pagexml(self, file): for line_i, line in enumerate(region.iter(schema + 'TextLine')): new_textline = TextLine(id=line.attrib['id']) if 'custom' in line.attrib: - custom_str = line.attrib['custom'] - if 'heights_v2' in custom_str: - for word in custom_str.split(): - if 'heights_v2' in word: - new_textline.heights = json.loads(word.split(":")[1]) - else: - if re.findall("heights", line.attrib['custom']): - heights = re.findall("\d+", line.attrib['custom']) - heights_array = np.asarray([float(x) for x in heights]) - if heights_array.shape[0] == 4: - heights = np.zeros(2, dtype=np.float32) - heights[0] = heights_array[0] - heights[1] = heights_array[2] - elif heights_array.shape[0] == 3: - heights = np.zeros(2, dtype=np.float32) - heights[0] = heights_array[1] - heights[1] = heights_array[2] - heights_array[0] - else: - heights = heights_array - new_textline.heights = heights.tolist() + try: + custom = json.loads(line.attrib['custom']) + new_textline.category = RegionCategory[custom.get('category', 'unknown')] + new_textline.heights = custom.get('heights', None) + except json.decoder.JSONDecodeError: + custom_str = line.attrib['custom'] + if 'heights_v2' in custom_str: + for word in custom_str.split(): + if 'heights_v2' in word: + new_textline.heights = json.loads(word.split(":")[1]) + else: + if re.findall("heights", line.attrib['custom']): + heights = re.findall("\d+", line.attrib['custom']) + heights_array = np.asarray([float(x) for x in heights]) + if heights_array.shape[0] == 4: + heights = np.zeros(2, dtype=np.float32) + heights[0] = heights_array[0] + heights[1] = heights_array[2] + elif heights_array.shape[0] == 3: + heights = np.zeros(2, dtype=np.float32) + heights[0] = heights_array[1] + heights[1] = heights_array[2] - heights_array[0] + else: + heights = heights_array + new_textline.heights = heights.tolist() if 'index' in line.attrib: try: @@ -373,7 +389,11 @@ def to_pagexml_string(self, creator='Pero OCR', validate_id=False, version=PAGEV else: text_line.set("index", f'{i:d}') if line.heights is not None: - text_line.set("custom", f"heights_v2:[{line.heights[0]:.1f},{line.heights[1]:.1f}]") + custom = { + "heights": list(np.round(line.heights, decimals=1)), + "category": line.category.name + } + text_line.set("custom", json.dumps(custom)) coords = ET.SubElement(text_line, "Coords") diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 25ebad8..6ce1a5a 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -10,7 +10,7 @@ from PIL import Image from pero_ocr.utils import compose_path -from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine, RegionCategory +from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine, RegionCategory, LineCategory import pero_ocr.core.crop_engine as cropper from pero_ocr.ocr_engine.pytorch_ocr_engine import PytorchEngineLineOCR from pero_ocr.ocr_engine.transformer_ocr_engine import TransformerEngineLineOCR @@ -330,17 +330,19 @@ def process_page(self, img, page_layout: PageLayout): baseline = np.array([[x_min, baseline_y], [x_max, baseline_y]]) height = np.floor(np.array([baseline_y - y_min, y_max - baseline_y])) - region = RegionLayout(id_str, polygon, category=RegionCategory(class_label)) + region_category = RegionCategory(class_label) + region = RegionLayout(id_str, polygon, category=region_category) - line = TextLine( - id=f'{id_str}-l000', - index=0, - polygon=polygon, - baseline=baseline, - heights=height - ) - - region.lines.append(line) + if region_category == RegionCategory.music: + line = TextLine( + id=f'{id_str}-l000', + index=0, + polygon=polygon, + baseline=baseline, + heights=height, + category=LineCategory.music + ) + region.lines.append(line) page_layout.regions.append(region) return page_layout From fae66ce6d4fcdace9eee7b9865718a48be240d21 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 14 Sep 2023 17:30:00 +0200 Subject: [PATCH 09/76] Little refactoring. --- pero_ocr/core/layout.py | 52 +++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index add8fb0..d5ebb52 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -284,31 +284,7 @@ def from_pagexml(self, file): for line_i, line in enumerate(region.iter(schema + 'TextLine')): new_textline = TextLine(id=line.attrib['id']) if 'custom' in line.attrib: - try: - custom = json.loads(line.attrib['custom']) - new_textline.category = RegionCategory[custom.get('category', 'unknown')] - new_textline.heights = custom.get('heights', None) - except json.decoder.JSONDecodeError: - custom_str = line.attrib['custom'] - if 'heights_v2' in custom_str: - for word in custom_str.split(): - if 'heights_v2' in word: - new_textline.heights = json.loads(word.split(":")[1]) - else: - if re.findall("heights", line.attrib['custom']): - heights = re.findall("\d+", line.attrib['custom']) - heights_array = np.asarray([float(x) for x in heights]) - if heights_array.shape[0] == 4: - heights = np.zeros(2, dtype=np.float32) - heights[0] = heights_array[0] - heights[1] = heights_array[2] - elif heights_array.shape[0] == 3: - heights = np.zeros(2, dtype=np.float32) - heights[0] = heights_array[1] - heights[1] = heights_array[2] - heights_array[0] - else: - heights = heights_array - new_textline.heights = heights.tolist() + self.from_pagexml_parse_line_custom(new_textline, line.attrib['custom']) if 'index' in line.attrib: try: @@ -345,6 +321,32 @@ def from_pagexml(self, file): self.regions.append(region_layout) + def from_pagexml_parse_line_custom(self, textline: TextLine, custom_str): + try: + custom = json.loads(custom_str) + textline.category = RegionCategory[custom.get('category', 'unknown')] + textline.heights = custom.get('heights', None) + except json.decoder.JSONDecodeError: + if 'heights_v2' in custom_str: + for word in custom_str.split(): + if 'heights_v2' in word: + textline.heights = json.loads(word.split(":")[1]) + else: + if re.findall("heights", custom_str): + heights = re.findall("\d+", custom_str) + heights_array = np.asarray([float(x) for x in heights]) + if heights_array.shape[0] == 4: + heights = np.zeros(2, dtype=np.float32) + heights[0] = heights_array[0] + heights[1] = heights_array[2] + elif heights_array.shape[0] == 3: + heights = np.zeros(2, dtype=np.float32) + heights[0] = heights_array[1] + heights[1] = heights_array[2] - heights_array[0] + else: + heights = heights_array + textline.heights = heights.tolist() + def to_pagexml_string(self, creator='Pero OCR', validate_id=False, version=PAGEVersion.PAGE_2019_07_15): if version == PAGEVersion.PAGE_2019_07_15: attr_qname = ET.QName("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation") From b02ebb2104a059338859306c62499b7abb85d464 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 14 Sep 2023 20:12:34 +0200 Subject: [PATCH 10/76] Add exporting music directly in `parse_folder.py` using `config.ini` to define settings and `page_parser.py` to create music exporter object of `music/export_music/ExportMusicPage`. --- pero_ocr/core/layout.py | 7 +++ pero_ocr/document_ocr/page_parser.py | 22 +++++++ pero_ocr/music/__init__.py | 2 - pero_ocr/music/export_music.py | 93 ++++++++++++++++------------ pero_ocr/music/music_structures.py | 2 +- user_scripts/parse_folder.py | 12 +++- 6 files changed, 94 insertions(+), 44 deletions(-) delete mode 100644 pero_ocr/music/__init__.py diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index d5ebb52..4e3ff23 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -115,6 +115,10 @@ def to_page_xml(self, page_element, validate_id=False): text_element.text = self.transcription return region_element + def get_lines_of_category(self, category: LineCategory): + return [line for line in self.lines if line.category == category] + + def get_coords_form_page_xml(coords_element, schema): if 'points' in coords_element.attrib: @@ -878,6 +882,9 @@ def delete_yolo_regions(self): self.regions = [region for region in self.regions if region.category in [RegionCategory.text, RegionCategory.unknown]] + def get_regions_of_category(self, category: RegionCategory): + return [region for region in self.regions if region.category == category] + def draw_lines(img, lines, color=(255, 0, 0), circles=(False, False, False), close=False, thickness=2): """Draw a line into image. diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 6ce1a5a..369a739 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -23,6 +23,7 @@ from pero_ocr.layout_engines.line_in_region_detector import detect_lines_in_region from pero_ocr.layout_engines.baseline_refiner import refine_baseline from pero_ocr.layout_engines import layout_helpers as helpers +from pero_ocr.music.export_music import ExportMusicPage logger = logging.getLogger(__name__) @@ -78,6 +79,21 @@ def page_decoder_factory(config, device, config_path=''): return PageDecoder(decoder, line_confidence_threshold=confidence_threshold, carry_h_over=carry_h_over) +def music_exporter_factory(config, config_path=''): + output_folder = config['PARSE_FOLDER']['OUTPUT_MUSIC_PATH'] + config = config['MUSIC_EXPORTER'] + export_midi = config.getboolean('EXPORT_MIDI') + export_musicxml = config.getboolean('EXPORT_MUSICXML') + translator_path = config.get('TRANSLATOR_PATH', None) + + return ExportMusicPage( + translator_path=translator_path, + export_midi=export_midi, + export_musicxml=export_musicxml, + output_folder=output_folder + ) + + class MissingLogits(Exception): pass @@ -524,6 +540,7 @@ def __init__(self, config, device=None, config_path='', ): self.run_line_cropper = config['PAGE_PARSER'].getboolean('RUN_LINE_CROPPER', fallback=False) self.run_ocr = config['PAGE_PARSER'].getboolean('RUN_OCR', fallback=False) self.run_decoder = config['PAGE_PARSER'].getboolean('RUN_DECODER', fallback=False) + self.export_music = config['PAGE_PARSER'].getboolean('EXPORT_MUSIC', fallback=False) self.filter_confident_lines_threshold = config['PAGE_PARSER'].getfloat('FILTER_CONFIDENT_LINES_THRESHOLD', fallback=-1) @@ -531,6 +548,7 @@ def __init__(self, config, device=None, config_path='', ): self.line_cropper = None self.ocr = None self.decoder = None + self.music_exporter = None self.device = device if device is not None else get_default_device() @@ -545,6 +563,8 @@ def __init__(self, config, device=None, config_path='', ): self.ocr = ocr_factory(config, self.device, config_path=config_path) if self.run_decoder: self.decoder = page_decoder_factory(config, self.device, config_path=config_path) + if self.export_music: + self.music_exporter = music_exporter_factory(config, config_path=config_path) @staticmethod def compute_line_confidence(line, threshold=None): @@ -579,6 +599,8 @@ def process_page(self, image, page_layout): page_layout = self.ocr.process_page(image, page_layout) if self.run_decoder: page_layout = self.decoder.process_page(page_layout) + if self.export_music: + self.music_exporter.process_page(page_layout) self.update_confidences(page_layout) diff --git a/pero_ocr/music/__init__.py b/pero_ocr/music/__init__.py deleted file mode 100644 index 60b154f..0000000 --- a/pero_ocr/music/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from export_music import ExportMusicPage -from export_music import ExportMusicLines diff --git a/pero_ocr/music/export_music.py b/pero_ocr/music/export_music.py index 93c7fe0..84bf84e 100644 --- a/pero_ocr/music/export_music.py +++ b/pero_ocr/music/export_music.py @@ -24,8 +24,8 @@ import music21 as music -from music_structures import Measure -from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine +from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine, RegionCategory, LineCategory +from pero_ocr.music.music_structures import Measure def parseargs(): @@ -49,6 +49,10 @@ def parseargs(): "-m", "--export-midi", action='store_true', help=("Enable exporting midi file to output_folder." "Exports whole file and individual lines with names corresponding to them TextLine IDs.")) + parser.add_argument( + "-m", "--export-musicxml", action='store_true', + help=("Enable exporting musicxml file to output_folder." + "Exports whole file as one MusicXML.")) parser.add_argument( '-v', "--verbose", action='store_true', default=False, help="Activate verbose logging.") @@ -76,9 +80,8 @@ def main(): class ExportMusicPage: """Take pageLayout XML exported from pero-ocr with transcriptions and re-construct page of musical notation.""" - def __init__(self, input_xml_path: str = '', translator_path: str = '', - input_transcription_files: list[str] = None, - output_folder: str = 'output_page', export_midi: bool = False, + def __init__(self, input_xml_path: str = '', input_transcription_files: list[str] = None, translator_path: str = '', + output_folder: str = 'output_page', export_midi: bool = False, export_musicxml: bool = False, verbose: bool = False): self.translator_path = translator_path if verbose: @@ -97,68 +100,78 @@ def __init__(self, input_xml_path: str = '', translator_path: str = '', os.makedirs(output_folder) self.output_folder = output_folder self.export_midi = export_midi + self.export_musicxml = export_musicxml self.translator = Translator(file_name=self.translator_path) - def __call__(self) -> None: + def __call__(self, page_layout = None) -> None: if self.input_transcription_files: ExportMusicLines(input_files=self.input_transcription_files, output_folder=self.output_folder, translator=self.translator, verbose=self.verbose)() + if page_layout: + self.process_page(page_layout) if self.input_xml_path: - self.export_xml() + input_page_layout = PageLayout(file=self.input_xml_path) + self.export_page_layout(input_page_layout) - def export_xml(self) -> None: - page = PageLayout(file=self.input_xml_path) - print(f'Page {self.input_xml_path} loaded successfully.') + def process_page(self, page_layout: PageLayout) -> None: + self.export_page_layout(page_layout, page_layout.id) - parts = ExportMusicPage.regions_to_parts(page.regions, self.translator, self.export_midi) - music_parts = [] - for part in parts: - music_parts.append(part.encode_to_music21()) + def export_page_layout(self, page_layout: PageLayout, file_id: str = None) -> None: + if self.export_musicxml or self.export_midi: + parts = ExportMusicPage.regions_to_parts( + page_layout.get_regions_of_category(RegionCategory.music), + self.translator) + + music_parts = [] + for part in parts: + music_parts.append(part.encode_to_music21()) - # Finalize score creation - metadata = music.metadata.Metadata() - metadata.title = metadata.composer = '' - score = music.stream.Score([metadata] + music_parts) + # Finalize score creation + metadata = music.metadata.Metadata() + metadata.title = metadata.composer = '' + score = music.stream.Score([metadata] + music_parts) - # Export score to MusicXML or something - output_file = self.get_output_file('musicxml') - xml = music21_to_musicxml(score) - write_to_file(output_file, xml) + if self.export_musicxml: + output_file = self.get_output_file(file_id, extension='musicxml') + xml = music21_to_musicxml(score) + write_to_file(output_file, xml) - if self.export_midi: - self.export_to_midi(score, parts) + if self.export_midi: + self.export_to_midi(score, parts, file_id) - def get_output_file(self, extension: str = 'musicxml') -> str: - base = self.get_output_file_base() + def get_output_file(self, file_id: str=None, extension: str = 'musicxml') -> str: + base = self.get_output_file_base(file_id) return f'{base}.{extension}' - def get_output_file_base(self) -> str: - input_file = os.path.basename(self.input_xml_path) - name, *_ = re.split(r'\.', input_file) + def get_output_file_base(self, file_id: str=None) -> str: + if not file_id: + file_id = os.path.basename(self.input_xml_path) + if not file_id: + file_id = 'output' + name, *_ = re.split(r'\.', file_id) return os.path.join(self.output_folder, f'{name}') - def export_to_midi(self, score, parts): + def export_to_midi(self, score, parts, file_id: str=None): # Export whole score to midi - output_file = self.get_output_file('mid') + output_file = self.get_output_file(file_id, extension='mid') score.write("midi", output_file) for part in parts: - base = self.get_output_file_base() + base = self.get_output_file_base(file_id) part.export_to_midi(base) @staticmethod - def regions_to_parts(regions: list[RegionLayout], translator, export_midi: bool = False - ) -> list: # -> list[Part]: + def regions_to_parts(regions: list[RegionLayout], translator) -> list[Part]: # -> list """Takes a list of regions and splits them to parts.""" - max_parts = max([len(region.lines) for region in regions]) - - # TODO add empty measure padding to parts without textlines in multi-part scores. + max_parts = max( + [len(region.get_lines_of_category(LineCategory.music)) for region in regions] + ) parts = [Part(translator) for _ in range(max_parts)] for region in regions: - for part, line in zip(parts, region.lines): + for part, line in zip(parts, region.get_lines_of_category(LineCategory.music)): part.add_textline(line) return parts @@ -289,7 +302,6 @@ class TextLineWrapper: """Class to wrap one TextLine for easier export etc.""" def __init__(self, text_line: TextLine, measures: list[music.stream.Measure]): self.text_line = text_line - print(f'len of measures: {len(measures)}') self.repr_music21 = music.stream.Part([music.instrument.Piano()] + measures) def export_midi(self, file_base: str = 'out'): @@ -327,6 +339,9 @@ def convert_symbol(self, symbol: str, to_shorter: bool = True): @staticmethod def read_json(filename) -> dict: + if not os.path.isfile(filename): + raise FileNotFoundError(f'Translator file ({filename}) not found. Cannot export music.') + with open(filename) as f: data = json.load(f) return data diff --git a/pero_ocr/music/music_structures.py b/pero_ocr/music/music_structures.py index 950f013..8261cab 100644 --- a/pero_ocr/music/music_structures.py +++ b/pero_ocr/music/music_structures.py @@ -12,7 +12,7 @@ import logging import music21 as music -from music_symbols import Symbol, SymbolType, AlteredPitches, LENGTH_TO_SYMBOL +from pero_ocr.music.music_symbols import Symbol, SymbolType, AlteredPitches, LENGTH_TO_SYMBOL class Measure: diff --git a/user_scripts/parse_folder.py b/user_scripts/parse_folder.py index a20ed82..f81a9c6 100644 --- a/user_scripts/parse_folder.py +++ b/user_scripts/parse_folder.py @@ -37,6 +37,8 @@ def parse_arguments(): parser.add_argument('--output-logit-path', help='') parser.add_argument('--output-alto-path', help='') parser.add_argument('--output-transcriptions-file-path', help='') + parser.add_argument('--output-music-path', help='Where to export music files (MusicXML/midi). ' + 'More options in config.ini') parser.add_argument('--skipp-missing-xml', action='store_true', help='Skipp images which have missing xml.') parser.add_argument('--device', choices=["gpu", "cpu"], default="gpu") @@ -139,7 +141,7 @@ def __call__(self, page_layout: PageLayout, file_id): class Computator: def __init__(self, page_parser, input_image_path, input_xml_path, input_logit_path, output_render_path, - output_logit_path, output_alto_path, output_xml_path, output_line_path): + output_logit_path, output_alto_path, output_xml_path, output_line_path, output_music_path): self.page_parser = page_parser self.input_image_path = input_image_path self.input_xml_path = input_xml_path @@ -149,6 +151,7 @@ def __init__(self, page_parser, input_image_path, input_xml_path, input_logit_pa self.output_alto_path = output_alto_path self.output_xml_path = output_xml_path self.output_line_path = output_line_path + self.output_music_path = output_music_path def __call__(self, image_file_name, file_id, index, ids_count): print(f"Processing {file_id}") @@ -253,6 +256,8 @@ def main(): config['PARSE_FOLDER']['OUTPUT_LOGIT_PATH'] = args.output_logit_path if args.output_alto_path is not None: config['PARSE_FOLDER']['OUTPUT_ALTO_PATH'] = args.output_alto_path + if args.output_music_path is not None: + config['PARSE_FOLDER']['OUTPUT_MUSIC_PATH'] = args.output_music_path setup_logging(config['PARSE_FOLDER']) logger = logging.getLogger() @@ -270,6 +275,7 @@ def main(): output_xml_path = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_XML_PATH') output_logit_path = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_LOGIT_PATH') output_alto_path = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_ALTO_PATH') + output_music_path = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_MUSIC_PATH') if output_render_path is not None: create_dir_if_not_exists(output_render_path) @@ -281,6 +287,8 @@ def main(): create_dir_if_not_exists(output_logit_path) if output_alto_path is not None: create_dir_if_not_exists(output_alto_path) + if output_music_path is not None: + create_dir_if_not_exists(output_music_path) if input_logit_path is not None and input_xml_path is None: input_logit_path = None @@ -326,7 +334,7 @@ def main(): images_to_process = filtered_images_to_process computator = Computator(page_parser, input_image_path, input_xml_path, input_logit_path, output_render_path, - output_logit_path, output_alto_path, output_xml_path, output_line_path) + output_logit_path, output_alto_path, output_xml_path, output_line_path, output_music_path) t_start = time.time() results = [] From c07ba6a4c5f1019c1672ea840f1d1a886b575813 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Mon, 18 Sep 2023 12:23:45 +0200 Subject: [PATCH 11/76] Fix minor issue with non-existing function `get_las_region_id` --- pero_ocr/document_ocr/page_parser.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 369a739..c518035 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -7,7 +7,6 @@ import re import torch.cuda -from PIL import Image from pero_ocr.utils import compose_path from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine, RegionCategory, LineCategory @@ -337,8 +336,8 @@ def process_page(self, img, page_layout: PageLayout): start_id = self.get_start_id(page_layout) boxes = result.boxes.data.cpu() - for id, box in enumerate(boxes): - id_str = 'r{:03d}'.format(start_id + id) + for box_id, box in enumerate(boxes): + id_str = 'r{:03d}'.format(start_id + box_id) x_min, y_min, x_max, y_max, _, class_label = box.tolist() polygon = np.array([[x_min, y_min], [x_min, y_max], [x_max, y_max], [x_max, y_min], [x_min, y_min]]) @@ -364,12 +363,14 @@ def process_page(self, img, page_layout: PageLayout): return page_layout @staticmethod - def get_start_id(page_layout: PageLayout) -> int | None: + def get_start_id(page_layout: PageLayout) -> int: + """Get int from which to start id naming for new regions. + + Expected region id is in format rXXX, where XXX is number. + """ used_region_ids = sorted([region.id for region in page_layout.regions]) if not used_region_ids: return 0 - else: - start_id = self.get_last_region_id(used_region_ids) + 1 ids = [] for id in used_region_ids: From 0afb958dfbf68fcc9d6e4ba5239c6e65f78a08a9 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Mon, 18 Sep 2023 14:54:30 +0200 Subject: [PATCH 12/76] Add sorting music regions "in reading order" using `y_min` of bounding box around polygon. --- pero_ocr/core/layout.py | 38 ++++++++++++++++++++++++++++ pero_ocr/document_ocr/page_parser.py | 22 ++++++++++++++++ pero_ocr/music/export_music.py | 2 +- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 4e3ff23..f363fb9 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -118,6 +118,22 @@ def to_page_xml(self, page_element, validate_id=False): def get_lines_of_category(self, category: LineCategory): return [line for line in self.lines if line.category == category] + def replace_id(self, new_id): + """Replace region ID and all IDs in TextLines which has region ID inside them.""" + for line in self.lines: + line.id = line.id.replace(self.id, new_id) + self.id = new_id + + def get_polygon_bounding_box(self) -> tuple[int, int, int, int]: + """Get bounding box of region polygon that includes all polygon points. + :return: tuple[int, int, int, int]: (x_min, y_min, x_max, y_max) + """ + x_min = min(self.polygon[:, 0]) + x_max = max(self.polygon[:, 0]) + y_min = min(self.polygon[:, 1]) + y_max = max(self.polygon[:, 1]) + + return x_min, y_min, x_max, y_max def get_coords_form_page_xml(coords_element, schema): @@ -885,6 +901,28 @@ def delete_yolo_regions(self): def get_regions_of_category(self, category: RegionCategory): return [region for region in self.regions if region.category == category] + def rename_region_id(self, old_id, new_id): + for region in self.regions: + if region.id == old_id: + region.replace_id(new_id) + break + else: + raise ValueError(f'Region with id {old_id} not found.') + + def get_music_regions_in_reading_order(self): + music_regions = [region for region in self.regions if region.category == RegionCategory.music] + + regions_with_bounding_boxes = {} + for region in music_regions: + regions_with_bounding_boxes[region] = { + 'id': region.id, + 'bounding_box': region.get_polygon_bounding_box(), + 'region': region} + + regions_sorted = sorted(regions_with_bounding_boxes.items(), key=lambda x: x[1]['bounding_box'][1]) + + return [region[1]['region'] for region in regions_sorted] + def draw_lines(img, lines, color=(255, 0, 0), circles=(False, False, False), close=False, thickness=2): """Draw a line into image. diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index c518035..7d46a54 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -360,6 +360,8 @@ def process_page(self, img, page_layout: PageLayout): region.lines.append(line) page_layout.regions.append(region) + page_layout = self.sort_music_regions_in_reading_order(page_layout) + return page_layout @staticmethod @@ -383,6 +385,26 @@ def get_start_id(page_layout: PageLayout) -> int: last_used_id = sorted(ids)[-1] return last_used_id + 1 + @staticmethod + def sort_music_regions_in_reading_order(page_layout: PageLayout) -> PageLayout: + music_regions = [region for region in page_layout.regions if region.category == RegionCategory.music] + music_region_ids = [region.id for region in music_regions] + + regions_with_bounding_boxes = {} + for region in music_regions: + regions_with_bounding_boxes[region] = {'id': region.id, 'bounding_box': region.get_polygon_bounding_box()} + + regions_sorted = sorted(regions_with_bounding_boxes.items(), key=lambda x: x[1]['bounding_box'][0]) + + # Rename all music regions as rXXX_tmp to prevent two regions having the same id while renaming them + for region in music_regions: + region.replace_id(region.id + '_tmp') + + for sorted_region, region_id in zip(regions_sorted, music_region_ids): + page_layout.rename_region_id(sorted_region[1]['id'] + '_tmp', region_id) + + return page_layout + class LineFilter(object): def __init__(self, config, device, config_path): diff --git a/pero_ocr/music/export_music.py b/pero_ocr/music/export_music.py index 84bf84e..c98f1d0 100644 --- a/pero_ocr/music/export_music.py +++ b/pero_ocr/music/export_music.py @@ -121,7 +121,7 @@ def process_page(self, page_layout: PageLayout) -> None: def export_page_layout(self, page_layout: PageLayout, file_id: str = None) -> None: if self.export_musicxml or self.export_midi: parts = ExportMusicPage.regions_to_parts( - page_layout.get_regions_of_category(RegionCategory.music), + page_layout.get_music_regions_in_reading_order(), self.translator) music_parts = [] From 9371467df1131ad4776b47f093205de1a960aa77 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Tue, 3 Oct 2023 13:47:14 +0200 Subject: [PATCH 13/76] Remove `RegionCategory` and `LineCategory` enums hard-coded in `layout.py`. Get names only from Yolo `result.names`. --- pero_ocr/core/layout.py | 55 ++++++++-------------------- pero_ocr/document_ocr/page_parser.py | 18 ++++----- pero_ocr/music/export_music.py | 6 +-- 3 files changed, 28 insertions(+), 51 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index f363fb9..b5dcd15 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -31,19 +31,10 @@ def export_id(id, validate_change_id): return 'id_' + id if validate_change_id else id -class LineCategory(Enum): - music = 10 - text = 42 - unknown = 99 - - @classmethod - def _missing_(cls, value): - return cls.unknown - class TextLine(object): def __init__(self, id=None, baseline=None, polygon=None, heights=None, transcription=None, logits=None, crop=None, characters=None, logit_coords=None, transcription_confidence=None, index=None, - category=LineCategory.unknown): + category: str = None): self.id = id self.index = index self.baseline = baseline @@ -67,25 +58,8 @@ def get_full_logprobs(self, zero_logit_value=-80): return log_softmax(dense_logits) -class RegionCategory(Enum): - chemical_formula = 0 - logo_symbol = 1 - ex_libris = 2 - photo = 3 - geometric_drawing = 4 - initial = 5 - music = 10 - # ... and other label classes of layout detector - text = 42 - unknown = 99 - - @classmethod - def _missing_(cls, value): - return cls.unknown - - class RegionLayout(object): - def __init__(self, id, polygon, region_type=None, category=RegionCategory.unknown): + def __init__(self, id, polygon, region_type=None, category: str = None): self.id = id # ID string self.polygon = polygon # bounding polygon self.region_type = region_type @@ -102,8 +76,8 @@ def to_page_xml(self, page_element, validate_id=False): if self.region_type is not None: region_element.set("type", self.region_type) - if self.category is not RegionCategory.unknown: - custom = json.dumps({"category": self.category.name}) + if self.category: + custom = json.dumps({"category": self.category}) region_element.set("custom", custom) points = ["{},{}".format(int(np.round(coord[0])), int(np.round(coord[1]))) for coord in self.polygon] @@ -115,9 +89,12 @@ def to_page_xml(self, page_element, validate_id=False): text_element.text = self.transcription return region_element - def get_lines_of_category(self, category: LineCategory): + def get_lines_of_category(self, category: str): return [line for line in self.lines if line.category == category] + def get_music_lines(self): + return [line for line in self.lines if line.category == 'Notový zápis'] + def replace_id(self, new_id): """Replace region ID and all IDs in TextLines which has region ID inside them.""" for line in self.lines: @@ -156,10 +133,10 @@ def get_region_from_page_xml(region_element, schema): if "type" in region_element.attrib: region_type = region_element.attrib["type"] - category = RegionCategory.unknown + category = None if "custom" in region_element.attrib: custom = json.loads(region_element.attrib["custom"]) - category = RegionCategory[custom.get('category', 'unknown')] + category = custom.get('category', None) layout_region = RegionLayout(region_element.attrib['id'], region_coords, region_type, category=category) @@ -344,7 +321,7 @@ def from_pagexml(self, file): def from_pagexml_parse_line_custom(self, textline: TextLine, custom_str): try: custom = json.loads(custom_str) - textline.category = RegionCategory[custom.get('category', 'unknown')] + textline.category = custom.get('category', None) textline.heights = custom.get('heights', None) except json.decoder.JSONDecodeError: if 'heights_v2' in custom_str: @@ -413,7 +390,7 @@ def to_pagexml_string(self, creator='Pero OCR', validate_id=False, version=PAGEV if line.heights is not None: custom = { "heights": list(np.round(line.heights, decimals=1)), - "category": line.category.name + "category": line.category } text_line.set("custom", json.dumps(custom)) @@ -892,13 +869,13 @@ def get_quality(self, x=None, y=None, width=None, height=None, power=6): def delete_text_regions(self): self.regions = [region for region in self.regions - if region.category not in [RegionCategory.text, RegionCategory.unknown]] + if region.category not in ['text', None]] def delete_yolo_regions(self): self.regions = [region for region in self.regions - if region.category in [RegionCategory.text, RegionCategory.unknown]] + if region.category in ['text', None]] - def get_regions_of_category(self, category: RegionCategory): + def get_regions_of_category(self, category: str): return [region for region in self.regions if region.category == category] def rename_region_id(self, old_id, new_id): @@ -910,7 +887,7 @@ def rename_region_id(self, old_id, new_id): raise ValueError(f'Region with id {old_id} not found.') def get_music_regions_in_reading_order(self): - music_regions = [region for region in self.regions if region.category == RegionCategory.music] + music_regions = [region for region in self.regions if region.category == 'Notový zápis'] regions_with_bounding_boxes = {} for region in music_regions: diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 7d46a54..72d2e38 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -9,7 +9,7 @@ import torch.cuda from pero_ocr.utils import compose_path -from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine, RegionCategory, LineCategory +from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine import pero_ocr.core.crop_engine as cropper from pero_ocr.ocr_engine.pytorch_ocr_engine import PytorchEngineLineOCR from pero_ocr.ocr_engine.transformer_ocr_engine import TransformerEngineLineOCR @@ -251,7 +251,7 @@ def process_page(self, img, page_layout: PageLayout): page_layout.delete_text_regions() if self.detect_lines: for region in page_layout.regions: - if region.category == RegionCategory.text: + if region.category == 'text': region.lines = [] if self.multi_orientation: @@ -268,7 +268,7 @@ def process_page(self, img, page_layout: PageLayout): id = 'r{:03d}_{}'.format(id, rot) else: id = 'r{:03d}'.format(id) - region = RegionLayout(id, polygon, category=RegionCategory.text) + region = RegionLayout(id, polygon, category='text') regions.append(region) if self.detect_lines: if not self.detect_regions: @@ -339,23 +339,23 @@ def process_page(self, img, page_layout: PageLayout): for box_id, box in enumerate(boxes): id_str = 'r{:03d}'.format(start_id + box_id) - x_min, y_min, x_max, y_max, _, class_label = box.tolist() + x_min, y_min, x_max, y_max, _, class_id = box.tolist() polygon = np.array([[x_min, y_min], [x_min, y_max], [x_max, y_max], [x_max, y_min], [x_min, y_min]]) baseline_y = y_min + (y_max - y_min) / 2 baseline = np.array([[x_min, baseline_y], [x_max, baseline_y]]) height = np.floor(np.array([baseline_y - y_min, y_max - baseline_y])) - region_category = RegionCategory(class_label) - region = RegionLayout(id_str, polygon, category=region_category) + category = result.names[class_id] + region = RegionLayout(id_str, polygon, category=category) - if region_category == RegionCategory.music: + if category == 'Notový zápis': line = TextLine( id=f'{id_str}-l000', index=0, polygon=polygon, baseline=baseline, heights=height, - category=LineCategory.music + category='Notový zápis' ) region.lines.append(line) page_layout.regions.append(region) @@ -387,7 +387,7 @@ def get_start_id(page_layout: PageLayout) -> int: @staticmethod def sort_music_regions_in_reading_order(page_layout: PageLayout) -> PageLayout: - music_regions = [region for region in page_layout.regions if region.category == RegionCategory.music] + music_regions = [region for region in page_layout.regions if region.category == 'Notový zápis'] music_region_ids = [region.id for region in music_regions] regions_with_bounding_boxes = {} diff --git a/pero_ocr/music/export_music.py b/pero_ocr/music/export_music.py index c98f1d0..b511cd8 100644 --- a/pero_ocr/music/export_music.py +++ b/pero_ocr/music/export_music.py @@ -24,7 +24,7 @@ import music21 as music -from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine, RegionCategory, LineCategory +from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine from pero_ocr.music.music_structures import Measure @@ -166,12 +166,12 @@ def export_to_midi(self, score, parts, file_id: str=None): def regions_to_parts(regions: list[RegionLayout], translator) -> list[Part]: # -> list """Takes a list of regions and splits them to parts.""" max_parts = max( - [len(region.get_lines_of_category(LineCategory.music)) for region in regions] + [len(region.get_music_lines()) for region in regions] ) parts = [Part(translator) for _ in range(max_parts)] for region in regions: - for part, line in zip(parts, region.get_lines_of_category(LineCategory.music)): + for part, line in zip(parts, region.get_music_lines()): part.add_textline(line) return parts From b733c74d74e949bb560f997d25b24a8a9f23e008 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Tue, 3 Oct 2023 15:27:32 +0200 Subject: [PATCH 14/76] Update `MusicExporter` to export music only from certain categories of lines. --- pero_ocr/core/layout.py | 37 +++++++++++++++++----------- pero_ocr/document_ocr/page_parser.py | 26 ++++++++++++------- pero_ocr/music/export_music.py | 20 +++++++++------ 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index b5dcd15..63f94ba 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -89,11 +89,11 @@ def to_page_xml(self, page_element, validate_id=False): text_element.text = self.transcription return region_element - def get_lines_of_category(self, category: str): - return [line for line in self.lines if line.category == category] + def get_lines_of_category(self, categories: str | list): + if isinstance(categories, str): + categories = [categories] - def get_music_lines(self): - return [line for line in self.lines if line.category == 'Notový zápis'] + return [line for line in self.lines if line.category in categories] def replace_id(self, new_id): """Replace region ID and all IDs in TextLines which has region ID inside them.""" @@ -875,19 +875,20 @@ def delete_yolo_regions(self): self.regions = [region for region in self.regions if region.category in ['text', None]] - def get_regions_of_category(self, category: str): - return [region for region in self.regions if region.category == category] + def get_regions_of_category(self, categories: str | list, reading_order=False): + if isinstance(categories, str): + category = [categories] - def rename_region_id(self, old_id, new_id): + if not reading_order: + return [region for region in self.regions if region.category in categories] + + print('self.regions:') for region in self.regions: - if region.id == old_id: - region.replace_id(new_id) - break - else: - raise ValueError(f'Region with id {old_id} not found.') + print(f'({region.category}) in ({categories}) => {region.category in categories}') - def get_music_regions_in_reading_order(self): - music_regions = [region for region in self.regions if region.category == 'Notový zápis'] + music_regions = [region for region in self.regions if region.category in categories] + print('music regions:') + print(music_regions) regions_with_bounding_boxes = {} for region in music_regions: @@ -900,6 +901,14 @@ def get_music_regions_in_reading_order(self): return [region[1]['region'] for region in regions_sorted] + def rename_region_id(self, old_id, new_id): + for region in self.regions: + if region.id == old_id: + region.replace_id(new_id) + break + else: + raise ValueError(f'Region with id {old_id} not found.') + def draw_lines(img, lines, color=(255, 0, 0), circles=(False, False, False), close=False, thickness=2): """Draw a line into image. diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 72d2e38..1913cd3 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -5,6 +5,7 @@ import math import time import re +import json import torch.cuda @@ -78,18 +79,20 @@ def page_decoder_factory(config, device, config_path=''): return PageDecoder(decoder, line_confidence_threshold=confidence_threshold, carry_h_over=carry_h_over) -def music_exporter_factory(config, config_path=''): +def music_exporter_factory(config): output_folder = config['PARSE_FOLDER']['OUTPUT_MUSIC_PATH'] config = config['MUSIC_EXPORTER'] export_midi = config.getboolean('EXPORT_MIDI') export_musicxml = config.getboolean('EXPORT_MUSICXML') - translator_path = config.get('TRANSLATOR_PATH', None) + translator_path = config.get('TRANSLATOR_PATH', None) + categories = json.loads(config.get('CATEGORIES', [])) return ExportMusicPage( translator_path=translator_path, export_midi=export_midi, export_musicxml=export_musicxml, - output_folder=output_folder + output_folder=output_folder, + categories=categories ) @@ -323,6 +326,7 @@ class LayoutExtractorYolo(object): def __init__(self, config, device, config_path=''): use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") + self.categories_for_transcription = config.get('CATEGORIES_FOR_TRANSCRIPTION', []) self.engine = LayoutEngineYolo( model_path=compose_path(config['MODEL_PATH'], config_path), @@ -348,19 +352,19 @@ def process_page(self, img, page_layout: PageLayout): category = result.names[class_id] region = RegionLayout(id_str, polygon, category=category) - if category == 'Notový zápis': + if category in self.categories_for_transcription: line = TextLine( id=f'{id_str}-l000', index=0, polygon=polygon, baseline=baseline, heights=height, - category='Notový zápis' + category=category ) region.lines.append(line) page_layout.regions.append(region) - page_layout = self.sort_music_regions_in_reading_order(page_layout) + page_layout = self.sort_regions_in_reading_order(page_layout, self.categories_for_transcription) return page_layout @@ -386,8 +390,12 @@ def get_start_id(page_layout: PageLayout) -> int: return last_used_id + 1 @staticmethod - def sort_music_regions_in_reading_order(page_layout: PageLayout) -> PageLayout: - music_regions = [region for region in page_layout.regions if region.category == 'Notový zápis'] + def sort_regions_in_reading_order(page_layout: PageLayout, categories_to_sort: list = None) -> PageLayout: + if not categories_to_sort: + music_regions = page_layout.regions + else: + music_regions = [region for region in page_layout.regions if region.category in categories_to_sort] + music_region_ids = [region.id for region in music_regions] regions_with_bounding_boxes = {} @@ -587,7 +595,7 @@ def __init__(self, config, device=None, config_path='', ): if self.run_decoder: self.decoder = page_decoder_factory(config, self.device, config_path=config_path) if self.export_music: - self.music_exporter = music_exporter_factory(config, config_path=config_path) + self.music_exporter = music_exporter_factory(config) @staticmethod def compute_line_confidence(line, threshold=None): diff --git a/pero_ocr/music/export_music.py b/pero_ocr/music/export_music.py index b511cd8..6f15acb 100644 --- a/pero_ocr/music/export_music.py +++ b/pero_ocr/music/export_music.py @@ -82,7 +82,7 @@ class ExportMusicPage: def __init__(self, input_xml_path: str = '', input_transcription_files: list[str] = None, translator_path: str = '', output_folder: str = 'output_page', export_midi: bool = False, export_musicxml: bool = False, - verbose: bool = False): + categories: list = None, verbose: bool = False): self.translator_path = translator_path if verbose: logging.basicConfig(level=logging.DEBUG, format='[%(levelname)-s] \t- %(message)s') @@ -103,6 +103,7 @@ def __init__(self, input_xml_path: str = '', input_transcription_files: list[str self.export_musicxml = export_musicxml self.translator = Translator(file_name=self.translator_path) + self.categories = categories def __call__(self, page_layout = None) -> None: if self.input_transcription_files: @@ -120,9 +121,11 @@ def process_page(self, page_layout: PageLayout) -> None: def export_page_layout(self, page_layout: PageLayout, file_id: str = None) -> None: if self.export_musicxml or self.export_midi: - parts = ExportMusicPage.regions_to_parts( - page_layout.get_music_regions_in_reading_order(), + parts = self.regions_to_parts( + page_layout.get_regions_of_category(self.categories, reading_order=True), self.translator) + if not parts: + return music_parts = [] for part in parts: @@ -162,16 +165,19 @@ def export_to_midi(self, score, parts, file_id: str=None): base = self.get_output_file_base(file_id) part.export_to_midi(base) - @staticmethod - def regions_to_parts(regions: list[RegionLayout], translator) -> list[Part]: # -> list + def regions_to_parts(self, regions: list[RegionLayout], translator) -> list[Part]: """Takes a list of regions and splits them to parts.""" max_parts = max( - [len(region.get_music_lines()) for region in regions] + [len(region.get_lines_of_category(self.categories)) for region in regions], + default=0 ) + if max_parts == 0: + print('Warning: No music lines found in page.') + return [] parts = [Part(translator) for _ in range(max_parts)] for region in regions: - for part, line in zip(parts, region.get_music_lines()): + for part, line in zip(parts, region.get_lines_of_category(self.categories)): part.add_textline(line) return parts From b40e479b9cfe295f963a1574783d0150ba19c799 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Wed, 25 Oct 2023 16:22:24 +0200 Subject: [PATCH 15/76] Remove music exporter option from `parse_folder.py` and make `export_music.py` a stand-alone script. --- pero_ocr/core/layout.py | 6 ------ pero_ocr/document_ocr/page_parser.py | 24 ------------------------ pero_ocr/music/export_music.py | 5 +++-- user_scripts/parse_folder.py | 12 ++---------- 4 files changed, 5 insertions(+), 42 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 63f94ba..506cab6 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -882,13 +882,7 @@ def get_regions_of_category(self, categories: str | list, reading_order=False): if not reading_order: return [region for region in self.regions if region.category in categories] - print('self.regions:') - for region in self.regions: - print(f'({region.category}) in ({categories}) => {region.category in categories}') - music_regions = [region for region in self.regions if region.category in categories] - print('music regions:') - print(music_regions) regions_with_bounding_boxes = {} for region in music_regions: diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 1913cd3..3878954 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -23,7 +23,6 @@ from pero_ocr.layout_engines.line_in_region_detector import detect_lines_in_region from pero_ocr.layout_engines.baseline_refiner import refine_baseline from pero_ocr.layout_engines import layout_helpers as helpers -from pero_ocr.music.export_music import ExportMusicPage logger = logging.getLogger(__name__) @@ -79,23 +78,6 @@ def page_decoder_factory(config, device, config_path=''): return PageDecoder(decoder, line_confidence_threshold=confidence_threshold, carry_h_over=carry_h_over) -def music_exporter_factory(config): - output_folder = config['PARSE_FOLDER']['OUTPUT_MUSIC_PATH'] - config = config['MUSIC_EXPORTER'] - export_midi = config.getboolean('EXPORT_MIDI') - export_musicxml = config.getboolean('EXPORT_MUSICXML') - translator_path = config.get('TRANSLATOR_PATH', None) - categories = json.loads(config.get('CATEGORIES', [])) - - return ExportMusicPage( - translator_path=translator_path, - export_midi=export_midi, - export_musicxml=export_musicxml, - output_folder=output_folder, - categories=categories - ) - - class MissingLogits(Exception): pass @@ -571,7 +553,6 @@ def __init__(self, config, device=None, config_path='', ): self.run_line_cropper = config['PAGE_PARSER'].getboolean('RUN_LINE_CROPPER', fallback=False) self.run_ocr = config['PAGE_PARSER'].getboolean('RUN_OCR', fallback=False) self.run_decoder = config['PAGE_PARSER'].getboolean('RUN_DECODER', fallback=False) - self.export_music = config['PAGE_PARSER'].getboolean('EXPORT_MUSIC', fallback=False) self.filter_confident_lines_threshold = config['PAGE_PARSER'].getfloat('FILTER_CONFIDENT_LINES_THRESHOLD', fallback=-1) @@ -579,7 +560,6 @@ def __init__(self, config, device=None, config_path='', ): self.line_cropper = None self.ocr = None self.decoder = None - self.music_exporter = None self.device = device if device is not None else get_default_device() @@ -594,8 +574,6 @@ def __init__(self, config, device=None, config_path='', ): self.ocr = ocr_factory(config, self.device, config_path=config_path) if self.run_decoder: self.decoder = page_decoder_factory(config, self.device, config_path=config_path) - if self.export_music: - self.music_exporter = music_exporter_factory(config) @staticmethod def compute_line_confidence(line, threshold=None): @@ -630,8 +608,6 @@ def process_page(self, image, page_layout): page_layout = self.ocr.process_page(image, page_layout) if self.run_decoder: page_layout = self.decoder.process_page(page_layout) - if self.export_music: - self.music_exporter.process_page(page_layout) self.update_confidences(page_layout) diff --git a/pero_ocr/music/export_music.py b/pero_ocr/music/export_music.py index 6f15acb..62c00e3 100644 --- a/pero_ocr/music/export_music.py +++ b/pero_ocr/music/export_music.py @@ -50,7 +50,7 @@ def parseargs(): help=("Enable exporting midi file to output_folder." "Exports whole file and individual lines with names corresponding to them TextLine IDs.")) parser.add_argument( - "-m", "--export-musicxml", action='store_true', + "-M", "--export-musicxml", action='store_true', help=("Enable exporting musicxml file to output_folder." "Exports whole file as one MusicXML.")) parser.add_argument( @@ -71,6 +71,7 @@ def main(): translator_path=args.translator_path, output_folder=args.output_folder, export_midi=args.export_midi, + export_musicxml=args.export_musicxml, verbose=args.verbose)() end = time.time() @@ -103,7 +104,7 @@ def __init__(self, input_xml_path: str = '', input_transcription_files: list[str self.export_musicxml = export_musicxml self.translator = Translator(file_name=self.translator_path) - self.categories = categories + self.categories = categories if categories else ['Notový zápis'] def __call__(self, page_layout = None) -> None: if self.input_transcription_files: diff --git a/user_scripts/parse_folder.py b/user_scripts/parse_folder.py index f81a9c6..a20ed82 100644 --- a/user_scripts/parse_folder.py +++ b/user_scripts/parse_folder.py @@ -37,8 +37,6 @@ def parse_arguments(): parser.add_argument('--output-logit-path', help='') parser.add_argument('--output-alto-path', help='') parser.add_argument('--output-transcriptions-file-path', help='') - parser.add_argument('--output-music-path', help='Where to export music files (MusicXML/midi). ' - 'More options in config.ini') parser.add_argument('--skipp-missing-xml', action='store_true', help='Skipp images which have missing xml.') parser.add_argument('--device', choices=["gpu", "cpu"], default="gpu") @@ -141,7 +139,7 @@ def __call__(self, page_layout: PageLayout, file_id): class Computator: def __init__(self, page_parser, input_image_path, input_xml_path, input_logit_path, output_render_path, - output_logit_path, output_alto_path, output_xml_path, output_line_path, output_music_path): + output_logit_path, output_alto_path, output_xml_path, output_line_path): self.page_parser = page_parser self.input_image_path = input_image_path self.input_xml_path = input_xml_path @@ -151,7 +149,6 @@ def __init__(self, page_parser, input_image_path, input_xml_path, input_logit_pa self.output_alto_path = output_alto_path self.output_xml_path = output_xml_path self.output_line_path = output_line_path - self.output_music_path = output_music_path def __call__(self, image_file_name, file_id, index, ids_count): print(f"Processing {file_id}") @@ -256,8 +253,6 @@ def main(): config['PARSE_FOLDER']['OUTPUT_LOGIT_PATH'] = args.output_logit_path if args.output_alto_path is not None: config['PARSE_FOLDER']['OUTPUT_ALTO_PATH'] = args.output_alto_path - if args.output_music_path is not None: - config['PARSE_FOLDER']['OUTPUT_MUSIC_PATH'] = args.output_music_path setup_logging(config['PARSE_FOLDER']) logger = logging.getLogger() @@ -275,7 +270,6 @@ def main(): output_xml_path = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_XML_PATH') output_logit_path = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_LOGIT_PATH') output_alto_path = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_ALTO_PATH') - output_music_path = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_MUSIC_PATH') if output_render_path is not None: create_dir_if_not_exists(output_render_path) @@ -287,8 +281,6 @@ def main(): create_dir_if_not_exists(output_logit_path) if output_alto_path is not None: create_dir_if_not_exists(output_alto_path) - if output_music_path is not None: - create_dir_if_not_exists(output_music_path) if input_logit_path is not None and input_xml_path is None: input_logit_path = None @@ -334,7 +326,7 @@ def main(): images_to_process = filtered_images_to_process computator = Computator(page_parser, input_image_path, input_xml_path, input_logit_path, output_render_path, - output_logit_path, output_alto_path, output_xml_path, output_line_path, output_music_path) + output_logit_path, output_alto_path, output_xml_path, output_line_path) t_start = time.time() results = [] From 7b93d162ac122cc8c2fbdac861e21a788af47b1b Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 27 Oct 2023 19:41:21 +0200 Subject: [PATCH 16/76] Add option to have more LineCroppers and ORC engines. Set every other text Layout engine to work only with 'text' lines. --- pero_ocr/core/layout.py | 14 +++-- pero_ocr/document_ocr/page_parser.py | 62 ++++++++++++++++------- pero_ocr/layout_engines/layout_helpers.py | 3 +- 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 506cab6..1404240 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -791,10 +791,16 @@ def render_to_image(self, image, thickness=2, circles=True, render_order=False): return image - def lines_iterator(self): - for r in self.regions: - for l in r.lines: - yield l + def lines_iterator(self, categories: list = None): + if not categories: + for r in self.regions: + for l in r.lines: + yield l + else: + for r in self.regions: + for l in r.lines: + if l.category in categories: + yield l def get_quality(self, x=None, y=None, width=None, height=None, power=6): bbox_confidences = [] diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 3878954..f15c33b 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -55,13 +55,19 @@ def layout_parser_factory(config, device, config_path='', order=1): return layout_parser -def line_cropper_factory(config, config_path=''): - config = config['LINE_CROPPER'] +def line_cropper_factory(config, config_path='', order=0): + if order == 0: + config = config['LINE_CROPPER'.format(order)] + else: + config = config['LINE_CROPPER_{}'.format(order)] return LineCropper(config, config_path=config_path) -def ocr_factory(config, device, config_path=''): - config = config['OCR'] +def ocr_factory(config, device, config_path='', order=0): + if order == 0: + config = config['OCR'.format(order)] + else: + config = config['OCR_{}'.format(order)] return PageOCR(config, device, config_path=config_path) @@ -105,13 +111,14 @@ def __init__(self, decoder, line_confidence_threshold=None, carry_h_over=False): self.lines_decoded = 0 self.seconds_decoding = 0.0 self.continue_lines = carry_h_over + self.categories = ['text'] self.last_h = None self.last_line = None def process_page(self, page_layout: PageLayout): self.last_h = None - for line in page_layout.lines_iterator(): + for line in page_layout.lines_iterator(self.categories): try: line.transcription = self.decode_line(line) except Exception: @@ -197,7 +204,8 @@ def process_page(self, img, page_layout: PageLayout): id='{}-l{:03d}'.format(region.id, line_num+1), baseline=baseline, polygon=textline, - heights=heights + heights=heights, + category='text' ) region.lines.append(new_textline) return page_layout @@ -212,6 +220,7 @@ def __init__(self, config, device, config_path=''): self.adjust_heights = config.getboolean('ADJUST_HEIGHTS') self.multi_orientation = config.getboolean('MULTI_ORIENTATION') self.adjust_baselines = config.getboolean('ADJUST_BASELINES') + self.categories = config.get('CATEGORIES', ['text']) use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") @@ -288,7 +297,7 @@ def process_page(self, img, page_layout: PageLayout): region = helpers.assign_lines_to_regions(pb_list, ph_list, pt_list, [region])[0] if self.adjust_heights: - for line in page_layout.lines_iterator(): + for line in page_layout.lines_iterator(self.categories): sample_points = helpers.resample_baselines( [line.baseline], num_points=40)[0] line.heights = self.engine.get_heights(maps, ds, sample_points) @@ -298,7 +307,7 @@ def process_page(self, img, page_layout: PageLayout): if self.adjust_baselines: crop_engine = cropper.EngineLineCropper( line_height=32, poly=0, scale=1) - for line in page_layout.lines_iterator(): + for line in page_layout.lines_iterator(self.categories): line.baseline = refine_baseline(line.baseline, line.heights, maps, ds, crop_engine) line.polygon = helpers.baseline_to_textline(line.baseline, line.heights) return page_layout @@ -402,6 +411,7 @@ def __init__(self, config, device, config_path): self.filter_incomplete_pages = config.getboolean('FILTER_INCOMPLETE_PAGES') self.filter_pages_with_short_lines = config.getboolean('FILTER_PAGES_WITH_SHORT_LINES') self.length_threshold = config.getint('LENGTH_THRESHOLD') + self.categories = config.get('CATEGORIES', ['text']) use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") @@ -423,7 +433,7 @@ def process_page(self, img, page_layout: PageLayout): region.lines = [line for line in region.lines if helpers.check_line_position(line.baseline, page_layout.page_size)] if self.filter_pages_with_short_lines: - b_list = [line.baseline for line in page_layout.lines_iterator()] + b_list = [line.baseline for line in page_layout.lines_iterator(self.categories)] if helpers.get_max_line_length(b_list) < self.length_threshold: page_layout.regions = [] @@ -475,11 +485,12 @@ def __init__(self, config, config_path=''): poly = config.getint('INTERP') line_scale = config.getfloat('LINE_SCALE') line_height = config.getint('LINE_HEIGHT') + self.categories = config.get('CATEGORIES', []) self.crop_engine = cropper.EngineLineCropper( line_height=line_height, poly=poly, scale=line_scale) def process_page(self, img, page_layout: PageLayout): - for line in page_layout.lines_iterator(): + for line in page_layout.lines_iterator(self.categories): try: line.crop = self.crop_engine.crop( img, line.baseline, line.heights) @@ -506,6 +517,7 @@ def __init__(self, config, device, config_path=''): use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") + self.categories = config.get('CATEGORIES', []) if 'METHOD' in config and config['METHOD'] == "pytorch_ocr-transformer": self.ocr_engine = TransformerEngineLineOCR(json_file, self.device) @@ -513,13 +525,13 @@ def __init__(self, config, device, config_path=''): self.ocr_engine = PytorchEngineLineOCR(json_file, self.device) def process_page(self, img, page_layout: PageLayout): - for line in page_layout.lines_iterator(): + for line in page_layout.lines_iterator(self.categories): if line.crop is None: raise Exception(f'Missing crop in line {line.id}.') - transcriptions, logits, logit_coords = self.ocr_engine.process_lines([line.crop for line in page_layout.lines_iterator()]) + transcriptions, logits, logit_coords = self.ocr_engine.process_lines([line.crop for line in page_layout.lines_iterator(self.categories)]) - for line, line_transcription, line_logits, line_logit_coords in zip(page_layout.lines_iterator(), transcriptions, logits, logit_coords): + for line, line_transcription, line_logits, line_logit_coords in zip(page_layout.lines_iterator(self.categories), transcriptions, logits, logit_coords): line.transcription = line_transcription line.logits = line_logits line.characters = self.ocr_engine.characters @@ -561,6 +573,7 @@ def __init__(self, config, device=None, config_path='', ): self.ocr = None self.decoder = None + self.MAX_ENGINES = 10 self.device = device if device is not None else get_default_device() if self.run_layout_parser: @@ -569,9 +582,19 @@ def __init__(self, config, device=None, config_path='', ): if config.has_section('LAYOUT_PARSER_{}'.format(i)): self.layout_parsers.append(layout_parser_factory(config, self.device, config_path=config_path, order=i)) if self.run_line_cropper: - self.line_cropper = line_cropper_factory(config, config_path=config_path) + self.line_croppers = {} + if config.has_section('LINE_CROPPER'): + self.line_croppers[0] = (line_cropper_factory(config, config_path=config_path)) + for i in range(1, self.MAX_ENGINES): + if config.has_section('LINE_CROPPER_{}'.format(i)): + self.line_croppers[i] = (line_cropper_factory(config, config_path=config_path, order=i)) if self.run_ocr: - self.ocr = ocr_factory(config, self.device, config_path=config_path) + self.ocrs = {} + if config.has_section('OCR'): + self.ocrs[0] = (ocr_factory(config, self.device, config_path=config_path)) + for i in range(1, self.MAX_ENGINES): + if config.has_section('OCR_{}'.format(i)): + self.ocrs[i] = (ocr_factory(config, self.device, config_path=config_path, order=i)) if self.run_decoder: self.decoder = page_decoder_factory(config, self.device, config_path=config_path) @@ -602,10 +625,11 @@ def process_page(self, image, page_layout): if self.run_layout_parser: for layout_parser in self.layout_parsers: page_layout = layout_parser.process_page(image, page_layout) - if self.run_line_cropper: - page_layout = self.line_cropper.process_page(image, page_layout) - if self.run_ocr: - page_layout = self.ocr.process_page(image, page_layout) + for i in range(0, self.MAX_ENGINES): + if self.run_line_cropper and i in self.line_croppers: + page_layout = self.line_croppers[i].process_page(image, page_layout) + if self.run_ocr and i in self.ocrs: + page_layout = self.ocrs[i].process_page(image, page_layout) if self.run_decoder: page_layout = self.decoder.process_page(page_layout) diff --git a/pero_ocr/layout_engines/layout_helpers.py b/pero_ocr/layout_engines/layout_helpers.py index 9c311e4..c26d0f1 100644 --- a/pero_ocr/layout_engines/layout_helpers.py +++ b/pero_ocr/layout_engines/layout_helpers.py @@ -68,7 +68,8 @@ def assign_lines_to_regions(baseline_list, heights_list, textline_list, regions) id='{}-l{:03d}'.format(region.id, line_id+1), baseline=baseline_intersection, polygon=textline_intersection, - heights=heights + heights=heights, + category='text' ) region.lines.append(new_textline) From 50418ddfe43c78ea2b5fa58b9035c02e25137e39 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 27 Oct 2023 20:50:51 +0200 Subject: [PATCH 17/76] Disable throwing error if no crop for line. Continue and ignore line. --- pero_ocr/document_ocr/page_parser.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index f15c33b..030d696 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -517,7 +517,7 @@ def __init__(self, config, device, config_path=''): use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") - self.categories = config.get('CATEGORIES', []) + self.categories = config.get('CATEGORIES', ['text']) if 'METHOD' in config and config['METHOD'] == "pytorch_ocr-transformer": self.ocr_engine = TransformerEngineLineOCR(json_file, self.device) @@ -525,13 +525,16 @@ def __init__(self, config, device, config_path=''): self.ocr_engine = PytorchEngineLineOCR(json_file, self.device) def process_page(self, img, page_layout: PageLayout): + lines_to_process = [] for line in page_layout.lines_iterator(self.categories): if line.crop is None: - raise Exception(f'Missing crop in line {line.id}.') + logger.error(f'Missing crop in line {line.id}.') + continue + lines_to_process.append(line) - transcriptions, logits, logit_coords = self.ocr_engine.process_lines([line.crop for line in page_layout.lines_iterator(self.categories)]) + transcriptions, logits, logit_coords = self.ocr_engine.process_lines([line.crop for line in lines_to_process]) - for line, line_transcription, line_logits, line_logit_coords in zip(page_layout.lines_iterator(self.categories), transcriptions, logits, logit_coords): + for line, line_transcription, line_logits, line_logit_coords in zip(lines_to_process, transcriptions, logits, logit_coords): line.transcription = line_transcription line.logits = line_logits line.characters = self.ocr_engine.characters From 885304630b2715a57ef7c9d9fd138360465acfeb Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 3 Nov 2023 17:23:01 +0100 Subject: [PATCH 18/76] Add PageLayout splitting enabling running multiple layout parsers each with its own setting and set of categories to work with. --- pero_ocr/core/layout.py | 4 ++ pero_ocr/document_ocr/page_parser.py | 20 +++--- pero_ocr/layout_engines/layout_helpers.py | 83 ++++++++++++++++++++++- 3 files changed, 98 insertions(+), 9 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 1404240..5a1414b 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -262,6 +262,10 @@ def __init__(self, id=None, page_size=(0, 0), file=None): if self.reading_order is not None and len(self.regions) > 0: self.sort_regions_by_reading_order() + def __str__(self): + return (f"PageLayout(id={self.id}, page_size={self.page_size}, regions={len(self.regions)}, " + f"lines={sum([len(region.lines) for region in self.regions])})") + def from_pagexml_string(self, pagexml_string): self.from_pagexml(BytesIO(pagexml_string)) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 030d696..4b5d7c7 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -240,13 +240,14 @@ def __init__(self, config, device, config_path=''): self.pool = Pool(1) def process_page(self, img, page_layout: PageLayout): + page_layout, page_layout_no_text = helpers.split_page_layout(page_layout) + if self.detect_regions or self.detect_lines: if self.detect_regions: - page_layout.delete_text_regions() + page_layout.regions = [] if self.detect_lines: for region in page_layout.regions: - if region.category == 'text': - region.lines = [] + region.lines = [] if self.multi_orientation: orientations = [0, 1, 3] @@ -310,6 +311,7 @@ def process_page(self, img, page_layout: PageLayout): for line in page_layout.lines_iterator(self.categories): line.baseline = refine_baseline(line.baseline, line.heights, maps, ds, crop_engine) line.polygon = helpers.baseline_to_textline(line.baseline, line.heights) + page_layout = helpers.merge_page_layouts(page_layout, page_layout_no_text) return page_layout @@ -326,9 +328,11 @@ def __init__(self, config, device, config_path=''): ) def process_page(self, img, page_layout: PageLayout): - page_layout.delete_yolo_regions() + page_layout_text, page_layout = helpers.split_page_layout(page_layout) + page_layout.regions = [] + result = self.engine.detect(img) - start_id = self.get_start_id(page_layout) + start_id = self.get_start_id([region.id for region in page_layout_text.regions]) boxes = result.boxes.data.cpu() for box_id, box in enumerate(boxes): @@ -356,16 +360,16 @@ def process_page(self, img, page_layout: PageLayout): page_layout.regions.append(region) page_layout = self.sort_regions_in_reading_order(page_layout, self.categories_for_transcription) - + page_layout = helpers.merge_page_layouts(page_layout_text, page_layout) return page_layout @staticmethod - def get_start_id(page_layout: PageLayout) -> int: + def get_start_id(used_ids: list) -> int: """Get int from which to start id naming for new regions. Expected region id is in format rXXX, where XXX is number. """ - used_region_ids = sorted([region.id for region in page_layout.regions]) + used_region_ids = sorted(used_ids) if not used_region_ids: return 0 diff --git a/pero_ocr/layout_engines/layout_helpers.py b/pero_ocr/layout_engines/layout_helpers.py index c26d0f1..f530501 100644 --- a/pero_ocr/layout_engines/layout_helpers.py +++ b/pero_ocr/layout_engines/layout_helpers.py @@ -1,6 +1,8 @@ import math import random import warnings +import logging +from copy import deepcopy import numpy as np import cv2 @@ -10,8 +12,9 @@ import shapely.geometry as sg from shapely.ops import cascaded_union, polygonize -from pero_ocr.core.layout import TextLine +from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine +logger = logging.getLogger(__name__) def check_line_position(baseline, page_size, margin=20, min_ratio=0.125): """Checks if line is short and very close to the page edge, which may indicate that the region actually belongs to @@ -405,3 +408,81 @@ def adjust_baselines_to_intensity(baselines, img, tolerance=5): baseline_pts[:,1] += best_offset new_baselines.append(resample_baselines([baseline_pts], num_points=len(baseline))[0]) return new_baselines + + +def insert_line_to_page_layout(page_layout: PageLayout, region: RegionLayout, line: TextLine) -> PageLayout: + """Insert line to page layout given region of origin""" + if len(page_layout.regions) == 0 or page_layout.regions[-1].id != region.id: + page_layout.regions.append(region) + page_layout.regions[-1].lines = [line] + else: + page_layout.regions[-1].lines.append(line) + return page_layout + + +def split_page_layout(page_layout: PageLayout) -> (PageLayout, PageLayout): + """Split page layout to text and non-text lines.""" + return split_page_layout_by_categories(page_layout, ['text']) + + +def split_page_layout_by_categories(page_layout: PageLayout, categories: list) -> (PageLayout, PageLayout): + """Split page_layout into two: one with textlines of given categories, the other with textlines of other categories. + + Example: + split_page_layout_by_categories(page_layout, ['text']) + IN: PageLayout(regions=[ + RegionLayout(id='r001', lines=[TextLine(id='r001-l001', category='text'), + TextLine(id='r001-l002', category='logo')])]) + OUT: PageLayout(regions=[ + RegionLayout(id='r001', lines=[TextLine(id='r001-l001', category='text')])]), + PageLayout(regions=[ + RegionLayout(id='r001', lines=[TextLine(id='r001-l002', category='logo')])]) + """ + if not categories: + page_layout_no_text = deepcopy(page_layout) + page_layout_no_text.regions = [] + return page_layout, page_layout_no_text + + regions = page_layout.regions + page_layout.regions = [] + + page_layout_text = page_layout + page_layout_no_text = deepcopy(page_layout) + + for region in regions: + for line in region.lines: + if line.category in categories: + page_layout_text = insert_line_to_page_layout(page_layout_text, region, line) + else: + page_layout_no_text = insert_line_to_page_layout(page_layout_no_text, region, line) + + return page_layout_text, page_layout_no_text + + +def merge_page_layouts(page_layout_text: PageLayout, page_layout_no_text: PageLayout) -> PageLayout: + """Merge two page_layouts into one by line. If same region ID, create new ID. + + Example: + IN: PageLayout(regions=[ + RegionLayout(id='r001', lines=[TextLine(id='r001-l001', category='text')])]), + PageLayout(regions=[ + RegionLayout(id='r001', lines=[TextLine(id='r001-l002', category='logo')])]) + OUT: PageLayout(regions=[ + RegionLayout(id='r001', lines=[TextLine(id='r001-l001', category='text')]), + RegionLayout(id='r002', lines=[TextLine(id='r002-l002', category='logo')])]) + """ + used_region_ids = set(region.id for region in page_layout_text.regions) + + id_offset = 0 + for region in page_layout_no_text.regions: + if region.id not in used_region_ids: + used_region_ids.add(region.id) + page_layout_text.regions.append(region) + else: + while 'r{:03d}'.format(id_offset) in used_region_ids: + id_offset += 1 + region.replace_id('r{:03d}'.format(id_offset)) + used_region_ids.add(region.id) + page_layout_text.regions.append(region) + + return page_layout_text From 5ce86d13e92949699b2075f27f1d58f57db8ac92 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 3 Nov 2023 17:26:59 +0100 Subject: [PATCH 19/76] Remove unused functions. --- pero_ocr/core/layout.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 5a1414b..53222ba 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -262,10 +262,6 @@ def __init__(self, id=None, page_size=(0, 0), file=None): if self.reading_order is not None and len(self.regions) > 0: self.sort_regions_by_reading_order() - def __str__(self): - return (f"PageLayout(id={self.id}, page_size={self.page_size}, regions={len(self.regions)}, " - f"lines={sum([len(region.lines) for region in self.regions])})") - def from_pagexml_string(self, pagexml_string): self.from_pagexml(BytesIO(pagexml_string)) @@ -877,14 +873,6 @@ def get_quality(self, x=None, y=None, width=None, height=None, power=6): else: return -1 - def delete_text_regions(self): - self.regions = [region for region in self.regions - if region.category not in ['text', None]] - - def delete_yolo_regions(self): - self.regions = [region for region in self.regions - if region.category in ['text', None]] - def get_regions_of_category(self, categories: str | list, reading_order=False): if isinstance(categories, str): category = [categories] From 3e2fcb862bc8cb0205f0137c2fa1bceb8639251e Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 1 Dec 2023 16:16:26 +0100 Subject: [PATCH 20/76] Add simple script to check if page layouts in two folders have same structure. --- user_scripts/compare_page_layouts.py | 147 +++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 user_scripts/compare_page_layouts.py diff --git a/user_scripts/compare_page_layouts.py b/user_scripts/compare_page_layouts.py new file mode 100644 index 0000000..f7bff66 --- /dev/null +++ b/user_scripts/compare_page_layouts.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import argparse +import sys +import logging +from enum import Enum + +from pero_ocr.core.layout import PageLayout + + +class Result(Enum): + UNPROCESSED = 0 + OK = 1 + REGION_COUNT_MISMATCH = 2 + LINE_COUNT_MISMATCH = 3 + HYP_MISSING = 4 + REF_MISSING = 5 + BOTH_MISSING = 6 + + +results_description = { + Result.UNPROCESSED: 'unprocessed', + Result.OK: 'are ok', + Result.REGION_COUNT_MISMATCH: 'have region count mismatch', + Result.LINE_COUNT_MISMATCH: 'have line count mismatch', + Result.HYP_MISSING: 'have hyp missing', + Result.REF_MISSING: 'have ref missing', + Result.BOTH_MISSING: 'have both missing', +} + + +def parse_arguments(): + parser = argparse.ArgumentParser( + description='Compare two folders with page layouts and output their structural and content differences.') + parser.add_argument('--print-all', action='store_true', help='Report info per page layout.') + parser.add_argument('--hyp', required=True, help='Folder with page xmls whose will be compared to reference') + parser.add_argument('--ref', required=True, help='Folder with reference page xmls.') + args = parser.parse_args() + return args + + +def read_page_xml(path): + try: + page_layout = PageLayout(file=path) + except OSError: + print(f'Warning: unable to load page xml "{path}"') + return None + return page_layout + + +def compare_page_layouts(hyp_fn, ref_fn, xml_file, results) -> dict: + hyp_page = read_page_xml(hyp_fn) + ref_page = read_page_xml(ref_fn) + if hyp_page is None and ref_page is None: + logging.debug(f'{xml_file}:\tboth missing') + results[xml_file] = Result.BOTH_MISSING + return results + if hyp_page is None and ref_page is not None: + results[xml_file] = Result.HYP_MISSING + logging.debug(f'{xml_file}:\thyp missing') + return results + if hyp_page is not None and ref_page is None: + results[xml_file] = Result.REF_MISSING + logging.debug(f'{xml_file}:\tref missing') + return results + + if len(hyp_page.regions) != len(ref_page.regions): + results[xml_file] = Result.REGION_COUNT_MISMATCH + logging.debug(f'{xml_file}:\tregions count mismatch ' + f'(hyp:{len(hyp_page.regions)} vs ref:{len(ref_page.regions)})') + return results + + hyp_lines = len([1 for _ in hyp_page.lines_iterator()]) + ref_lines = len([1 for _ in ref_page.lines_iterator()]) + if hyp_lines != ref_lines: + results[xml_file] = results.LINE_COUNT_MISMATCH + logging.debug(f'{xml_file}:\tlines count mismatch ' + f'(hyp:{hyp_lines} vs ref:{ref_lines})') + return results + + results[xml_file] = Result.OK + # compare content of lines somehow? (problem with aligning due to different ids generated by pero) + # Can be done like this: take all lines to a list, sort them alphabetically, then compare them one by one. + + return results + + +def group_results(results): + grouped_results = {result: 0 for result in set(results.values())} + + for _, result in results.items(): + grouped_results[result] += 1 + + return grouped_results + + +def print_results(results, print_all): + total_files = len(results) + results = group_results(results) + ok_count = results.get(Result.OK, 0) + results.pop(Result.OK) + + logging.debug(f'{total_files} files total:') + logging.debug(f'\t{ok_count} {results_description[Result.OK]}') + for result, desc in sorted(results_description.items(), key=lambda x: x[0].value): + result_count = results.get(result, 0) + if result_count > 0: + logging.debug(f'\t{result_count} {desc}') + + if not print_all: + if ok_count == total_files: + logging.info('All files are ok.') + else: + logging.info('Some files are not ok.') + + +def setup_logging(print_all): + logging.root.addHandler(logging.StreamHandler(sys.stdout)) # print to stdout + logging.getLogger().handlers[0].setFormatter(logging.Formatter('%(message)s')) # print only message + + if print_all: + logging.root.setLevel(logging.DEBUG) + else: + logging.root.setLevel(logging.INFO) + + +def main(): + args = parse_arguments() + setup_logging(args.print_all) + + xml_to_process = set(f for f in os.listdir(args.ref) if os.path.splitext(f)[1] == '.xml') + xml_to_process |= set(f for f in os.listdir(args.hyp) if os.path.splitext(f)[1] == '.xml') + + results = {xml_file: Result.UNPROCESSED for xml_file in xml_to_process} + + for xml_file in xml_to_process: + hyp_path = os.path.join(args.hyp, xml_file) + ref_path = os.path.join(args.ref, xml_file) + results = compare_page_layouts(hyp_path, ref_path, xml_file, results) + + print_results(results, args.print_all) + + +if __name__ == "__main__": + main() From 94bcae85b95e851f2c0d656c08d98874f50ee57c Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 1 Dec 2023 17:21:37 +0100 Subject: [PATCH 21/76] Disable double logging (stdout + stderr) --- user_scripts/compare_page_layouts.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/user_scripts/compare_page_layouts.py b/user_scripts/compare_page_layouts.py index f7bff66..10a7ca5 100644 --- a/user_scripts/compare_page_layouts.py +++ b/user_scripts/compare_page_layouts.py @@ -100,7 +100,7 @@ def print_results(results, print_all): total_files = len(results) results = group_results(results) ok_count = results.get(Result.OK, 0) - results.pop(Result.OK) + results.pop(Result.OK, None) logging.debug(f'{total_files} files total:') logging.debug(f'\t{ok_count} {results_description[Result.OK]}') @@ -117,7 +117,6 @@ def print_results(results, print_all): def setup_logging(print_all): - logging.root.addHandler(logging.StreamHandler(sys.stdout)) # print to stdout logging.getLogger().handlers[0].setFormatter(logging.Formatter('%(message)s')) # print only message if print_all: From d688bc04eed2de9f392983e4b3abeccef30aaf27 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 1 Dec 2023 18:24:43 +0100 Subject: [PATCH 22/76] Refactor page xml export + import. --- README.md | 2 +- pero_ocr/core/layout.py | 282 ++++++++++++------------ pero_ocr/document_ocr/pdf_production.py | 2 +- user_scripts/merge_ocr_results.py | 2 +- user_scripts/parse_folder.py | 2 +- 5 files changed, 150 insertions(+), 140 deletions(-) diff --git a/README.md b/README.md index 045f1cf..ceaae17 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ page_layout = PageLayout(id=input_image_path, # Process the image by the OCR pipeline page_layout = page_parser.process_page(image, page_layout) -page_layout.to_pagexml('output_page.xml') # Save results as Page XML. +page_layout.to_page_xml('output_page.xml') # Save results as Page XML. page_layout.to_altoxml('output_ALTO.xml') # Save results as ALTO XML. # Render detected text regions and text lines into the image and diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 9c1d95c..359abb0 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -73,6 +73,108 @@ def get_full_logprobs(self, zero_logit_value: int = -80): dense_logits = self.get_dense_logits(zero_logit_value) return log_softmax(dense_logits) + def to_page_xml(self, region_element: ET.SubElement, fallback_id: int, validate_id: bool = False): + text_line = ET.SubElement(region_element, "TextLine") + text_line.set("id", export_id(self.id, validate_id)) + if self.index is not None: + text_line.set("index", f'{self.index:d}') + else: + text_line.set("index", f'{fallback_id:d}') + if self.heights is not None: + custom = { + "heights": list(np.round(self.heights, decimals=1)), + "category": self.category + } + text_line.set("custom", json.dumps(custom)) + + coords = ET.SubElement(text_line, "Coords") + + if self.polygon is not None: + points = ["{},{}".format(int(np.round(coord[0])), int(np.round(coord[1]))) for coord in + self.polygon] + points = " ".join(points) + coords.set("points", points) + + if self.baseline is not None: + baseline_element = ET.SubElement(text_line, "Baseline") + points = ["{},{}".format(int(np.round(coord[0])), int(np.round(coord[1]))) for coord in + self.baseline] + points = " ".join(points) + baseline_element.set("points", points) + + if self.transcription is not None: + text_element = ET.SubElement(text_line, "TextEquiv") + if self.transcription_confidence is not None: + text_element.set("conf", f"{self.transcription_confidence:.3f}") + text_element = ET.SubElement(text_element, "Unicode") + text_element.text = self.transcription + + @classmethod + def from_page_xml(cls, line_element: ET.SubElement, schema, fallback_index: int): + new_textline = cls(id=line_element.attrib['id']) + if 'custom' in line_element.attrib: + new_textline.from_page_xml_parse_custom(line_element.attrib['custom']) + + if 'index' in line_element.attrib: + try: + new_textline.index = int(line_element.attrib['index']) + except ValueError: + pass + + if new_textline.index is None: + new_textline.index = fallback_index + + baseline = line_element.find(schema + 'Baseline') + if baseline is not None: + new_textline.baseline = get_coords_form_page_xml(baseline, schema) + else: + logger.warning(f'Warning: Baseline is missing in TextLine. ' + f'Skipping this line during import. Line ID: {new_textline.id}') + return None + + textline = line_element.find(schema + 'Coords') + if textline is not None: + new_textline.polygon = get_coords_form_page_xml(textline, schema) + + if not new_textline.heights: + guess_line_heights_from_polygon(new_textline, use_center=False, n=len(new_textline.baseline)) + + transcription = line_element.find(schema + 'TextEquiv') + if transcription is not None: + t_unicode = transcription.find(schema + 'Unicode').text + if t_unicode is None: + t_unicode = '' + new_textline.transcription = t_unicode + conf = transcription.get('conf', None) + new_textline.transcription_confidence = float(conf) if conf is not None else None + return new_textline + + def from_page_xml_parse_custom(self, custom_str): + try: + custom = json.loads(custom_str) + self.category = custom.get('category', None) + self.heights = custom.get('heights', None) + except json.decoder.JSONDecodeError: + if 'heights_v2' in custom_str: + for word in custom_str.split(): + if 'heights_v2' in word: + self.heights = json.loads(word.split(":")[1]) + else: + if re.findall("heights", custom_str): + heights = re.findall(r"\d+", custom_str) + heights_array = np.asarray([float(x) for x in heights]) + if heights_array.shape[0] == 4: + heights = np.zeros(2, dtype=np.float32) + heights[0] = heights_array[0] + heights[1] = heights_array[2] + elif heights_array.shape[0] == 3: + heights = np.zeros(2, dtype=np.float32) + heights[0] = heights_array[1] + heights[1] = heights_array[2] - heights_array[0] + else: + heights = heights_array + self.heights = heights.tolist() + class RegionLayout(object): def __init__(self, id: str, @@ -105,6 +207,10 @@ def to_page_xml(self, page_element: ET.SubElement, validate_id: bool = False): text_element = ET.SubElement(region_element, "TextEquiv") text_element = ET.SubElement(text_element, "Unicode") text_element.text = self.transcription + + for i, line in enumerate(self.lines): + line.to_page_xml(region_element, fallback_id=i, validate_id=validate_id) + return region_element def get_lines_of_category(self, categories: str | list): @@ -120,7 +226,7 @@ def replace_id(self, new_id): self.id = new_id def get_polygon_bounding_box(self) -> tuple[int, int, int, int]: - """Get bounding box of region polygon that includes all polygon points. + """Get bounding box of region polygon which includes all polygon points. :return: tuple[int, int, int, int]: (x_min, y_min, x_max, y_max) """ x_min = min(self.polygon[:, 0]) @@ -130,6 +236,35 @@ def get_polygon_bounding_box(self) -> tuple[int, int, int, int]: return x_min, y_min, x_max, y_max + @classmethod + def from_page_xml(cls, region_element: ET.SubElement, schema): + coords_element = region_element.find(schema + 'Coords') + region_coords = get_coords_form_page_xml(coords_element, schema) + + region_type = None + if "type" in region_element.attrib: + region_type = region_element.attrib["type"] + + category = None + if "custom" in region_element.attrib: + custom = json.loads(region_element.attrib["custom"]) + category = custom.get('category', None) + + layout_region = cls(region_element.attrib['id'], region_coords, region_type, category=category) + + transcription = region_element.find(schema + 'TextEquiv') + if transcription is not None: + layout_region.transcription = transcription.find(schema + 'Unicode').text + if layout_region.transcription is None: + layout_region.transcription = '' + + for i, line in enumerate(region_element.iter(schema + 'TextLine')): + new_textline = TextLine.from_page_xml(line, schema, fallback_index=i) + if new_textline is not None: + layout_region.lines.append(new_textline) + + return layout_region + def get_coords_form_page_xml(coords_element, schema): if 'points' in coords_element.attrib: @@ -143,29 +278,6 @@ def get_coords_form_page_xml(coords_element, schema): return coords -def get_region_from_page_xml(region_element, schema): - coords_element = region_element.find(schema + 'Coords') - region_coords = get_coords_form_page_xml(coords_element, schema) - - region_type = None - if "type" in region_element.attrib: - region_type = region_element.attrib["type"] - - category = None - if "custom" in region_element.attrib: - custom = json.loads(region_element.attrib["custom"]) - category = custom.get('category', None) - - layout_region = RegionLayout(region_element.attrib['id'], region_coords, region_type, category=category) - - transcription = region_element.find(schema + 'TextEquiv') - if transcription is not None: - layout_region.transcription = transcription.find(schema + 'Unicode').text - if layout_region.transcription is None: - layout_region.transcription = '' - return layout_region - - def guess_line_heights_from_polygon(text_line: TextLine, use_center: bool = False, n: int = 10, interpolate=False): ''' Guess line heights for line if missing (e.g. import from Transkribus). @@ -275,15 +387,15 @@ def __init__(self, id: str = None, page_size: list[int, int] = (0, 0), file: str self.reading_order = None if file is not None: - self.from_pagexml(file) + self.from_page_xml(file) if self.reading_order is not None and len(self.regions) > 0: self.sort_regions_by_reading_order() - def from_pagexml_string(self, pagexml_string: str): - self.from_pagexml(BytesIO(pagexml_string.encode('utf-8'))) + def from_page_xml_string(self, page_xml_string: str): + self.from_page_xml(BytesIO(page_xml_string.encode('utf-8'))) - def from_pagexml(self, file: Union[str, BytesIO]): + def from_page_xml(self, file: Union[str, BytesIO]): page_tree = ET.parse(file) schema = element_schema(page_tree.getroot()) @@ -294,76 +406,10 @@ def from_pagexml(self, file: Union[str, BytesIO]): self.reading_order = get_reading_order(page, schema) for region in page_tree.iter(schema + 'TextRegion'): - region_layout = get_region_from_page_xml(region, schema) - - for line_i, line in enumerate(region.iter(schema + 'TextLine')): - new_textline = TextLine(id=line.attrib['id']) - if 'custom' in line.attrib: - self.from_pagexml_parse_line_custom(new_textline, line.attrib['custom']) - - if 'index' in line.attrib: - try: - new_textline.index = int(line.attrib['index']) - except ValueError: - pass - - if new_textline.index is None: - new_textline.index = line_i - - baseline = line.find(schema + 'Baseline') - if baseline is not None: - new_textline.baseline = get_coords_form_page_xml(baseline, schema) - else: - logger.warning(f'Warning: Baseline is missing in TextLine. ' - f'Skipping this line during import. Line ID: {new_textline.id} Page ID: {self.id}') - continue - - textline = line.find(schema + 'Coords') - if textline is not None: - new_textline.polygon = get_coords_form_page_xml(textline, schema) - - if not new_textline.heights: - guess_line_heights_from_polygon(new_textline, use_center=False, n=len(new_textline.baseline)) - - transcription = line.find(schema + 'TextEquiv') - if transcription is not None: - t_unicode = transcription.find(schema + 'Unicode').text - if t_unicode is None: - t_unicode = '' - new_textline.transcription = t_unicode - conf = transcription.get('conf', None) - new_textline.transcription_confidence = float(conf) if conf is not None else None - region_layout.lines.append(new_textline) - + region_layout = RegionLayout.from_page_xml(region, schema) self.regions.append(region_layout) - def from_pagexml_parse_line_custom(self, textline: TextLine, custom_str): - try: - custom = json.loads(custom_str) - textline.category = custom.get('category', None) - textline.heights = custom.get('heights', None) - except json.decoder.JSONDecodeError: - if 'heights_v2' in custom_str: - for word in custom_str.split(): - if 'heights_v2' in word: - textline.heights = json.loads(word.split(":")[1]) - else: - if re.findall("heights", custom_str): - heights = re.findall("\d+", custom_str) - heights_array = np.asarray([float(x) for x in heights]) - if heights_array.shape[0] == 4: - heights = np.zeros(2, dtype=np.float32) - heights[0] = heights_array[0] - heights[1] = heights_array[2] - elif heights_array.shape[0] == 3: - heights = np.zeros(2, dtype=np.float32) - heights[0] = heights_array[1] - heights[1] = heights_array[2] - heights_array[0] - else: - heights = heights_array - textline.heights = heights.tolist() - - def to_pagexml_string(self, creator: str = 'Pero OCR', validate_id: bool = False, + def to_page_xml_string(self, creator: str = 'Pero OCR', validate_id: bool = False, version: PAGEVersion = PAGEVersion.PAGE_2019_07_15): if version == PAGEVersion.PAGE_2019_07_15: attr_qname = ET.QName("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation") @@ -398,49 +444,13 @@ def to_pagexml_string(self, creator: str = 'Pero OCR', validate_id: bool = False self.reading_order_to_page_xml(page) for region_layout in self.regions: - text_region = region_layout.to_page_xml(page, validate_id=validate_id) - - for i, line in enumerate(region_layout.lines): - text_line = ET.SubElement(text_region, "TextLine") - text_line.set("id", export_id(line.id, validate_id)) - if line.index is not None: - text_line.set("index", f'{line.index:d}') - else: - text_line.set("index", f'{i:d}') - if line.heights is not None: - custom = { - "heights": list(np.round(line.heights, decimals=1)), - "category": line.category - } - text_line.set("custom", json.dumps(custom)) - - coords = ET.SubElement(text_line, "Coords") - - if line.polygon is not None: - points = ["{},{}".format(int(np.round(coord[0])), int(np.round(coord[1]))) for coord in - line.polygon] - points = " ".join(points) - coords.set("points", points) - - if line.baseline is not None: - baseline_element = ET.SubElement(text_line, "Baseline") - points = ["{},{}".format(int(np.round(coord[0])), int(np.round(coord[1]))) for coord in - line.baseline] - points = " ".join(points) - baseline_element.set("points", points) - - if line.transcription is not None: - text_element = ET.SubElement(text_line, "TextEquiv") - if line.transcription_confidence is not None: - text_element.set("conf", f"{line.transcription_confidence:.3f}") - text_element = ET.SubElement(text_element, "Unicode") - text_element.text = line.transcription + region_layout.to_page_xml(page, validate_id=validate_id) return ET.tostring(root, pretty_print=True, encoding="utf-8", xml_declaration=True).decode("utf-8") - def to_pagexml(self, file_name: str, creator: str = 'Pero OCR', + def to_page_xml(self, file_name: str, creator: str = 'Pero OCR', validate_id: bool = False, version: PAGEVersion = PAGEVersion.PAGE_2019_07_15): - xml_string = self.to_pagexml_string(version=version, creator=creator, validate_id=validate_id) + xml_string = self.to_page_xml_string(version=version, creator=creator, validate_id=validate_id) with open(file_name, 'w', encoding='utf-8') as out_f: out_f.write(xml_string) @@ -537,7 +547,7 @@ def to_altoxml_string(self, ocr_processing_element: ET.SubElement = None, page_u logprobs = line.get_full_logprobs()[line.logit_coords[0]:line.logit_coords[1]] aligned_letters = align_text(-logprobs, np.array(label), blank_idx) except (ValueError, IndexError, TypeError) as e: - logger.warning(f'Error: Alto export, unable to align line {line.id} due to exception {e}.') + logger.warning(f'Error: Alto export, unable to align line {line.id} due to exception: {e}.') line.transcription_confidence = 0 average_word_width = (text_line_hpos + text_line_width) / len(line.transcription.split()) for w, word in enumerate(line.transcription.split()): diff --git a/pero_ocr/document_ocr/pdf_production.py b/pero_ocr/document_ocr/pdf_production.py index 47356ad..c7f3596 100644 --- a/pero_ocr/document_ocr/pdf_production.py +++ b/pero_ocr/document_ocr/pdf_production.py @@ -44,7 +44,7 @@ def get_xml_processor(self, xml_path): def parse_page_xml(xml_path): layout = PageLayout() - layout.from_pagexml(xml_path) + layout.from_page_xml(xml_path) h, w = layout.page_size diff --git a/user_scripts/merge_ocr_results.py b/user_scripts/merge_ocr_results.py index d8e5776..ecd05d4 100644 --- a/user_scripts/merge_ocr_results.py +++ b/user_scripts/merge_ocr_results.py @@ -124,7 +124,7 @@ def main(): if arabic_helper.is_arabic_line(line.transcription): line.transcription = arabic_helper.label_form_to_string(line.transcription) - merged_layout.to_pagexml(os.path.join(args.output_path, xml_file_name)) + merged_layout.to_page_xml(os.path.join(args.output_path, xml_file_name)) merged_layout.save_logits(os.path.join(args.output_path, os.path.splitext(xml_file_name)[0] + '.logits')) diff --git a/user_scripts/parse_folder.py b/user_scripts/parse_folder.py index a20ed82..c0b8afa 100644 --- a/user_scripts/parse_folder.py +++ b/user_scripts/parse_folder.py @@ -173,7 +173,7 @@ def __call__(self, image_file_name, file_id, index, ids_count): page_layout = self.page_parser.process_page(image, page_layout) if self.output_xml_path is not None: - page_layout.to_pagexml( + page_layout.to_page_xml( os.path.join(self.output_xml_path, file_id + '.xml')) if self.output_render_path is not None: From 65eacb5eb6cad36c12b46d3a4e78194f45ad773c Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 1 Dec 2023 20:00:00 +0100 Subject: [PATCH 23/76] Refactor alto xml export. --- README.md | 2 +- pero_ocr/core/layout.py | 412 +++++++++++++++++++---------------- user_scripts/parse_folder.py | 2 +- 3 files changed, 223 insertions(+), 193 deletions(-) diff --git a/README.md b/README.md index ceaae17..a2ec1ec 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ page_layout = PageLayout(id=input_image_path, page_layout = page_parser.process_page(image, page_layout) page_layout.to_page_xml('output_page.xml') # Save results as Page XML. -page_layout.to_altoxml('output_ALTO.xml') # Save results as ALTO XML. +page_layout.to_alto_xml('output_ALTO.xml') # Save results as ALTO XML. # Render detected text regions and text lines into the image and # save it into a file. diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 359abb0..66d1ef9 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -175,6 +175,150 @@ def from_page_xml_parse_custom(self, custom_str): heights = heights_array self.heights = heights.tolist() + def to_alto_xml(self, text_block, arabic_helper, min_line_confidence): + arabic_line = False + if arabic_helper.is_arabic_line(self.transcription): + arabic_line = True + text_line = ET.SubElement(text_block, "TextLine") + text_line_baseline = int(np.average(np.array(self.baseline)[:, 1])) + text_line.set("BASELINE", str(text_line_baseline)) + + text_line_height, text_line_width, text_line_vpos, text_line_hpos = get_hwvh(self.polygon) + + text_line.set("VPOS", str(int(text_line_vpos))) + text_line.set("HPOS", str(int(text_line_hpos))) + text_line.set("HEIGHT", str(int(text_line_height))) + text_line.set("WIDTH", str(int(text_line_width))) + + try: + chars = [i for i in range(len(self.characters))] + char_to_num = dict(zip(self.characters, chars)) + + blank_idx = self.logits.shape[1] - 1 + + label = [] + for item in self.transcription: + if item in char_to_num.keys(): + if char_to_num[item] >= blank_idx: + label.append(0) + else: + label.append(char_to_num[item]) + else: + label.append(0) + + logits = self.get_dense_logits()[self.logit_coords[0]:self.logit_coords[1]] + logprobs = self.get_full_logprobs()[self.logit_coords[0]:self.logit_coords[1]] + aligned_letters = align_text(-logprobs, np.array(label), blank_idx) + except (ValueError, IndexError, TypeError) as e: + logger.warning(f'Error: Alto export, unable to align line {self.id} due to exception: {e}.') + self.transcription_confidence = 0 + average_word_width = (text_line_hpos + text_line_width) / len(self.transcription.split()) + for w, word in enumerate(self.transcription.split()): + string = ET.SubElement(text_line, "String") + string.set("CONTENT", word) + + string.set("HEIGHT", str(int(text_line_height))) + string.set("WIDTH", str(int(average_word_width))) + string.set("VPOS", str(int(text_line_vpos))) + string.set("HPOS", str(int(text_line_hpos + (w * average_word_width)))) + else: + crop_engine = EngineLineCropper(poly=2) + line_coords = crop_engine.get_crop_inputs(self.baseline, self.heights, 16) + space_idxs = [pos for pos, char in enumerate(self.transcription) if char == ' '] + + words = [] + space_idxs = [-1] + space_idxs + [len(aligned_letters)] + for i in range(len(space_idxs[1:])): + if space_idxs[i] != space_idxs[i + 1] - 1: + words.append([aligned_letters[space_idxs[i] + 1], aligned_letters[space_idxs[i + 1] - 1]]) + splitted_transcription = self.transcription.split() + lm_const = line_coords.shape[1] / logits.shape[0] + letter_counter = 0 + confidences = get_line_confidence(self, np.array(label), aligned_letters, logprobs) + # if self.transcription_confidence is None: + self.transcription_confidence = np.quantile(confidences, .50) + for w, word in enumerate(words): + extension = 2 + while line_coords.size > 0 and extension < 40: + all_x = line_coords[:, + max(0, int((words[w][0] - extension) * lm_const)):int((words[w][1] + extension) * lm_const), + 0] + all_y = line_coords[:, + max(0, int((words[w][0] - extension) * lm_const)):int((words[w][1] + extension) * lm_const), + 1] + + if all_x.size == 0 or all_y.size == 0: + extension += 1 + else: + break + + if line_coords.size == 0 or all_x.size == 0 or all_y.size == 0: + all_x = self.baseline[:, 0] + all_y = np.concatenate( + [self.baseline[:, 1] - self.heights[0], self.baseline[:, 1] + self.heights[1]]) + + word_confidence = None + if self.transcription_confidence == 1: + word_confidence = 1 + else: + if confidences.size != 0: + word_confidence = np.quantile( + confidences[letter_counter:letter_counter + len(splitted_transcription[w])], .50) + + string = ET.SubElement(text_line, "String") + + if arabic_line: + string.set("CONTENT", arabic_helper.label_form_to_string(splitted_transcription[w])) + else: + string.set("CONTENT", splitted_transcription[w]) + + string.set("HEIGHT", str(int((np.max(all_y) - np.min(all_y))))) + string.set("WIDTH", str(int((np.max(all_x) - np.min(all_x))))) + string.set("VPOS", str(int(np.min(all_y)))) + string.set("HPOS", str(int(np.min(all_x)))) + + if word_confidence is not None: + string.set("WC", str(round(word_confidence, 2))) + + if w != (len(self.transcription.split()) - 1): + space = ET.SubElement(text_line, "SP") + + space.set("WIDTH", str(4)) + space.set("VPOS", str(int(np.min(all_y)))) + space.set("HPOS", str(int(np.max(all_x)))) + letter_counter += len(splitted_transcription[w]) + 1 + if self.transcription_confidence is not None: + if self.transcription_confidence < min_line_confidence: + text_block.remove(text_line) + + @classmethod + def from_alto_xml(cls, line: ET.SubElement, schema): + new_textline = cls(baseline=np.asarray( + [[int(line.attrib['HPOS']), int(line.attrib['BASELINE'])], + [int(line.attrib['HPOS']) + int(line.attrib['WIDTH']), int(line.attrib['BASELINE'])]])) + polygon = [] + new_textline.heights = np.asarray([ + int(line.attrib['HEIGHT']) + int(line.attrib['VPOS']) - int(line.attrib['BASELINE']), + int(line.attrib['BASELINE']) - int(line.attrib['VPOS'])]) + polygon.append([int(line.attrib['HPOS']), int(line.attrib['VPOS'])]) + polygon.append( + [int(line.attrib['HPOS']) + int(line.attrib['WIDTH']), int(line.attrib['VPOS'])]) + polygon.append([int(line.attrib['HPOS']) + int(line.attrib['WIDTH']), + int(line.attrib['VPOS']) + int(line.attrib['HEIGHT'])]) + polygon.append( + [int(line.attrib['HPOS']), int(line.attrib['VPOS']) + int(line.attrib['HEIGHT'])]) + new_textline.polygon = np.asarray(polygon) + word = '' + start = True + for text in line.iter(schema + 'String'): + if start: + start = False + word = word + text.get('CONTENT') + else: + word = word + " " + text.get('CONTENT') + new_textline.transcription = word + return new_textline + class RegionLayout(object): def __init__(self, id: str, @@ -188,6 +332,29 @@ def __init__(self, id: str, self.lines: list[TextLine] = [] self.transcription = None + def get_lines_of_category(self, categories: str | list): + if isinstance(categories, str): + categories = [categories] + + return [line for line in self.lines if line.category in categories] + + def replace_id(self, new_id): + """Replace region ID and all IDs in TextLines which has region ID inside them.""" + for line in self.lines: + line.id = line.id.replace(self.id, new_id) + self.id = new_id + + def get_polygon_bounding_box(self) -> tuple[int, int, int, int]: + """Get bounding box of region polygon which includes all polygon points. + :return: tuple[int, int, int, int]: (x_min, y_min, x_max, y_max) + """ + x_min = min(self.polygon[:, 0]) + x_max = max(self.polygon[:, 0]) + y_min = min(self.polygon[:, 1]) + y_max = max(self.polygon[:, 1]) + + return x_min, y_min, x_max, y_max + def to_page_xml(self, page_element: ET.SubElement, validate_id: bool = False): region_element = ET.SubElement(page_element, "TextRegion") coords = ET.SubElement(region_element, "Coords") @@ -213,29 +380,6 @@ def to_page_xml(self, page_element: ET.SubElement, validate_id: bool = False): return region_element - def get_lines_of_category(self, categories: str | list): - if isinstance(categories, str): - categories = [categories] - - return [line for line in self.lines if line.category in categories] - - def replace_id(self, new_id): - """Replace region ID and all IDs in TextLines which has region ID inside them.""" - for line in self.lines: - line.id = line.id.replace(self.id, new_id) - self.id = new_id - - def get_polygon_bounding_box(self) -> tuple[int, int, int, int]: - """Get bounding box of region polygon which includes all polygon points. - :return: tuple[int, int, int, int]: (x_min, y_min, x_max, y_max) - """ - x_min = min(self.polygon[:, 0]) - x_max = max(self.polygon[:, 0]) - y_min = min(self.polygon[:, 1]) - y_max = max(self.polygon[:, 1]) - - return x_min, y_min, x_max, y_max - @classmethod def from_page_xml(cls, region_element: ET.SubElement, schema): coords_element = region_element.find(schema + 'Coords') @@ -265,6 +409,49 @@ def from_page_xml(cls, region_element: ET.SubElement, schema): return layout_region + def to_alto_xml(self, print_space, arabic_helper, min_line_confidence, print_space_coords: (int, int, int, int) + ) -> (int, int, int, int): + print_space_height, print_space_width, print_space_vpos, print_space_hpos = print_space_coords + + text_block = ET.SubElement(print_space, "TextBlock") + text_block.set("ID", 'block_{}'.format(self.id)) + + text_block_height, text_block_width, text_block_vpos, text_block_hpos = get_hwvh(self.polygon) + text_block.set("HEIGHT", str(int(text_block_height))) + text_block.set("WIDTH", str(int(text_block_width))) + text_block.set("VPOS", str(int(text_block_vpos))) + text_block.set("HPOS", str(int(text_block_hpos))) + + print_space_height = max([print_space_vpos + print_space_height, text_block_vpos + text_block_height]) + print_space_width = max([print_space_hpos + print_space_width, text_block_hpos + text_block_width]) + print_space_vpos = min([print_space_vpos, text_block_vpos]) + print_space_hpos = min([print_space_hpos, text_block_hpos]) + print_space_height = print_space_height - print_space_vpos + print_space_width = print_space_width - print_space_hpos + + for line in self.lines: + if not line.transcription or line.transcription.strip() == "": + continue + line.to_alto_xml(text_block, arabic_helper, min_line_confidence) + return print_space_height, print_space_width, print_space_vpos, print_space_hpos + + @classmethod + def from_alto_xml(cls, text_block: ET.SubElement, schema): + region_coords = list() + region_coords.append([int(text_block.get('HPOS')), int(text_block.get('VPOS'))]) + region_coords.append([int(text_block.get('HPOS')) + int(text_block.get('WIDTH')), int(text_block.get('VPOS'))]) + region_coords.append([int(text_block.get('HPOS')) + int(text_block.get('WIDTH')), + int(text_block.get('VPOS')) + int(text_block.get('HEIGHT'))]) + region_coords.append([int(text_block.get('HPOS')), int(text_block.get('VPOS')) + int(text_block.get('HEIGHT'))]) + + region_layout = cls(text_block.attrib['ID'], np.asarray(region_coords).tolist()) + + for line in text_block.iter(schema + 'TextLine'): + new_textline = TextLine.from_alto_xml(line, schema) + region_layout.lines.append(new_textline) + + return region_layout + def get_coords_form_page_xml(coords_element, schema): if 'points' in coords_element.attrib: @@ -454,7 +641,7 @@ def to_page_xml(self, file_name: str, creator: str = 'Pero OCR', with open(file_name, 'w', encoding='utf-8') as out_f: out_f.write(xml_string) - def to_altoxml_string(self, ocr_processing_element: ET.SubElement = None, page_uuid: str = None, min_line_confidence: float = 0): + def to_alto_xml_string(self, ocr_processing_element: ET.SubElement = None, page_uuid: str = None, min_line_confidence: float = 0): arabic_helper = ArabicHelper() NSMAP = {"xlink": 'http://www.w3.org/1999/xlink', "xsi": 'http://www.w3.org/2001/XMLSchema-instance'} @@ -492,135 +679,13 @@ def to_altoxml_string(self, ocr_processing_element: ET.SubElement = None, page_u print_space_width = 0 print_space_vpos = self.page_size[0] print_space_hpos = self.page_size[1] + print_space_coords = (print_space_height, print_space_width, print_space_vpos, print_space_hpos) - for b, block in enumerate(self.regions): - text_block = ET.SubElement(print_space, "TextBlock") - text_block.set("ID", 'block_{}' .format(block.id)) - - text_block_height, text_block_width, text_block_vpos, text_block_hpos = get_hwvh(block.polygon) - text_block.set("HEIGHT", str(int(text_block_height))) - text_block.set("WIDTH", str(int(text_block_width))) - text_block.set("VPOS", str(int(text_block_vpos))) - text_block.set("HPOS", str(int(text_block_hpos))) - - print_space_height = max([print_space_vpos + print_space_height, text_block_vpos + text_block_height]) - print_space_width = max([print_space_hpos + print_space_width, text_block_hpos + text_block_width]) - print_space_vpos = min([print_space_vpos, text_block_vpos]) - print_space_hpos = min([print_space_hpos, text_block_hpos]) - print_space_height = print_space_height - print_space_vpos - print_space_width = print_space_width - print_space_hpos - - for l, line in enumerate(block.lines): - if not line.transcription or line.transcription.strip() == "": - continue - arabic_line = False - if arabic_helper.is_arabic_line(line.transcription): - arabic_line = True - text_line = ET.SubElement(text_block, "TextLine") - text_line_baseline = int(np.average(np.array(line.baseline)[:, 1])) - text_line.set("BASELINE", str(text_line_baseline)) - - text_line_height, text_line_width, text_line_vpos, text_line_hpos = get_hwvh(line.polygon) - - text_line.set("VPOS", str(int(text_line_vpos))) - text_line.set("HPOS", str(int(text_line_hpos))) - text_line.set("HEIGHT", str(int(text_line_height))) - text_line.set("WIDTH", str(int(text_line_width))) - - try: - chars = [i for i in range(len(line.characters))] - char_to_num = dict(zip(line.characters, chars)) - - blank_idx = line.logits.shape[1] - 1 + for block in self.regions: + print_space_coords = block.to_alto_xml(print_space, arabic_helper, min_line_confidence, print_space_coords) - label = [] - for item in line.transcription: - if item in char_to_num.keys(): - if char_to_num[item] >= blank_idx: - label.append(0) - else: - label.append(char_to_num[item]) - else: - label.append(0) - - logits = line.get_dense_logits()[line.logit_coords[0]:line.logit_coords[1]] - logprobs = line.get_full_logprobs()[line.logit_coords[0]:line.logit_coords[1]] - aligned_letters = align_text(-logprobs, np.array(label), blank_idx) - except (ValueError, IndexError, TypeError) as e: - logger.warning(f'Error: Alto export, unable to align line {line.id} due to exception: {e}.') - line.transcription_confidence = 0 - average_word_width = (text_line_hpos + text_line_width) / len(line.transcription.split()) - for w, word in enumerate(line.transcription.split()): - string = ET.SubElement(text_line, "String") - string.set("CONTENT", word) - - string.set("HEIGHT", str(int(text_line_height))) - string.set("WIDTH", str(int(average_word_width))) - string.set("VPOS", str(int(text_line_vpos))) - string.set("HPOS", str(int(text_line_hpos + (w * average_word_width)))) - else: - crop_engine = EngineLineCropper(poly=2) - line_coords = crop_engine.get_crop_inputs(line.baseline, line.heights, 16) - space_idxs = [pos for pos, char in enumerate(line.transcription) if char == ' '] + print_space_height, print_space_width, print_space_vpos, print_space_hpos = print_space_coords - words = [] - space_idxs = [-1] + space_idxs + [len(aligned_letters)] - for i in range(len(space_idxs[1:])): - if space_idxs[i] != space_idxs[i+1]-1: - words.append([aligned_letters[space_idxs[i]+1], aligned_letters[space_idxs[i+1]-1]]) - splitted_transcription = line.transcription.split() - lm_const = line_coords.shape[1] / logits.shape[0] - letter_counter = 0 - confidences = get_line_confidence(line, np.array(label), aligned_letters, logprobs) - #if line.transcription_confidence is None: - line.transcription_confidence = np.quantile(confidences, .50) - for w, word in enumerate(words): - extension = 2 - while line_coords.size > 0 and extension < 40: - all_x = line_coords[:, max(0, int((words[w][0]-extension) * lm_const)):int((words[w][1]+extension) * lm_const), 0] - all_y = line_coords[:, max(0, int((words[w][0]-extension) * lm_const)):int((words[w][1]+extension) * lm_const), 1] - - if all_x.size == 0 or all_y.size == 0: - extension += 1 - else: - break - - if line_coords.size == 0 or all_x.size == 0 or all_y.size == 0: - all_x = line.baseline[:, 0] - all_y = np.concatenate([line.baseline[:, 1] - line.heights[0], line.baseline[:, 1] + line.heights[1]]) - - word_confidence = None - if line.transcription_confidence == 1: - word_confidence = 1 - else: - if confidences.size != 0: - word_confidence = np.quantile(confidences[letter_counter:letter_counter+len(splitted_transcription[w])], .50) - - string = ET.SubElement(text_line, "String") - - if arabic_line: - string.set("CONTENT", arabic_helper.label_form_to_string(splitted_transcription[w])) - else: - string.set("CONTENT", splitted_transcription[w]) - - string.set("HEIGHT", str(int((np.max(all_y) - np.min(all_y))))) - string.set("WIDTH", str(int((np.max(all_x) - np.min(all_x))))) - string.set("VPOS", str(int(np.min(all_y)))) - string.set("HPOS", str(int(np.min(all_x)))) - - if word_confidence is not None: - string.set("WC", str(round(word_confidence, 2))) - - if w != (len(line.transcription.split())-1): - space = ET.SubElement(text_line, "SP") - - space.set("WIDTH", str(4)) - space.set("VPOS", str(int(np.min(all_y)))) - space.set("HPOS", str(int(np.max(all_x)))) - letter_counter += len(splitted_transcription[w])+1 - if line.transcription_confidence is not None: - if line.transcription_confidence < min_line_confidence: - text_block.remove(text_line) top_margin.set("HEIGHT", "{}" .format(int(print_space_vpos))) top_margin.set("WIDTH", "{}" .format(int(self.page_size[1]))) top_margin.set("VPOS", "0") @@ -648,15 +713,15 @@ def to_altoxml_string(self, ocr_processing_element: ET.SubElement = None, page_u return ET.tostring(root, pretty_print=True, encoding="utf-8", xml_declaration=True).decode("utf-8") - def to_altoxml(self, file_name: str, ocr_processing_element: ET.SubElement = None, page_uuid: str = None): - alto_string = self.to_altoxml_string(ocr_processing_element=ocr_processing_element, page_uuid=page_uuid) + def to_alto_xml(self, file_name: str, ocr_processing_element: ET.SubElement = None, page_uuid: str = None): + alto_string = self.to_alto_xml_string(ocr_processing_element=ocr_processing_element, page_uuid=page_uuid) with open(file_name, 'w', encoding='utf-8') as out_f: out_f.write(alto_string) - def from_altoxml_string(self, altoxml_string: str): - self.from_altoxml(BytesIO(altoxml_string.encode('utf-8'))) + def from_alto_xml_string(self, alto_xml_string: str): + self.from_alto_xml(BytesIO(alto_xml_string.encode('utf-8'))) - def from_altoxml(self, file: Union[str, BytesIO]): + def from_alto_xml(self, file: Union[str, BytesIO]): page_tree = ET.parse(file) schema = element_schema(page_tree.getroot()) root = page_tree.getroot() @@ -669,42 +734,7 @@ def from_altoxml(self, file: Union[str, BytesIO]): print_space = page.findall(schema + 'PrintSpace')[0] for region in print_space.iter(schema + 'TextBlock'): - region_coords = list() - region_coords.append([int(region.get('HPOS')), int(region.get('VPOS'))]) - region_coords.append([int(region.get('HPOS')) + int(region.get('WIDTH')), int(region.get('VPOS'))]) - region_coords.append([int(region.get('HPOS')) + int(region.get('WIDTH')), - int(region.get('VPOS')) + int(region.get('HEIGHT'))]) - region_coords.append([int(region.get('HPOS')), int(region.get('VPOS')) + int(region.get('HEIGHT'))]) - - region_layout = RegionLayout(region.attrib['ID'], np.asarray(region_coords).tolist()) - - for line in region.iter(schema + 'TextLine'): - new_textline = TextLine(baseline=np.asarray( - [[int(line.attrib['HPOS']), int(line.attrib['BASELINE'])], - [int(line.attrib['HPOS']) + int(line.attrib['WIDTH']), int(line.attrib['BASELINE'])]])) - polygon = [] - new_textline.heights = np.asarray([ - int(line.attrib['HEIGHT']) + int(line.attrib['VPOS']) - int(line.attrib['BASELINE']), - int(line.attrib['BASELINE']) - int(line.attrib['VPOS'])]) - polygon.append([int(line.attrib['HPOS']), int(line.attrib['VPOS'])]) - polygon.append( - [int(line.attrib['HPOS']) + int(line.attrib['WIDTH']), int(line.attrib['VPOS'])]) - polygon.append([int(line.attrib['HPOS']) + int(line.attrib['WIDTH']), - int(line.attrib['VPOS']) + int(line.attrib['HEIGHT'])]) - polygon.append( - [int(line.attrib['HPOS']), int(line.attrib['VPOS']) + int(line.attrib['HEIGHT'])]) - new_textline.polygon = np.asarray(polygon) - word = '' - start = True - for text in line.iter(schema + 'String'): - if start: - start = False - word = word + text.get('CONTENT') - else: - word = word + " " + text.get('CONTENT') - new_textline.transcription = word - region_layout.lines.append(new_textline) - + region_layout = RegionLayout.from_alto_xml(region, schema) self.regions.append(region_layout) def sort_regions_by_reading_order(self): diff --git a/user_scripts/parse_folder.py b/user_scripts/parse_folder.py index c0b8afa..7f1b68b 100644 --- a/user_scripts/parse_folder.py +++ b/user_scripts/parse_folder.py @@ -184,7 +184,7 @@ def __call__(self, image_file_name, file_id, index, ids_count): page_layout.save_logits(os.path.join(self.output_logit_path, file_id + '.logits')) if self.output_alto_path is not None: - page_layout.to_altoxml(os.path.join(self.output_alto_path, file_id + '.xml')) + page_layout.to_alto_xml(os.path.join(self.output_alto_path, file_id + '.xml')) if self.output_line_path is not None and page_layout is not None: if 'lmdb' in self.output_line_path: From 3354a5bb73e38c1448a29082fc5c9d4581ec1c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ki=C5=A1=C5=A1?= Date: Thu, 14 Dec 2023 11:01:27 +0100 Subject: [PATCH 24/76] Minor updates --- pero_ocr/core/layout.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 66d1ef9..705b0c7 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -858,15 +858,10 @@ def render_to_image(self, image, thickness: int = 2, circles: bool = True, rende return image def lines_iterator(self, categories: list = None): - if not categories: - for region in self.regions: - for line in region.lines: + for region in self.regions: + for line in region.lines: + if not categories or line.category in categories: yield line - else: - for region in self.regions: - for line in region.lines: - if line.category in categories: - yield line def get_quality(self, x: int = None, y: int = None, width: int = None, height: int = None, power: int = 6): bbox_confidences = [] @@ -946,10 +941,10 @@ def get_regions_of_category(self, categories: str | list, reading_order=False): if not reading_order: return [region for region in self.regions if region.category in categories] - music_regions = [region for region in self.regions if region.category in categories] + category_regions = [region for region in self.regions if region.category in categories] regions_with_bounding_boxes = {} - for region in music_regions: + for region in category_regions: regions_with_bounding_boxes[region] = { 'id': region.id, 'bounding_box': region.get_polygon_bounding_box(), From 59dd3306b8e7725b148b675d33026b631463e204 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 14 Dec 2023 12:07:22 +0100 Subject: [PATCH 25/76] Unify most of method names: page_xml to pagexml and alto_xml to altoxml. --- README.md | 4 +- pero_ocr/core/layout.py | 74 ++++++++++++------------- pero_ocr/document_ocr/pdf_production.py | 2 +- user_scripts/compare_page_layouts.py | 6 +- user_scripts/merge_ocr_results.py | 2 +- user_scripts/parse_folder.py | 4 +- 6 files changed, 46 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index a2ec1ec..045f1cf 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ page_layout = PageLayout(id=input_image_path, # Process the image by the OCR pipeline page_layout = page_parser.process_page(image, page_layout) -page_layout.to_page_xml('output_page.xml') # Save results as Page XML. -page_layout.to_alto_xml('output_ALTO.xml') # Save results as ALTO XML. +page_layout.to_pagexml('output_page.xml') # Save results as Page XML. +page_layout.to_altoxml('output_ALTO.xml') # Save results as ALTO XML. # Render detected text regions and text lines into the image and # save it into a file. diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 705b0c7..a95b835 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -73,7 +73,7 @@ def get_full_logprobs(self, zero_logit_value: int = -80): dense_logits = self.get_dense_logits(zero_logit_value) return log_softmax(dense_logits) - def to_page_xml(self, region_element: ET.SubElement, fallback_id: int, validate_id: bool = False): + def to_pagexml(self, region_element: ET.SubElement, fallback_id: int, validate_id: bool = False): text_line = ET.SubElement(region_element, "TextLine") text_line.set("id", export_id(self.id, validate_id)) if self.index is not None: @@ -110,10 +110,10 @@ def to_page_xml(self, region_element: ET.SubElement, fallback_id: int, validate_ text_element.text = self.transcription @classmethod - def from_page_xml(cls, line_element: ET.SubElement, schema, fallback_index: int): + def from_pagexml(cls, line_element: ET.SubElement, schema, fallback_index: int): new_textline = cls(id=line_element.attrib['id']) if 'custom' in line_element.attrib: - new_textline.from_page_xml_parse_custom(line_element.attrib['custom']) + new_textline.from_pagexml_parse_custom(line_element.attrib['custom']) if 'index' in line_element.attrib: try: @@ -126,7 +126,7 @@ def from_page_xml(cls, line_element: ET.SubElement, schema, fallback_index: int) baseline = line_element.find(schema + 'Baseline') if baseline is not None: - new_textline.baseline = get_coords_form_page_xml(baseline, schema) + new_textline.baseline = get_coords_form_pagexml(baseline, schema) else: logger.warning(f'Warning: Baseline is missing in TextLine. ' f'Skipping this line during import. Line ID: {new_textline.id}') @@ -134,7 +134,7 @@ def from_page_xml(cls, line_element: ET.SubElement, schema, fallback_index: int) textline = line_element.find(schema + 'Coords') if textline is not None: - new_textline.polygon = get_coords_form_page_xml(textline, schema) + new_textline.polygon = get_coords_form_pagexml(textline, schema) if not new_textline.heights: guess_line_heights_from_polygon(new_textline, use_center=False, n=len(new_textline.baseline)) @@ -149,7 +149,7 @@ def from_page_xml(cls, line_element: ET.SubElement, schema, fallback_index: int) new_textline.transcription_confidence = float(conf) if conf is not None else None return new_textline - def from_page_xml_parse_custom(self, custom_str): + def from_pagexml_parse_custom(self, custom_str): try: custom = json.loads(custom_str) self.category = custom.get('category', None) @@ -175,7 +175,7 @@ def from_page_xml_parse_custom(self, custom_str): heights = heights_array self.heights = heights.tolist() - def to_alto_xml(self, text_block, arabic_helper, min_line_confidence): + def to_altoxml(self, text_block, arabic_helper, min_line_confidence): arabic_line = False if arabic_helper.is_arabic_line(self.transcription): arabic_line = True @@ -292,7 +292,7 @@ def to_alto_xml(self, text_block, arabic_helper, min_line_confidence): text_block.remove(text_line) @classmethod - def from_alto_xml(cls, line: ET.SubElement, schema): + def from_altoxml(cls, line: ET.SubElement, schema): new_textline = cls(baseline=np.asarray( [[int(line.attrib['HPOS']), int(line.attrib['BASELINE'])], [int(line.attrib['HPOS']) + int(line.attrib['WIDTH']), int(line.attrib['BASELINE'])]])) @@ -355,7 +355,7 @@ def get_polygon_bounding_box(self) -> tuple[int, int, int, int]: return x_min, y_min, x_max, y_max - def to_page_xml(self, page_element: ET.SubElement, validate_id: bool = False): + def to_pagexml(self, page_element: ET.SubElement, validate_id: bool = False): region_element = ET.SubElement(page_element, "TextRegion") coords = ET.SubElement(region_element, "Coords") region_element.set("id", export_id(self.id, validate_id)) @@ -376,14 +376,14 @@ def to_page_xml(self, page_element: ET.SubElement, validate_id: bool = False): text_element.text = self.transcription for i, line in enumerate(self.lines): - line.to_page_xml(region_element, fallback_id=i, validate_id=validate_id) + line.to_pagexml(region_element, fallback_id=i, validate_id=validate_id) return region_element @classmethod - def from_page_xml(cls, region_element: ET.SubElement, schema): + def from_pagexml(cls, region_element: ET.SubElement, schema): coords_element = region_element.find(schema + 'Coords') - region_coords = get_coords_form_page_xml(coords_element, schema) + region_coords = get_coords_form_pagexml(coords_element, schema) region_type = None if "type" in region_element.attrib: @@ -403,13 +403,13 @@ def from_page_xml(cls, region_element: ET.SubElement, schema): layout_region.transcription = '' for i, line in enumerate(region_element.iter(schema + 'TextLine')): - new_textline = TextLine.from_page_xml(line, schema, fallback_index=i) + new_textline = TextLine.from_pagexml(line, schema, fallback_index=i) if new_textline is not None: layout_region.lines.append(new_textline) return layout_region - def to_alto_xml(self, print_space, arabic_helper, min_line_confidence, print_space_coords: (int, int, int, int) + def to_altoxml(self, print_space, arabic_helper, min_line_confidence, print_space_coords: (int, int, int, int) ) -> (int, int, int, int): print_space_height, print_space_width, print_space_vpos, print_space_hpos = print_space_coords @@ -432,11 +432,11 @@ def to_alto_xml(self, print_space, arabic_helper, min_line_confidence, print_spa for line in self.lines: if not line.transcription or line.transcription.strip() == "": continue - line.to_alto_xml(text_block, arabic_helper, min_line_confidence) + line.to_altoxml(text_block, arabic_helper, min_line_confidence) return print_space_height, print_space_width, print_space_vpos, print_space_hpos @classmethod - def from_alto_xml(cls, text_block: ET.SubElement, schema): + def from_altoxml(cls, text_block: ET.SubElement, schema): region_coords = list() region_coords.append([int(text_block.get('HPOS')), int(text_block.get('VPOS'))]) region_coords.append([int(text_block.get('HPOS')) + int(text_block.get('WIDTH')), int(text_block.get('VPOS'))]) @@ -447,13 +447,13 @@ def from_alto_xml(cls, text_block: ET.SubElement, schema): region_layout = cls(text_block.attrib['ID'], np.asarray(region_coords).tolist()) for line in text_block.iter(schema + 'TextLine'): - new_textline = TextLine.from_alto_xml(line, schema) + new_textline = TextLine.from_altoxml(line, schema) region_layout.lines.append(new_textline) return region_layout -def get_coords_form_page_xml(coords_element, schema): +def get_coords_form_pagexml(coords_element, schema): if 'points' in coords_element.attrib: coords = points_string_to_array(coords_element.attrib['points']) else: @@ -574,15 +574,15 @@ def __init__(self, id: str = None, page_size: list[int, int] = (0, 0), file: str self.reading_order = None if file is not None: - self.from_page_xml(file) + self.from_pagexml(file) if self.reading_order is not None and len(self.regions) > 0: self.sort_regions_by_reading_order() - def from_page_xml_string(self, page_xml_string: str): - self.from_page_xml(BytesIO(page_xml_string.encode('utf-8'))) + def from_pagexml_string(self, pagexml_string: str): + self.from_pagexml(BytesIO(pagexml_string.encode('utf-8'))) - def from_page_xml(self, file: Union[str, BytesIO]): + def from_pagexml(self, file: Union[str, BytesIO]): page_tree = ET.parse(file) schema = element_schema(page_tree.getroot()) @@ -593,10 +593,10 @@ def from_page_xml(self, file: Union[str, BytesIO]): self.reading_order = get_reading_order(page, schema) for region in page_tree.iter(schema + 'TextRegion'): - region_layout = RegionLayout.from_page_xml(region, schema) + region_layout = RegionLayout.from_pagexml(region, schema) self.regions.append(region_layout) - def to_page_xml_string(self, creator: str = 'Pero OCR', validate_id: bool = False, + def to_pagexml_string(self, creator: str = 'Pero OCR', validate_id: bool = False, version: PAGEVersion = PAGEVersion.PAGE_2019_07_15): if version == PAGEVersion.PAGE_2019_07_15: attr_qname = ET.QName("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation") @@ -628,20 +628,20 @@ def to_page_xml_string(self, creator: str = 'Pero OCR', validate_id: bool = Fals if self.reading_order is not None: self.sort_regions_by_reading_order() - self.reading_order_to_page_xml(page) + self.reading_order_to_pagexml(page) for region_layout in self.regions: - region_layout.to_page_xml(page, validate_id=validate_id) + region_layout.to_pagexml(page, validate_id=validate_id) return ET.tostring(root, pretty_print=True, encoding="utf-8", xml_declaration=True).decode("utf-8") - def to_page_xml(self, file_name: str, creator: str = 'Pero OCR', + def to_pagexml(self, file_name: str, creator: str = 'Pero OCR', validate_id: bool = False, version: PAGEVersion = PAGEVersion.PAGE_2019_07_15): - xml_string = self.to_page_xml_string(version=version, creator=creator, validate_id=validate_id) + xml_string = self.to_pagexml_string(version=version, creator=creator, validate_id=validate_id) with open(file_name, 'w', encoding='utf-8') as out_f: out_f.write(xml_string) - def to_alto_xml_string(self, ocr_processing_element: ET.SubElement = None, page_uuid: str = None, min_line_confidence: float = 0): + def to_altoxml_string(self, ocr_processing_element: ET.SubElement = None, page_uuid: str = None, min_line_confidence: float = 0): arabic_helper = ArabicHelper() NSMAP = {"xlink": 'http://www.w3.org/1999/xlink', "xsi": 'http://www.w3.org/2001/XMLSchema-instance'} @@ -682,7 +682,7 @@ def to_alto_xml_string(self, ocr_processing_element: ET.SubElement = None, page_ print_space_coords = (print_space_height, print_space_width, print_space_vpos, print_space_hpos) for block in self.regions: - print_space_coords = block.to_alto_xml(print_space, arabic_helper, min_line_confidence, print_space_coords) + print_space_coords = block.to_altoxml(print_space, arabic_helper, min_line_confidence, print_space_coords) print_space_height, print_space_width, print_space_vpos, print_space_hpos = print_space_coords @@ -713,15 +713,15 @@ def to_alto_xml_string(self, ocr_processing_element: ET.SubElement = None, page_ return ET.tostring(root, pretty_print=True, encoding="utf-8", xml_declaration=True).decode("utf-8") - def to_alto_xml(self, file_name: str, ocr_processing_element: ET.SubElement = None, page_uuid: str = None): - alto_string = self.to_alto_xml_string(ocr_processing_element=ocr_processing_element, page_uuid=page_uuid) + def to_altoxml(self, file_name: str, ocr_processing_element: ET.SubElement = None, page_uuid: str = None): + alto_string = self.to_altoxml_string(ocr_processing_element=ocr_processing_element, page_uuid=page_uuid) with open(file_name, 'w', encoding='utf-8') as out_f: out_f.write(alto_string) - def from_alto_xml_string(self, alto_xml_string: str): - self.from_alto_xml(BytesIO(alto_xml_string.encode('utf-8'))) + def from_altoxml_string(self, altoxml_string: str): + self.from_altoxml(BytesIO(altoxml_string.encode('utf-8'))) - def from_alto_xml(self, file: Union[str, BytesIO]): + def from_altoxml(self, file: Union[str, BytesIO]): page_tree = ET.parse(file) schema = element_schema(page_tree.getroot()) root = page_tree.getroot() @@ -734,13 +734,13 @@ def from_alto_xml(self, file: Union[str, BytesIO]): print_space = page.findall(schema + 'PrintSpace')[0] for region in print_space.iter(schema + 'TextBlock'): - region_layout = RegionLayout.from_alto_xml(region, schema) + region_layout = RegionLayout.from_altoxml(region, schema) self.regions.append(region_layout) def sort_regions_by_reading_order(self): self.regions = sorted(self.regions, key=lambda k: self.reading_order[k] if k in self.reading_order else float("inf")) - def reading_order_to_page_xml(self, page_element: ET.SubElement): + def reading_order_to_pagexml(self, page_element: ET.SubElement): reading_order_element = ET.SubElement(page_element, "ReadingOrder") ordered_group_element = ET.SubElement(reading_order_element, "OrderedGroup") ordered_group_element.set("id", "reading_order") diff --git a/pero_ocr/document_ocr/pdf_production.py b/pero_ocr/document_ocr/pdf_production.py index c7f3596..47356ad 100644 --- a/pero_ocr/document_ocr/pdf_production.py +++ b/pero_ocr/document_ocr/pdf_production.py @@ -44,7 +44,7 @@ def get_xml_processor(self, xml_path): def parse_page_xml(xml_path): layout = PageLayout() - layout.from_page_xml(xml_path) + layout.from_pagexml(xml_path) h, w = layout.page_size diff --git a/user_scripts/compare_page_layouts.py b/user_scripts/compare_page_layouts.py index 10a7ca5..67a84ec 100644 --- a/user_scripts/compare_page_layouts.py +++ b/user_scripts/compare_page_layouts.py @@ -41,7 +41,7 @@ def parse_arguments(): return args -def read_page_xml(path): +def read_pagexml(path): try: page_layout = PageLayout(file=path) except OSError: @@ -51,8 +51,8 @@ def read_page_xml(path): def compare_page_layouts(hyp_fn, ref_fn, xml_file, results) -> dict: - hyp_page = read_page_xml(hyp_fn) - ref_page = read_page_xml(ref_fn) + hyp_page = read_pagexml(hyp_fn) + ref_page = read_pagexml(ref_fn) if hyp_page is None and ref_page is None: logging.debug(f'{xml_file}:\tboth missing') results[xml_file] = Result.BOTH_MISSING diff --git a/user_scripts/merge_ocr_results.py b/user_scripts/merge_ocr_results.py index ecd05d4..d8e5776 100644 --- a/user_scripts/merge_ocr_results.py +++ b/user_scripts/merge_ocr_results.py @@ -124,7 +124,7 @@ def main(): if arabic_helper.is_arabic_line(line.transcription): line.transcription = arabic_helper.label_form_to_string(line.transcription) - merged_layout.to_page_xml(os.path.join(args.output_path, xml_file_name)) + merged_layout.to_pagexml(os.path.join(args.output_path, xml_file_name)) merged_layout.save_logits(os.path.join(args.output_path, os.path.splitext(xml_file_name)[0] + '.logits')) diff --git a/user_scripts/parse_folder.py b/user_scripts/parse_folder.py index 7f1b68b..a20ed82 100644 --- a/user_scripts/parse_folder.py +++ b/user_scripts/parse_folder.py @@ -173,7 +173,7 @@ def __call__(self, image_file_name, file_id, index, ids_count): page_layout = self.page_parser.process_page(image, page_layout) if self.output_xml_path is not None: - page_layout.to_page_xml( + page_layout.to_pagexml( os.path.join(self.output_xml_path, file_id + '.xml')) if self.output_render_path is not None: @@ -184,7 +184,7 @@ def __call__(self, image_file_name, file_id, index, ids_count): page_layout.save_logits(os.path.join(self.output_logit_path, file_id + '.logits')) if self.output_alto_path is not None: - page_layout.to_alto_xml(os.path.join(self.output_alto_path, file_id + '.xml')) + page_layout.to_altoxml(os.path.join(self.output_alto_path, file_id + '.xml')) if self.output_line_path is not None and page_layout is not None: if 'lmdb' in self.output_line_path: From 3c3322e0e03a21dbc41108ce6923d97f41bfba61 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 14 Dec 2023 12:26:25 +0100 Subject: [PATCH 26/76] Unify most of the method names: page_xml to pagexml and alto_xml to altoxml. part 2. --- pero_ocr/core/layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index a95b835..747b5ee 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -936,7 +936,7 @@ def get_quality(self, x: int = None, y: int = None, width: int = None, height: i def get_regions_of_category(self, categories: str | list, reading_order=False): if isinstance(categories, str): - category = [categories] + categories = [categories] if not reading_order: return [region for region in self.regions if region.category in categories] From 27a82eb5023844f5651cadc72f134a395c828e1d Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 14 Dec 2023 18:43:03 +0100 Subject: [PATCH 27/76] New config section parsing and other changes after code review. - in layout_helpers.py add searching for a region by ID - in page_parser.py change log error (missing crop) back to raise exception - in page_parser.py change naming convention for config section names to {section_name}_\d+. All following examples are possible (with warnings) for OCR section: OCR, OCR_0, OCR_0_asdf... --- pero_ocr/document_ocr/page_parser.py | 88 +++++++++++++---------- pero_ocr/layout_engines/layout_helpers.py | 29 +++++--- 2 files changed, 70 insertions(+), 47 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 4b5d7c7..cd9c3a6 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -28,8 +28,7 @@ logger = logging.getLogger(__name__) -def layout_parser_factory(config, device, config_path='', order=1): - config = config['LAYOUT_PARSER_{}'.format(order)] +def layout_parser_factory(config, device, config_path=''): if config['METHOD'] == 'REGION_WHOLE_PAGE': layout_parser = WholePageRegion(config, config_path=config_path) elif config['METHOD'] == 'REGION_SIMPLE_THRESHOLD': @@ -55,19 +54,11 @@ def layout_parser_factory(config, device, config_path='', order=1): return layout_parser -def line_cropper_factory(config, config_path='', order=0): - if order == 0: - config = config['LINE_CROPPER'.format(order)] - else: - config = config['LINE_CROPPER_{}'.format(order)] +def line_cropper_factory(config, config_path='', device=None): return LineCropper(config, config_path=config_path) -def ocr_factory(config, device, config_path='', order=0): - if order == 0: - config = config['OCR'.format(order)] - else: - config = config['OCR_{}'.format(order)] +def ocr_factory(config, device, config_path=''): return PageOCR(config, device, config_path=config_path) @@ -532,8 +523,7 @@ def process_page(self, img, page_layout: PageLayout): lines_to_process = [] for line in page_layout.lines_iterator(self.categories): if line.crop is None: - logger.error(f'Missing crop in line {line.id}.') - continue + raise Exception(f'Missing crop in line {line.id}.') lines_to_process.append(line) transcriptions, logits, logit_coords = self.ocr_engine.process_lines([line.crop for line in lines_to_process]) @@ -580,28 +570,16 @@ def __init__(self, config, device=None, config_path='', ): self.ocr = None self.decoder = None - self.MAX_ENGINES = 10 self.device = device if device is not None else get_default_device() + self.line_croppers = {} + self.ocrs = {} if self.run_layout_parser: - self.layout_parsers = [] - for i in range(1, 10): - if config.has_section('LAYOUT_PARSER_{}'.format(i)): - self.layout_parsers.append(layout_parser_factory(config, self.device, config_path=config_path, order=i)) + self.layout_parsers = self.init_config_sections(config, config_path, 'LAYOUT_PARSER', layout_parser_factory) if self.run_line_cropper: - self.line_croppers = {} - if config.has_section('LINE_CROPPER'): - self.line_croppers[0] = (line_cropper_factory(config, config_path=config_path)) - for i in range(1, self.MAX_ENGINES): - if config.has_section('LINE_CROPPER_{}'.format(i)): - self.line_croppers[i] = (line_cropper_factory(config, config_path=config_path, order=i)) + self.line_croppers = self.init_config_sections(config, config_path, 'LINE_CROPPER', line_cropper_factory) if self.run_ocr: - self.ocrs = {} - if config.has_section('OCR'): - self.ocrs[0] = (ocr_factory(config, self.device, config_path=config_path)) - for i in range(1, self.MAX_ENGINES): - if config.has_section('OCR_{}'.format(i)): - self.ocrs[i] = (ocr_factory(config, self.device, config_path=config_path, order=i)) + self.ocrs = self.init_config_sections(config, config_path, 'OCR', ocr_factory) if self.run_decoder: self.decoder = page_decoder_factory(config, self.device, config_path=config_path) @@ -630,13 +608,15 @@ def filter_confident_lines(self, page_layout): def process_page(self, image, page_layout): if self.run_layout_parser: - for layout_parser in self.layout_parsers: + for _, layout_parser in sorted(self.layout_parsers.items()): page_layout = layout_parser.process_page(image, page_layout) - for i in range(0, self.MAX_ENGINES): - if self.run_line_cropper and i in self.line_croppers: - page_layout = self.line_croppers[i].process_page(image, page_layout) - if self.run_ocr and i in self.ocrs: - page_layout = self.ocrs[i].process_page(image, page_layout) + + merged_keys = set(self.line_croppers.keys()) | set(self.ocrs.keys()) + for key in sorted(merged_keys): + if self.run_line_cropper and key in self.line_croppers: + page_layout = self.line_croppers[key].process_page(image, page_layout) + if self.run_ocr and key in self.ocrs: + page_layout = self.ocrs[key].process_page(image, page_layout) if self.run_decoder: page_layout = self.decoder.process_page(page_layout) @@ -646,3 +626,37 @@ def process_page(self, image, page_layout): page_layout = self.filter_confident_lines(page_layout) return page_layout + + def init_config_sections(self, config, config_path, section_name, section_factory) -> dict: + """Return dict of sections. + + Naming convention: section_name_[0-9]+. + Also accepts other names, but logges warning. + e.g. for OCR section: OCR, OCR_0, OCR_42_asdf, OCR_99_last_one...""" + sections = {} + if section_name in config.sections(): + sections['-1'] = section_name + + section_names = [config_section for config_section in config.sections() + if re.match(rf'{section_name}_(\d+)', config_section)] + section_names = sorted(section_names) + + for config_section in section_names: + section_id = config_section.replace(section_name + '_', '') + try: + int(section_id) + except ValueError: + logger.warning( + f'Warning: section name {config_section} does not follow naming convention. ' + f'Use only {section_name}_[0-9]+.') + sections[section_id] = config_section + + if 0 in sections.keys() and -1 in sections.keys(): + logger.warning(f'Warning: sections {sections[0]} and {sections[-1]} are both present. ' + f'Use only names following {section_name}_[0-9]+ convention.') + + for section_id, section_full_name in sections.items(): + sections[section_id] = section_factory(config[section_full_name], + config_path=config_path, device=self.device) + + return sections diff --git a/pero_ocr/layout_engines/layout_helpers.py b/pero_ocr/layout_engines/layout_helpers.py index f530501..6041d07 100644 --- a/pero_ocr/layout_engines/layout_helpers.py +++ b/pero_ocr/layout_engines/layout_helpers.py @@ -410,16 +410,6 @@ def adjust_baselines_to_intensity(baselines, img, tolerance=5): return new_baselines -def insert_line_to_page_layout(page_layout: PageLayout, region: RegionLayout, line: TextLine) -> PageLayout: - """Insert line to page layout given region of origin""" - if len(page_layout.regions) == 0 or page_layout.regions[-1].id != region.id: - page_layout.regions.append(region) - page_layout.regions[-1].lines = [line] - else: - page_layout.regions[-1].lines.append(line) - return page_layout - - def split_page_layout(page_layout: PageLayout) -> (PageLayout, PageLayout): """Split page layout to text and non-text lines.""" return split_page_layout_by_categories(page_layout, ['text']) @@ -486,3 +476,22 @@ def merge_page_layouts(page_layout_text: PageLayout, page_layout_no_text: PageLa page_layout_text.regions.append(region) return page_layout_text + + +def insert_line_to_page_layout(page_layout: PageLayout, region: RegionLayout, line: TextLine) -> PageLayout: + """Insert line to page layout given region of origin. Find if region already exists by ID.""" + existing_region = find_region_by_id(page_layout, region.id) + + if existing_region is not None: + existing_region.lines.append(line) + else: + region.lines = [line] + page_layout.regions.append(region) + return page_layout + +def find_region_by_id(page_layout: PageLayout, region_id: str) -> RegionLayout | None: + for region in page_layout.regions: + if region.id == region_id: + return region + return None + From ae516f4887329be8b9c459908a484539ff5f2cec Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 22 Dec 2023 13:16:19 +0100 Subject: [PATCH 28/76] Add image_size to Yolo engine. Add `config_get_list` to get list of categories instead of json string. - `image_size` can be either int or (int, int) according to ultralytics docu: https://docs.ultralytics.com/modes/predict/#inference-arguments --- pero_ocr/document_ocr/page_parser.py | 48 +++++++++++++++++--- pero_ocr/layout_engines/cnn_layout_engine.py | 18 +++++++- pero_ocr/layout_engines/layout_helpers.py | 2 +- user_scripts/compare_page_layouts.py | 2 +- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index cd9c3a6..fd26fc5 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -6,6 +6,7 @@ import time import re import json +from typing import Union, Tuple import torch.cuda @@ -211,7 +212,7 @@ def __init__(self, config, device, config_path=''): self.adjust_heights = config.getboolean('ADJUST_HEIGHTS') self.multi_orientation = config.getboolean('MULTI_ORIENTATION') self.adjust_baselines = config.getboolean('ADJUST_BASELINES') - self.categories = config.get('CATEGORIES', ['text']) + self.categories = config_get_list(config, key='CATEGORIES', fallback=['text']) use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") @@ -310,12 +311,14 @@ class LayoutExtractorYolo(object): def __init__(self, config, device, config_path=''): use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") - self.categories_for_transcription = config.get('CATEGORIES_FOR_TRANSCRIPTION', []) + self.categories = config_get_list(config, key='CATEGORIES', fallback=[]) + self.image_size = self.get_image_size(config) self.engine = LayoutEngineYolo( model_path=compose_path(config['MODEL_PATH'], config_path), device=self.device, detection_threshold=config.getfloat('DETECTION_THRESHOLD'), + image_size=self.image_size ) def process_page(self, img, page_layout: PageLayout): @@ -338,7 +341,7 @@ def process_page(self, img, page_layout: PageLayout): category = result.names[class_id] region = RegionLayout(id_str, polygon, category=category) - if category in self.categories_for_transcription: + if category in self.categories: line = TextLine( id=f'{id_str}-l000', index=0, @@ -350,10 +353,25 @@ def process_page(self, img, page_layout: PageLayout): region.lines.append(line) page_layout.regions.append(region) - page_layout = self.sort_regions_in_reading_order(page_layout, self.categories_for_transcription) + page_layout = self.sort_regions_in_reading_order(page_layout, self.categories) page_layout = helpers.merge_page_layouts(page_layout_text, page_layout) return page_layout + @staticmethod + def get_image_size(config) -> Union[int, Tuple[int, int], None]: + if 'IMAGE_SIZE' not in config: + return None + + try: + image_size = config.getint('IMAGE_SIZE') + except ValueError: + image_size = config_get_list(config, key='IMAGE_SIZE') + if len(image_size) != 2: + raise ValueError(f'Invalid image size. Expected int or list of two ints, but got: ' + f'{image_size} of type {type(image_size)}') + image_size = image_size[0], image_size[1] + return image_size + @staticmethod def get_start_id(used_ids: list) -> int: """Get int from which to start id naming for new regions. @@ -406,7 +424,7 @@ def __init__(self, config, device, config_path): self.filter_incomplete_pages = config.getboolean('FILTER_INCOMPLETE_PAGES') self.filter_pages_with_short_lines = config.getboolean('FILTER_PAGES_WITH_SHORT_LINES') self.length_threshold = config.getint('LENGTH_THRESHOLD') - self.categories = config.get('CATEGORIES', ['text']) + self.categories = config_get_list(config, key='CATEGORIES', fallback=['text']) use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") @@ -480,7 +498,7 @@ def __init__(self, config, config_path=''): poly = config.getint('INTERP') line_scale = config.getfloat('LINE_SCALE') line_height = config.getint('LINE_HEIGHT') - self.categories = config.get('CATEGORIES', []) + self.categories = config_get_list(config, key='CATEGORIES', fallback=[]) self.crop_engine = cropper.EngineLineCropper( line_height=line_height, poly=poly, scale=line_scale) @@ -512,7 +530,7 @@ def __init__(self, config, device, config_path=''): use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") - self.categories = config.get('CATEGORIES', ['text']) + self.categories = config_get_list(config, key='CATEGORIES', fallback=['text']) if 'METHOD' in config and config['METHOD'] == "pytorch_ocr-transformer": self.ocr_engine = TransformerEngineLineOCR(json_file, self.device) @@ -556,6 +574,22 @@ def get_default_device(): return torch.device('cuda') if torch.cuda.is_available() else torch.device ('cpu') +def config_get_list(config, key, fallback=None): + """Get list from config.""" + fallback = fallback if fallback is not None else [] + + if key not in config: + return fallback + + try: + value = json.loads(config[key]) + except json.decoder.JSONDecodeError as e: + logger.warning(f'Failed to parse list from config key "{key}": {e}') + return fallback + else: + return value + + class PageParser(object): def __init__(self, config, device=None, config_path='', ): self.run_layout_parser = config['PAGE_PARSER'].getboolean('RUN_LAYOUT_PARSER', fallback=False) diff --git a/pero_ocr/layout_engines/cnn_layout_engine.py b/pero_ocr/layout_engines/cnn_layout_engine.py index 52bae4a..cb682e4 100644 --- a/pero_ocr/layout_engines/cnn_layout_engine.py +++ b/pero_ocr/layout_engines/cnn_layout_engine.py @@ -1,6 +1,7 @@ import numpy as np from copy import deepcopy import time +from typing import Union import cv2 from scipy import ndimage @@ -375,15 +376,28 @@ def make_clusters(self, b_list, h_list, t_list, layout_separator_map, ds): class LayoutEngineYolo(object): - def __init__(self, model_path, device, detection_threshold=0.2): + def __init__(self, model_path, device, + image_size: Union[int, tuple[int, int], None] = None, + detection_threshold=0.2): self.yolo_net = YOLO(model_path).to(device) self.detection_threshold = detection_threshold + self.image_size = image_size # height or (height, width) def detect(self, image): """Uses yolo_net to find bounding boxes. :param image: input image """ - return self.yolo_net(image, conf=self.detection_threshold)[0] + if self.image_size is not None: + results = self.yolo_net(image, + conf=self.detection_threshold, + imgsz=self.image_size) + else: + results = self.yolo_net(image, conf=self.detection_threshold) + + if results is None: + raise Exception('LayoutEngineYolo returned None.') + return results[0] + def nonmaxima_suppression(input, element_size=(7, 1)): """Vertical non-maxima suppression. diff --git a/pero_ocr/layout_engines/layout_helpers.py b/pero_ocr/layout_engines/layout_helpers.py index 6041d07..2bca399 100644 --- a/pero_ocr/layout_engines/layout_helpers.py +++ b/pero_ocr/layout_engines/layout_helpers.py @@ -489,9 +489,9 @@ def insert_line_to_page_layout(page_layout: PageLayout, region: RegionLayout, li page_layout.regions.append(region) return page_layout + def find_region_by_id(page_layout: PageLayout, region_id: str) -> RegionLayout | None: for region in page_layout.regions: if region.id == region_id: return region return None - diff --git a/user_scripts/compare_page_layouts.py b/user_scripts/compare_page_layouts.py index 67a84ec..b17ba0c 100644 --- a/user_scripts/compare_page_layouts.py +++ b/user_scripts/compare_page_layouts.py @@ -75,7 +75,7 @@ def compare_page_layouts(hyp_fn, ref_fn, xml_file, results) -> dict: hyp_lines = len([1 for _ in hyp_page.lines_iterator()]) ref_lines = len([1 for _ in ref_page.lines_iterator()]) if hyp_lines != ref_lines: - results[xml_file] = results.LINE_COUNT_MISMATCH + results[xml_file] = Result.LINE_COUNT_MISMATCH logging.debug(f'{xml_file}:\tlines count mismatch ' f'(hyp:{hyp_lines} vs ref:{ref_lines})') return results From e5dd2b8fdb3c6735103fb4aed5d724918d3290fd Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 22 Dec 2023 14:44:57 +0100 Subject: [PATCH 29/76] Store box confidence (in LayoutExtractorYOLO) to RegionLayout and export to page_xml region custom. --- pero_ocr/core/layout.py | 21 ++++++++++++++++----- pero_ocr/document_ocr/page_parser.py | 4 ++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 747b5ee..bf41510 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -323,14 +323,16 @@ def from_altoxml(cls, line: ET.SubElement, schema): class RegionLayout(object): def __init__(self, id: str, polygon: np.ndarray, - region_type=None, - category: str = None): + region_type: Optional[str] = None, + category: Optional[str] = None, + detection_confidence: Optional[float] = None): self.id = id # ID string self.polygon = polygon # bounding polygon self.region_type = region_type self.category = category self.lines: list[TextLine] = [] self.transcription = None + self.detection_confidence = detection_confidence def get_lines_of_category(self, categories: str | list): if isinstance(categories, str): @@ -363,8 +365,13 @@ def to_pagexml(self, page_element: ET.SubElement, validate_id: bool = False): if self.region_type is not None: region_element.set("type", self.region_type) - if self.category: - custom = json.dumps({"category": self.category}) + custom = {} + if self.category is not None: + custom['category'] = self.category + if self.detection_confidence is not None: + custom['detection_confidence'] = round(self.detection_confidence, 3) + if len(custom) > 0: + custom = json.dumps(custom) region_element.set("custom", custom) points = ["{},{}".format(int(np.round(coord[0])), int(np.round(coord[1]))) for coord in self.polygon] @@ -390,11 +397,15 @@ def from_pagexml(cls, region_element: ET.SubElement, schema): region_type = region_element.attrib["type"] category = None + detection_confidence = None if "custom" in region_element.attrib: custom = json.loads(region_element.attrib["custom"]) category = custom.get('category', None) + detection_confidence = custom.get('detection_confidence', None) - layout_region = cls(region_element.attrib['id'], region_coords, region_type, category=category) + layout_region = cls(region_element.attrib['id'], region_coords, region_type, + category=category, + detection_confidence=detection_confidence) transcription = region_element.find(schema + 'TextEquiv') if transcription is not None: diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index fd26fc5..42e191f 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -332,14 +332,14 @@ def process_page(self, img, page_layout: PageLayout): for box_id, box in enumerate(boxes): id_str = 'r{:03d}'.format(start_id + box_id) - x_min, y_min, x_max, y_max, _, class_id = box.tolist() + x_min, y_min, x_max, y_max, conf, class_id = box.tolist() polygon = np.array([[x_min, y_min], [x_min, y_max], [x_max, y_max], [x_max, y_min], [x_min, y_min]]) baseline_y = y_min + (y_max - y_min) / 2 baseline = np.array([[x_min, baseline_y], [x_max, baseline_y]]) height = np.floor(np.array([baseline_y - y_min, y_max - baseline_y])) category = result.names[class_id] - region = RegionLayout(id_str, polygon, category=category) + region = RegionLayout(id_str, polygon, category=category, detection_confidence=conf) if category in self.categories: line = TextLine( From 529234ea3702f774e36a830544b3abf0c1356268 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 22 Dec 2023 15:18:57 +0100 Subject: [PATCH 30/76] Add line ID to ALTO export + import. --- pero_ocr/core/layout.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index bf41510..adedc2e 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -181,6 +181,7 @@ def to_altoxml(self, text_block, arabic_helper, min_line_confidence): arabic_line = True text_line = ET.SubElement(text_block, "TextLine") text_line_baseline = int(np.average(np.array(self.baseline)[:, 1])) + text_line.set("ID", f'line_{self.id}') text_line.set("BASELINE", str(text_line_baseline)) text_line_height, text_line_width, text_line_vpos, text_line_hpos = get_hwvh(self.polygon) @@ -293,21 +294,22 @@ def to_altoxml(self, text_block, arabic_helper, min_line_confidence): @classmethod def from_altoxml(cls, line: ET.SubElement, schema): - new_textline = cls(baseline=np.asarray( - [[int(line.attrib['HPOS']), int(line.attrib['BASELINE'])], - [int(line.attrib['HPOS']) + int(line.attrib['WIDTH']), int(line.attrib['BASELINE'])]])) - polygon = [] - new_textline.heights = np.asarray([ - int(line.attrib['HEIGHT']) + int(line.attrib['VPOS']) - int(line.attrib['BASELINE']), - int(line.attrib['BASELINE']) - int(line.attrib['VPOS'])]) - polygon.append([int(line.attrib['HPOS']), int(line.attrib['VPOS'])]) - polygon.append( - [int(line.attrib['HPOS']) + int(line.attrib['WIDTH']), int(line.attrib['VPOS'])]) - polygon.append([int(line.attrib['HPOS']) + int(line.attrib['WIDTH']), - int(line.attrib['VPOS']) + int(line.attrib['HEIGHT'])]) - polygon.append( - [int(line.attrib['HPOS']), int(line.attrib['VPOS']) + int(line.attrib['HEIGHT'])]) + hpos = int(line.attrib['HPOS']) + vpos = int(line.attrib['VPOS']) + width = int(line.attrib['WIDTH']) + height = int(line.attrib['HEIGHT']) + baseline = int(line.attrib['BASELINE']) + + new_textline = cls(id=line.attrib['ID'], + baseline=np.asarray([[hpos, baseline], [hpos + width, baseline]]), + heights=np.asarray([height + vpos - baseline, baseline - vpos])) + + polygon = [[hpos, vpos], + [hpos + width, vpos], + [hpos + width, vpos + height], + [hpos, vpos + height]] new_textline.polygon = np.asarray(polygon) + word = '' start = True for text in line.iter(schema + 'String'): From 036daf3f775e68a11c00836119b6c1820b20bd39 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 22 Dec 2023 15:29:26 +0100 Subject: [PATCH 31/76] Delete unwanted script. --- user_scripts/compare_page_layouts.py | 146 --------------------------- 1 file changed, 146 deletions(-) delete mode 100644 user_scripts/compare_page_layouts.py diff --git a/user_scripts/compare_page_layouts.py b/user_scripts/compare_page_layouts.py deleted file mode 100644 index b17ba0c..0000000 --- a/user_scripts/compare_page_layouts.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import os -import argparse -import sys -import logging -from enum import Enum - -from pero_ocr.core.layout import PageLayout - - -class Result(Enum): - UNPROCESSED = 0 - OK = 1 - REGION_COUNT_MISMATCH = 2 - LINE_COUNT_MISMATCH = 3 - HYP_MISSING = 4 - REF_MISSING = 5 - BOTH_MISSING = 6 - - -results_description = { - Result.UNPROCESSED: 'unprocessed', - Result.OK: 'are ok', - Result.REGION_COUNT_MISMATCH: 'have region count mismatch', - Result.LINE_COUNT_MISMATCH: 'have line count mismatch', - Result.HYP_MISSING: 'have hyp missing', - Result.REF_MISSING: 'have ref missing', - Result.BOTH_MISSING: 'have both missing', -} - - -def parse_arguments(): - parser = argparse.ArgumentParser( - description='Compare two folders with page layouts and output their structural and content differences.') - parser.add_argument('--print-all', action='store_true', help='Report info per page layout.') - parser.add_argument('--hyp', required=True, help='Folder with page xmls whose will be compared to reference') - parser.add_argument('--ref', required=True, help='Folder with reference page xmls.') - args = parser.parse_args() - return args - - -def read_pagexml(path): - try: - page_layout = PageLayout(file=path) - except OSError: - print(f'Warning: unable to load page xml "{path}"') - return None - return page_layout - - -def compare_page_layouts(hyp_fn, ref_fn, xml_file, results) -> dict: - hyp_page = read_pagexml(hyp_fn) - ref_page = read_pagexml(ref_fn) - if hyp_page is None and ref_page is None: - logging.debug(f'{xml_file}:\tboth missing') - results[xml_file] = Result.BOTH_MISSING - return results - if hyp_page is None and ref_page is not None: - results[xml_file] = Result.HYP_MISSING - logging.debug(f'{xml_file}:\thyp missing') - return results - if hyp_page is not None and ref_page is None: - results[xml_file] = Result.REF_MISSING - logging.debug(f'{xml_file}:\tref missing') - return results - - if len(hyp_page.regions) != len(ref_page.regions): - results[xml_file] = Result.REGION_COUNT_MISMATCH - logging.debug(f'{xml_file}:\tregions count mismatch ' - f'(hyp:{len(hyp_page.regions)} vs ref:{len(ref_page.regions)})') - return results - - hyp_lines = len([1 for _ in hyp_page.lines_iterator()]) - ref_lines = len([1 for _ in ref_page.lines_iterator()]) - if hyp_lines != ref_lines: - results[xml_file] = Result.LINE_COUNT_MISMATCH - logging.debug(f'{xml_file}:\tlines count mismatch ' - f'(hyp:{hyp_lines} vs ref:{ref_lines})') - return results - - results[xml_file] = Result.OK - # compare content of lines somehow? (problem with aligning due to different ids generated by pero) - # Can be done like this: take all lines to a list, sort them alphabetically, then compare them one by one. - - return results - - -def group_results(results): - grouped_results = {result: 0 for result in set(results.values())} - - for _, result in results.items(): - grouped_results[result] += 1 - - return grouped_results - - -def print_results(results, print_all): - total_files = len(results) - results = group_results(results) - ok_count = results.get(Result.OK, 0) - results.pop(Result.OK, None) - - logging.debug(f'{total_files} files total:') - logging.debug(f'\t{ok_count} {results_description[Result.OK]}') - for result, desc in sorted(results_description.items(), key=lambda x: x[0].value): - result_count = results.get(result, 0) - if result_count > 0: - logging.debug(f'\t{result_count} {desc}') - - if not print_all: - if ok_count == total_files: - logging.info('All files are ok.') - else: - logging.info('Some files are not ok.') - - -def setup_logging(print_all): - logging.getLogger().handlers[0].setFormatter(logging.Formatter('%(message)s')) # print only message - - if print_all: - logging.root.setLevel(logging.DEBUG) - else: - logging.root.setLevel(logging.INFO) - - -def main(): - args = parse_arguments() - setup_logging(args.print_all) - - xml_to_process = set(f for f in os.listdir(args.ref) if os.path.splitext(f)[1] == '.xml') - xml_to_process |= set(f for f in os.listdir(args.hyp) if os.path.splitext(f)[1] == '.xml') - - results = {xml_file: Result.UNPROCESSED for xml_file in xml_to_process} - - for xml_file in xml_to_process: - hyp_path = os.path.join(args.hyp, xml_file) - ref_path = os.path.join(args.ref, xml_file) - results = compare_page_layouts(hyp_path, ref_path, xml_file, results) - - print_results(results, args.print_all) - - -if __name__ == "__main__": - main() From 697990c15a5d2f6ef74401b26e99302b962e0dc1 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 22 Dec 2023 17:00:05 +0100 Subject: [PATCH 32/76] Add translating short music output to original encoding. - `TransformerEngineLineOCR` now translates all outputs according to `music_dictionary` dictionary in ocr.json (or omr.json), if exists. - Move `MusicTranslator` to a separate file for nicer imports everywhere. --- pero_ocr/music/export_music.py | 35 +--------- pero_ocr/music/music_translator.py | 65 +++++++++++++++++++ pero_ocr/ocr_engine/line_ocr_engine.py | 8 ++- pero_ocr/ocr_engine/transformer_ocr_engine.py | 4 ++ 4 files changed, 76 insertions(+), 36 deletions(-) create mode 100644 pero_ocr/music/music_translator.py diff --git a/pero_ocr/music/export_music.py b/pero_ocr/music/export_music.py index 62c00e3..195c5bd 100644 --- a/pero_ocr/music/export_music.py +++ b/pero_ocr/music/export_music.py @@ -26,6 +26,7 @@ from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine from pero_ocr.music.music_structures import Measure +from pero_ocr.music.music_translator import MusicTranslator as Translator def parseargs(): @@ -319,40 +320,6 @@ def export_midi(self, file_base: str = 'out'): parsed_xml.write('mid', filename) -class Translator: - """Translator class for translating shorter SSemantic encoding to Semantic encoding using translator dictionary.""" - def __init__(self, file_name: str): - self.translator = Translator.read_json(file_name) - self.translator_reversed = {v: k for k, v in self.translator.items()} - self.n_existing_labels = set() - - def convert_line(self, line, to_shorter: bool = True): - line = line.strip('"').strip() - symbols = re.split(r'\s+', line) - converted_symbols = [self.convert_symbol(symbol, to_shorter) for symbol in symbols] - - return ' '.join(converted_symbols) - - def convert_symbol(self, symbol: str, to_shorter: bool = True): - dictionary = self.translator if to_shorter else self.translator_reversed - - try: - return dictionary[symbol] - except KeyError: - if symbol not in self.n_existing_labels: - self.n_existing_labels.add(symbol) - print(f'Not existing label: ({symbol})') - return '' - - @staticmethod - def read_json(filename) -> dict: - if not os.path.isfile(filename): - raise FileNotFoundError(f'Translator file ({filename}) not found. Cannot export music.') - - with open(filename) as f: - data = json.load(f) - return data - def parse_semantic_to_measures(labels: str) -> list[Measure]: """Convert line of semantic labels to list of measures. diff --git a/pero_ocr/music/music_translator.py b/pero_ocr/music/music_translator.py new file mode 100644 index 0000000..23e5d41 --- /dev/null +++ b/pero_ocr/music/music_translator.py @@ -0,0 +1,65 @@ + +from typing import Union +import re +import logging +import json +import os + +logger = logging.getLogger(__name__) + + +class MusicTranslator: + """MusicTranslator class for translating shorter SSemantic encoding to Semantic encoding using dictionary.""" + def __init__(self, dictionary: dict = None, filename: str = None): + self.dictionary = self.load_dictionary(dictionary, filename) + self.dictionary_reversed = {v: k for k, v in self.dictionary.items()} + self.n_existing_labels = set() + + def __call__(self, inputs: Union[str, list], to_longer: bool = True) -> Union[str, list]: + if isinstance(inputs, list): + if len(inputs[0]) > 1: # list of strings (lines) + return self.translate_lines(inputs, to_longer) + else: # list of chars (one line total) + return self.translate_line(''.join(inputs), to_longer) + elif isinstance(inputs, str): # one line + return self.translate_line(inputs, to_longer) + else: + raise ValueError(f'MusicTranslator: Unsupported input type: {type(inputs)}') + + def translate_lines(self, lines: list, to_longer: bool = True) -> list: + return [self.translate_line(line, to_longer) for line in lines] + + def translate_line(self, line, to_longer: bool = True): + line = line.strip('"').strip() + symbols = re.split(r'\s+', line) + converted_symbols = [self.translate_symbol(symbol, to_longer) for symbol in symbols] + + return ' '.join(converted_symbols) + + def translate_symbol(self, symbol: str, to_longer: bool = True): + dictionary = self.dictionary_reversed if to_longer else self.dictionary + + try: + return dictionary[symbol] + except KeyError: + if symbol not in self.n_existing_labels: + self.n_existing_labels.add(symbol) + logger.info(f'Not existing label: ({symbol})') + return '' + + def load_dictionary(self, dictionary: dict = None, filename: str = None) -> dict: + if dictionary is not None: + return dictionary + elif filename is not None: + return self.read_json(filename) + else: + raise ValueError('MusicTranslator: Either dictionary or filename must be provided.') + + @staticmethod + def read_json(filename) -> dict: + if not os.path.isfile(filename): + raise FileNotFoundError(f'Translator file ({filename}) not found. Cannot export music.') + + with open(filename) as f: + data = json.load(f) + return data diff --git a/pero_ocr/ocr_engine/line_ocr_engine.py b/pero_ocr/ocr_engine/line_ocr_engine.py index b8340ea..56cf8b3 100644 --- a/pero_ocr/ocr_engine/line_ocr_engine.py +++ b/pero_ocr/ocr_engine/line_ocr_engine.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import print_function -import argparse import json -import cv2 import numpy as np from os.path import isabs, realpath, join, dirname from scipy import sparse @@ -12,6 +10,7 @@ from .softmax import softmax from pero_ocr.sequence_alignment import levenshtein_distance +from pero_ocr.music.music_translator import MusicTranslator class BaseEngineLineOCR(object): @@ -28,6 +27,11 @@ def __init__(self, json_def, device, batch_size=8, model_type="ctc"): self.checkpoint = realpath(join(dirname(json_def), self.config['checkpoint'])) self.characters = tuple(self.config['characters']) + + self.music_translator = None + if 'music_dictionary' in self.config: + self.music_translator = MusicTranslator(dictionary=self.config['music_dictionary']) + self.net_name = self.config['net_name'] if "embed_num" in self.config: self.embed_num = int(self.config["embed_num"]) diff --git a/pero_ocr/ocr_engine/transformer_ocr_engine.py b/pero_ocr/ocr_engine/transformer_ocr_engine.py index d96c573..e00061c 100644 --- a/pero_ocr/ocr_engine/transformer_ocr_engine.py +++ b/pero_ocr/ocr_engine/transformer_ocr_engine.py @@ -107,5 +107,9 @@ def decode(self, labels): outputs = [] for line_labels in labels: outputs.append(''.join([self.characters[c] for c in line_labels])) + + if self.music_translator is not None: + outputs = self.music_translator(outputs, to_longer=True) + return outputs From 893bacadf5fc22170971c71d91429a5a929429bd Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 29 Dec 2023 17:58:58 +0100 Subject: [PATCH 33/76] Enable loading model to cpu. --- pero_ocr/ocr_engine/transformer_ocr_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pero_ocr/ocr_engine/transformer_ocr_engine.py b/pero_ocr/ocr_engine/transformer_ocr_engine.py index e00061c..7200fd4 100644 --- a/pero_ocr/ocr_engine/transformer_ocr_engine.py +++ b/pero_ocr/ocr_engine/transformer_ocr_engine.py @@ -25,7 +25,7 @@ def __init__(self, json_def, device, batch_size=4): print(self.net) - self.net.load_state_dict(torch.load(self.checkpoint)) + self.net.load_state_dict(torch.load(self.checkpoint, map_location=device)) self.net.eval() self.net = self.net.to(device) From e2a26def92d6ef556e1db3c07752a9b4e6f78efb Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 29 Dec 2023 18:03:30 +0100 Subject: [PATCH 34/76] Add `CATEGORIES` option to sorters and delete therefore unused function `sort_regions_in_reading_order` --- pero_ocr/document_ocr/page_parser.py | 43 +---------------------- pero_ocr/layout_engines/layout_helpers.py | 7 ++-- pero_ocr/layout_engines/naive_sorter.py | 9 ++++- pero_ocr/layout_engines/smart_sorter.py | 8 +++++ pero_ocr/utils.py | 19 ++++++++++ 5 files changed, 40 insertions(+), 46 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 42e191f..c18810b 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -10,7 +10,7 @@ import torch.cuda -from pero_ocr.utils import compose_path +from pero_ocr.utils import compose_path, config_get_list from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine import pero_ocr.core.crop_engine as cropper from pero_ocr.ocr_engine.pytorch_ocr_engine import PytorchEngineLineOCR @@ -353,7 +353,6 @@ def process_page(self, img, page_layout: PageLayout): region.lines.append(line) page_layout.regions.append(region) - page_layout = self.sort_regions_in_reading_order(page_layout, self.categories) page_layout = helpers.merge_page_layouts(page_layout_text, page_layout) return page_layout @@ -393,30 +392,6 @@ def get_start_id(used_ids: list) -> int: last_used_id = sorted(ids)[-1] return last_used_id + 1 - @staticmethod - def sort_regions_in_reading_order(page_layout: PageLayout, categories_to_sort: list = None) -> PageLayout: - if not categories_to_sort: - music_regions = page_layout.regions - else: - music_regions = [region for region in page_layout.regions if region.category in categories_to_sort] - - music_region_ids = [region.id for region in music_regions] - - regions_with_bounding_boxes = {} - for region in music_regions: - regions_with_bounding_boxes[region] = {'id': region.id, 'bounding_box': region.get_polygon_bounding_box()} - - regions_sorted = sorted(regions_with_bounding_boxes.items(), key=lambda x: x[1]['bounding_box'][0]) - - # Rename all music regions as rXXX_tmp to prevent two regions having the same id while renaming them - for region in music_regions: - region.replace_id(region.id + '_tmp') - - for sorted_region, region_id in zip(regions_sorted, music_region_ids): - page_layout.rename_region_id(sorted_region[1]['id'] + '_tmp', region_id) - - return page_layout - class LineFilter(object): def __init__(self, config, device, config_path): @@ -574,22 +549,6 @@ def get_default_device(): return torch.device('cuda') if torch.cuda.is_available() else torch.device ('cpu') -def config_get_list(config, key, fallback=None): - """Get list from config.""" - fallback = fallback if fallback is not None else [] - - if key not in config: - return fallback - - try: - value = json.loads(config[key]) - except json.decoder.JSONDecodeError as e: - logger.warning(f'Failed to parse list from config key "{key}": {e}') - return fallback - else: - return value - - class PageParser(object): def __init__(self, config, device=None, config_path='', ): self.run_layout_parser = config['PAGE_PARSER'].getboolean('RUN_LAYOUT_PARSER', fallback=False) diff --git a/pero_ocr/layout_engines/layout_helpers.py b/pero_ocr/layout_engines/layout_helpers.py index 2bca399..a122988 100644 --- a/pero_ocr/layout_engines/layout_helpers.py +++ b/pero_ocr/layout_engines/layout_helpers.py @@ -429,9 +429,10 @@ def split_page_layout_by_categories(page_layout: PageLayout, categories: list) - RegionLayout(id='r001', lines=[TextLine(id='r001-l002', category='logo')])]) """ if not categories: - page_layout_no_text = deepcopy(page_layout) - page_layout_no_text.regions = [] - return page_layout, page_layout_no_text + # if no categories, return original page_layout and empty page_layout + page_layout_no_regions = deepcopy(page_layout) + page_layout_no_regions.regions = [] + return page_layout, page_layout_no_regions regions = page_layout.regions page_layout.regions = [] diff --git a/pero_ocr/layout_engines/naive_sorter.py b/pero_ocr/layout_engines/naive_sorter.py index 36d2357..4b9372d 100644 --- a/pero_ocr/layout_engines/naive_sorter.py +++ b/pero_ocr/layout_engines/naive_sorter.py @@ -1,12 +1,17 @@ #!/usr/bin/env python3 import numpy as np +import logging from configparser import SectionProxy from sklearn.cluster import DBSCAN from typing import List +from pero_ocr.utils import config_get_list from pero_ocr.core.layout import PageLayout, RegionLayout +from pero_ocr.layout_engines import layout_helpers as helpers + +logger = logging.getLogger(__name__) class Region: @@ -42,10 +47,11 @@ class NaiveRegionSorter: def __init__(self, config: SectionProxy, config_path=""): # minimal distance between clusters = page_width / width_denom self.width_denom = config.getint('ImageWidthDenominator', fallback=10) + self.categories = config_get_list(config, key='CATEGORIES', fallback=[]) def process_page(self, image, page_layout: PageLayout): + page_layout, page_layout_ignore = helpers.split_page_layout_by_categories(page_layout, self.categories) regions = [] - for region in page_layout.regions: regions.append(Region(region)) @@ -54,6 +60,7 @@ def process_page(self, image, page_layout: PageLayout): page_layout.regions = [page_layout.regions[idx] for idx in order] + page_layout = helpers.merge_page_layouts(page_layout_ignore, page_layout) return page_layout @staticmethod diff --git a/pero_ocr/layout_engines/smart_sorter.py b/pero_ocr/layout_engines/smart_sorter.py index d4c893f..eedfb1d 100644 --- a/pero_ocr/layout_engines/smart_sorter.py +++ b/pero_ocr/layout_engines/smart_sorter.py @@ -3,6 +3,7 @@ import cv2 import math import numpy as np +import logging from configparser import SectionProxy from copy import deepcopy @@ -11,6 +12,10 @@ from typing import List, Dict, Union, Optional from pero_ocr.core.layout import PageLayout, RegionLayout +from pero_ocr.utils import config_get_list +from pero_ocr.layout_engines import layout_helpers as helpers + +logger = logging.getLogger(__name__) def pairwise(iterable): @@ -275,8 +280,10 @@ class SmartRegionSorter: def __init__(self, config: SectionProxy, config_path=""): # if intersection of two regions is less than given parameter w.r.t. both regions, intersection doesn't count self.intersect_param = config.getfloat('FakeIntersectionParameter', fallback=0.1) + self.categories = config_get_list(config, key='CATEGORIES', fallback=[]) def process_page(self, image, page_layout: PageLayout): + page_layout, page_layout_ignore = helpers.split_page_layout_by_categories(page_layout, self.categories) regions = [] if len(page_layout.regions) < 2: @@ -300,6 +307,7 @@ def process_page(self, image, page_layout: PageLayout): page_layout.regions = [page_layout.regions[idx] for idx in region_idxs] page_layout = SmartRegionSorter.rotate_page_layout(page_layout, rotation) + page_layout = helpers.merge_page_layouts(page_layout_ignore, page_layout) return page_layout @staticmethod diff --git a/pero_ocr/utils.py b/pero_ocr/utils.py index f25c700..b1894b8 100644 --- a/pero_ocr/utils.py +++ b/pero_ocr/utils.py @@ -2,6 +2,9 @@ import sys import logging import subprocess +import json + +logger = logging.getLogger(__name__) try: subprocess.check_output( @@ -22,3 +25,19 @@ def compose_path(file_path, reference_path): if reference_path and not isabs(file_path): file_path = join(reference_path, file_path) return file_path + + +def config_get_list(config, key, fallback=None): + """Get list from config.""" + fallback = fallback if fallback is not None else [] + + if key not in config: + return fallback + + try: + value = json.loads(config[key]) + except json.decoder.JSONDecodeError as e: + logger.warning(f'Failed to parse list from config key "{key}": {e}') + return fallback + else: + return value From d59884ae1481ad06cf8172c992ca03ca018ea664 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 4 Jan 2024 12:13:27 +0100 Subject: [PATCH 35/76] Alto_export: export music transcription as one string in each TextLine --- pero_ocr/core/layout.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index adedc2e..cca235b 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -176,9 +176,9 @@ def from_pagexml_parse_custom(self, custom_str): self.heights = heights.tolist() def to_altoxml(self, text_block, arabic_helper, min_line_confidence): - arabic_line = False - if arabic_helper.is_arabic_line(self.transcription): - arabic_line = True + if self.transcription_confidence is not None and self.transcription_confidence < min_line_confidence: + return + text_line = ET.SubElement(text_block, "TextLine") text_line_baseline = int(np.average(np.array(self.baseline)[:, 1])) text_line.set("ID", f'line_{self.id}') @@ -191,6 +191,27 @@ def to_altoxml(self, text_block, arabic_helper, min_line_confidence): text_line.set("HEIGHT", str(int(text_line_height))) text_line.set("WIDTH", str(int(text_line_width))) + if self.category == 'text': + self.to_altoxml_text(text_line, arabic_helper, + text_line_height, text_line_width, text_line_vpos, text_line_hpos) + else: + string = ET.SubElement(text_line, "String") + string.set("CONTENT", self.transcription) + + string.set("HEIGHT", str(int(text_line_height))) + string.set("WIDTH", str(int(text_line_width))) + string.set("VPOS", str(int(text_line_vpos))) + string.set("HPOS", str(int(text_line_hpos))) + + if self.transcription_confidence is not None: + string.set("WC", str(round(self.transcription_confidence, 2))) + + def to_altoxml_text(self, text_line, arabic_helper, + text_line_height, text_line_width, text_line_vpos, text_line_hpos): + arabic_line = False + if arabic_helper.is_arabic_line(self.transcription): + arabic_line = True + try: chars = [i for i in range(len(self.characters))] char_to_num = dict(zip(self.characters, chars)) @@ -288,9 +309,6 @@ def to_altoxml(self, text_block, arabic_helper, min_line_confidence): space.set("VPOS", str(int(np.min(all_y)))) space.set("HPOS", str(int(np.max(all_x)))) letter_counter += len(splitted_transcription[w]) + 1 - if self.transcription_confidence is not None: - if self.transcription_confidence < min_line_confidence: - text_block.remove(text_line) @classmethod def from_altoxml(cls, line: ET.SubElement, schema): From 0a1a0c096be1cd7dec6502cf1331c678f73daec8 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Mon, 15 Jan 2024 17:15:21 +0100 Subject: [PATCH 36/76] Tiny improvements. - print more informative warning message - add `__init__.py` to enable python package functionality for future. --- pero_ocr/music/__init__.py | 0 pero_ocr/utils.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 pero_ocr/music/__init__.py diff --git a/pero_ocr/music/__init__.py b/pero_ocr/music/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pero_ocr/utils.py b/pero_ocr/utils.py index b1894b8..669f293 100644 --- a/pero_ocr/utils.py +++ b/pero_ocr/utils.py @@ -37,7 +37,7 @@ def config_get_list(config, key, fallback=None): try: value = json.loads(config[key]) except json.decoder.JSONDecodeError as e: - logger.warning(f'Failed to parse list from config key "{key}": {e}') + logger.warning(f'Failed to parse list from config key "{key}", returning fallback {fallback}:\n{e}') return fallback else: return value From faf0496e738976c0e0ae0f448afe61d3f838c037 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Tue, 16 Jan 2024 09:45:06 +0100 Subject: [PATCH 37/76] Delete unused function from `layout`, music integration. - delete `layout.py/PageLayout.get_regions_of_category` as its functionality was substituted by `layout_helpers.py/split_page_layout_by_categories`. For sorting regions use (Naive|Smart)RegionSorter during `parse_folder` execution resulting in regions being sorted in xml output. - in `music_exporter.py` make translator optional argument. --- pero_ocr/core/layout.py | 20 ----------------- pero_ocr/music/export_music.py | 40 ++++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index cca235b..c74c002 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -965,26 +965,6 @@ def get_quality(self, x: int = None, y: int = None, width: int = None, height: i else: return -1 - def get_regions_of_category(self, categories: str | list, reading_order=False): - if isinstance(categories, str): - categories = [categories] - - if not reading_order: - return [region for region in self.regions if region.category in categories] - - category_regions = [region for region in self.regions if region.category in categories] - - regions_with_bounding_boxes = {} - for region in category_regions: - regions_with_bounding_boxes[region] = { - 'id': region.id, - 'bounding_box': region.get_polygon_bounding_box(), - 'region': region} - - regions_sorted = sorted(regions_with_bounding_boxes.items(), key=lambda x: x[1]['bounding_box'][1]) - - return [region[1]['region'] for region in regions_sorted] - def rename_region_id(self, old_id, new_id): for region in self.regions: if region.id == old_id: diff --git a/pero_ocr/music/export_music.py b/pero_ocr/music/export_music.py index 195c5bd..b3a3807 100644 --- a/pero_ocr/music/export_music.py +++ b/pero_ocr/music/export_music.py @@ -20,11 +20,11 @@ import re import time import logging -import json import music21 as music from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine +from pero_ocr.layout_engines.layout_helpers import split_page_layout_by_categories from pero_ocr.music.music_structures import Measure from pero_ocr.music.music_translator import MusicTranslator as Translator @@ -41,8 +41,11 @@ def parseargs(): '-f', '--input-transcription-files', nargs='*', default=None, help='Input files with sequences as lines with IDs at the beginning.') parser.add_argument( - "-t", "--translator-path", type=str, required=True, - help="JSON File containing translation dictionary from shorter encoding (exported by model) to longest.") + "-t", "--translator-path", type=str, default=None, + help="JSON File containing translation dictionary from shorter encoding (exported by model) to longest " + "Check if needed by seeing start of any line in the transcription." + "(e.g. SSemantic (model output): >2 + kGM + B3z + C4z + |..." + " Semantic (stored in XML): clef-G2 + keySignature-GM + note-B3_eighth + note-C4_eighth + barline...") parser.add_argument( "-o", "--output-folder", default='output_page', help="Set output file with extension. Output format is JSON") @@ -82,9 +85,9 @@ def main(): class ExportMusicPage: """Take pageLayout XML exported from pero-ocr with transcriptions and re-construct page of musical notation.""" - def __init__(self, input_xml_path: str = '', input_transcription_files: list[str] = None, translator_path: str = '', - output_folder: str = 'output_page', export_midi: bool = False, export_musicxml: bool = False, - categories: list = None, verbose: bool = False): + def __init__(self, input_xml_path: str = '', input_transcription_files: list[str] = None, + translator_path: str = None, output_folder: str = 'output_page', export_midi: bool = False, + export_musicxml: bool = False, categories: list = None, verbose: bool = False): self.translator_path = translator_path if verbose: logging.basicConfig(level=logging.DEBUG, format='[%(levelname)-s] \t- %(message)s') @@ -104,10 +107,10 @@ def __init__(self, input_xml_path: str = '', input_transcription_files: list[str self.export_midi = export_midi self.export_musicxml = export_musicxml - self.translator = Translator(file_name=self.translator_path) + self.translator = Translator(filename=self.translator_path) if translator_path else None self.categories = categories if categories else ['Notový zápis'] - def __call__(self, page_layout = None) -> None: + def __call__(self, page_layout=None) -> None: if self.input_transcription_files: ExportMusicLines(input_files=self.input_transcription_files, output_folder=self.output_folder, translator=self.translator, verbose=self.verbose)() @@ -123,9 +126,9 @@ def process_page(self, page_layout: PageLayout) -> None: def export_page_layout(self, page_layout: PageLayout, file_id: str = None) -> None: if self.export_musicxml or self.export_midi: + page_layout, _ = split_page_layout_by_categories(page_layout, self.categories) parts = self.regions_to_parts( - page_layout.get_regions_of_category(self.categories, reading_order=True), - self.translator) + page_layout.regions) if not parts: return @@ -167,7 +170,7 @@ def export_to_midi(self, score, parts, file_id: str=None): base = self.get_output_file_base(file_id) part.export_to_midi(base) - def regions_to_parts(self, regions: list[RegionLayout], translator) -> list[Part]: + def regions_to_parts(self, regions: list[RegionLayout]) -> list[Part]: """Takes a list of regions and splits them to parts.""" max_parts = max( [len(region.get_lines_of_category(self.categories)) for region in regions], @@ -177,7 +180,7 @@ def regions_to_parts(self, regions: list[RegionLayout], translator) -> list[Part print('Warning: No music lines found in page.') return [] - parts = [Part(translator) for _ in range(max_parts)] + parts = [Part(self.translator) for _ in range(max_parts)] for region in regions: for part, line in zip(parts, region.get_lines_of_category(self.categories)): part.add_textline(line) @@ -187,8 +190,9 @@ def regions_to_parts(self, regions: list[RegionLayout], translator) -> list[Part class ExportMusicLines: """Takes text files with transcriptions as individual lines and exports musicxml file for each one""" - def __init__(self, translator: Translator, input_files: list[str] = None, + def __init__(self, translator: Translator = None, input_files: list[str] = None, output_folder: str = 'output_musicxml', verbose: bool = False): + self.translate_to_longer = translator is not None self.translator = translator self.output_folder = output_folder @@ -222,7 +226,8 @@ def __call__(self): stave_id = match.group(1) labels = match.group(3) - labels = self.translator.convert_line(labels, to_shorter=False) + if self.translate_to_longer: + labels = self.translator.translate_line(labels, to_longer=True) output_file_name = os.path.join(self.output_folder, f'{stave_id}.musicxml') parsed_labels = semantic_line_to_music21_score(labels) @@ -269,7 +274,8 @@ def read_file_lines(input_file: str) -> list[str]: class Part: """Represent musical part (part of notation for one instrument/section)""" - def __init__(self, translator): + def __init__(self, translator: Translator = None): + self.translate_to_longer = translator is not None self.translator = translator self.repr_music21 = music.stream.Part([music.instrument.Piano()]) @@ -278,7 +284,9 @@ def __init__(self, translator): self.measures: list[Measure] = [] # List of measures in internal representation, NOT music21 def add_textline(self, line: TextLine) -> None: - labels = self.translator.convert_line(line.transcription, False) + labels = line.transcription + if self.translate_to_longer: + labels = self.translator.translate_line(labels, to_longer=True) self.labels.append(labels) new_measures = parse_semantic_to_measures(labels) From dd4bbc8a20d0e4311230b6db2603790fabbed9f3 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 18 Jan 2024 11:27:10 +0100 Subject: [PATCH 38/76] Change simple print warnings to `logger.warning`. --- pero_ocr/document_ocr/page_parser.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index c18810b..05618b4 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -5,7 +5,6 @@ import math import time import re -import json from typing import Union, Tuple import torch.cuda @@ -443,7 +442,7 @@ def __init__(self, config, config_path=''): def process_page(self, img, page_layout: PageLayout): if not page_layout.regions: - print(f"Warning: Skipping line post processing for page {page_layout.id}. No text region present.") + logger.warning(f"Skipping line post processing for page {page_layout.id}. No text region present.") return page_layout for region in page_layout.regions: @@ -458,7 +457,7 @@ def __init__(self, config, config_path=''): def process_page(self, img, page_layout: PageLayout): if not page_layout.regions: - print(f"Warning: Skipping layout post processing for page {page_layout.id}. No text region present.") + logger.warning(f"Skipping layout post processing for page {page_layout.id}. No text region present.") return page_layout if self.retrace_regions: @@ -485,7 +484,7 @@ def process_page(self, img, page_layout: PageLayout): except ValueError: line.crop = np.zeros( (self.crop_engine.line_height, self.crop_engine.line_height, 3)) - print(f"WARNING: Failed to crop line {line.id} in page {page_layout.id}. Probably contain vertical line. Contanct Olda Kodym to fix this bug!") + logger.warning(f"Failed to crop line {line.id} in page {page_layout.id}. Probably contain vertical line. Contanct Olda Kodym to fix this bug!") return page_layout def crop_lines(self, img, lines: list): @@ -496,7 +495,7 @@ def crop_lines(self, img, lines: list): except ValueError: line.crop = np.zeros( (self.crop_engine.line_height, self.crop_engine.line_height, 3)) - print(f"WARNING: Failed to crop line {line.id}. Probably contain vertical line. Contanct Olda Kodym to fix this bug!") + logger.warning(f"Failed to crop line {line.id}. Probably contain vertical line. Contanct Olda Kodym to fix this bug!") class PageOCR(object): From 6fc82e4328e10821ded4a695198770d25cd4cf41 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 18 Jan 2024 15:33:06 +0100 Subject: [PATCH 39/76] Change config categories, line_categories, add decoder filter. - `LINE_CATEGORIES`: for detected region also create a TextLine for future transcription. - `CATEGORIES`: categories to save in `LayoutExtractorYolo`, delete others. (if `None` or `[]`, save all) - disable YOLO detection results printing - filter categories for `PageDecoder` --- pero_ocr/document_ocr/page_parser.py | 17 ++++++++++++----- pero_ocr/layout_engines/cnn_layout_engine.py | 7 +++++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 05618b4..f015b25 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -72,7 +72,9 @@ def page_decoder_factory(config, device, config_path=''): decoder = decoding_itf.decoder_factory(config['DECODER'], ocr_chars, device, allow_no_decoder=False, config_path=config_path) confidence_threshold = config['DECODER'].getfloat('CONFIDENCE_THRESHOLD', fallback=math.inf) carry_h_over = config['DECODER'].getboolean('CARRY_H_OVER') - return PageDecoder(decoder, line_confidence_threshold=confidence_threshold, carry_h_over=carry_h_over) + categories = config_get_list(config['DECODER'], key='CATEGORIES', fallback=['text']) + return PageDecoder(decoder, line_confidence_threshold=confidence_threshold, carry_h_over=carry_h_over, + categories=categories) class MissingLogits(Exception): @@ -95,14 +97,14 @@ def prepare_dense_logits(line): class PageDecoder: - def __init__(self, decoder, line_confidence_threshold=None, carry_h_over=False): + def __init__(self, decoder, line_confidence_threshold=None, carry_h_over=False, categories=None): self.decoder = decoder self.line_confidence_threshold = line_confidence_threshold self.lines_examined = 0 self.lines_decoded = 0 self.seconds_decoding = 0.0 self.continue_lines = carry_h_over - self.categories = ['text'] + self.categories = categories if categories else ['text'] self.last_h = None self.last_line = None @@ -113,7 +115,8 @@ def process_page(self, page_layout: PageLayout): try: line.transcription = self.decode_line(line) except Exception: - logger.error(f'Failed to process line {line.id} of page {page_layout.id}. The page has been processed no further.', exc_info=True) + logger.error(f'Failed to process line {line.id} of page {page_layout.id}. ' + f'The page has been processed no further.', exc_info=True) return page_layout @@ -311,6 +314,7 @@ def __init__(self, config, device, config_path=''): use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") self.categories = config_get_list(config, key='CATEGORIES', fallback=[]) + self.line_categories = config_get_list(config, key='LINE_CATEGORIES', fallback=[]) self.image_size = self.get_image_size(config) self.engine = LayoutEngineYolo( @@ -338,9 +342,12 @@ def process_page(self, img, page_layout: PageLayout): height = np.floor(np.array([baseline_y - y_min, y_max - baseline_y])) category = result.names[class_id] + if self.categories and category not in self.categories: + continue + region = RegionLayout(id_str, polygon, category=category, detection_confidence=conf) - if category in self.categories: + if category in self.line_categories: line = TextLine( id=f'{id_str}-l000', index=0, diff --git a/pero_ocr/layout_engines/cnn_layout_engine.py b/pero_ocr/layout_engines/cnn_layout_engine.py index cb682e4..e49b758 100644 --- a/pero_ocr/layout_engines/cnn_layout_engine.py +++ b/pero_ocr/layout_engines/cnn_layout_engine.py @@ -390,9 +390,12 @@ def detect(self, image): if self.image_size is not None: results = self.yolo_net(image, conf=self.detection_threshold, - imgsz=self.image_size) + imgsz=self.image_size, + verbose=False) else: - results = self.yolo_net(image, conf=self.detection_threshold) + results = self.yolo_net(image, + conf=self.detection_threshold, + verbose=False) if results is None: raise Exception('LayoutEngineYolo returned None.') From 53ee27a0dc6159762351590747d78ef51ba0f6aa Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 18 Jan 2024 16:58:34 +0100 Subject: [PATCH 40/76] Rename `MusicTranslator` for to more general `OutputTranslator` and everything around it. - substitution dictionary needs reversed format now. Translate key to values (to be more obvious). Example follows: - SSemantic music, Model output (now keys of dictionary): >2 + kGM + B3z + C4z + |..." - Semantic music (now values of dictionary): clef-G2 + keySignature-GM + note-B3_eighth + note-C4_eighth + barline...") --- pero_ocr/music/export_music.py | 18 ++++----- ...sic_translator.py => output_translator.py} | 37 ++++++++++--------- pero_ocr/ocr_engine/line_ocr_engine.py | 8 ++-- pero_ocr/ocr_engine/transformer_ocr_engine.py | 6 +-- 4 files changed, 34 insertions(+), 35 deletions(-) rename pero_ocr/music/{music_translator.py => output_translator.py} (50%) diff --git a/pero_ocr/music/export_music.py b/pero_ocr/music/export_music.py index b3a3807..f895db0 100644 --- a/pero_ocr/music/export_music.py +++ b/pero_ocr/music/export_music.py @@ -26,7 +26,7 @@ from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine from pero_ocr.layout_engines.layout_helpers import split_page_layout_by_categories from pero_ocr.music.music_structures import Measure -from pero_ocr.music.music_translator import MusicTranslator as Translator +from pero_ocr.music.output_translator import OutputTranslator as Translator def parseargs(): @@ -149,11 +149,11 @@ def export_page_layout(self, page_layout: PageLayout, file_id: str = None) -> No if self.export_midi: self.export_to_midi(score, parts, file_id) - def get_output_file(self, file_id: str=None, extension: str = 'musicxml') -> str: + def get_output_file(self, file_id: str = None, extension: str = 'musicxml') -> str: base = self.get_output_file_base(file_id) return f'{base}.{extension}' - def get_output_file_base(self, file_id: str=None) -> str: + def get_output_file_base(self, file_id: str = None) -> str: if not file_id: file_id = os.path.basename(self.input_xml_path) if not file_id: @@ -161,7 +161,7 @@ def get_output_file_base(self, file_id: str=None) -> str: name, *_ = re.split(r'\.', file_id) return os.path.join(self.output_folder, f'{name}') - def export_to_midi(self, score, parts, file_id: str=None): + def export_to_midi(self, score, parts, file_id: str = None): # Export whole score to midi output_file = self.get_output_file(file_id, extension='mid') score.write("midi", output_file) @@ -192,7 +192,6 @@ class ExportMusicLines: """Takes text files with transcriptions as individual lines and exports musicxml file for each one""" def __init__(self, translator: Translator = None, input_files: list[str] = None, output_folder: str = 'output_musicxml', verbose: bool = False): - self.translate_to_longer = translator is not None self.translator = translator self.output_folder = output_folder @@ -226,8 +225,8 @@ def __call__(self): stave_id = match.group(1) labels = match.group(3) - if self.translate_to_longer: - labels = self.translator.translate_line(labels, to_longer=True) + if self.translator is not None: + labels = self.translator.translate_line(labels) output_file_name = os.path.join(self.output_folder, f'{stave_id}.musicxml') parsed_labels = semantic_line_to_music21_score(labels) @@ -275,7 +274,6 @@ class Part: """Represent musical part (part of notation for one instrument/section)""" def __init__(self, translator: Translator = None): - self.translate_to_longer = translator is not None self.translator = translator self.repr_music21 = music.stream.Part([music.instrument.Piano()]) @@ -285,8 +283,8 @@ def __init__(self, translator: Translator = None): def add_textline(self, line: TextLine) -> None: labels = line.transcription - if self.translate_to_longer: - labels = self.translator.translate_line(labels, to_longer=True) + if self.translator is not None: + labels = self.translator.translate_line(labels) self.labels.append(labels) new_measures = parse_semantic_to_measures(labels) diff --git a/pero_ocr/music/music_translator.py b/pero_ocr/music/output_translator.py similarity index 50% rename from pero_ocr/music/music_translator.py rename to pero_ocr/music/output_translator.py index 23e5d41..8e6e7a1 100644 --- a/pero_ocr/music/music_translator.py +++ b/pero_ocr/music/output_translator.py @@ -8,36 +8,38 @@ logger = logging.getLogger(__name__) -class MusicTranslator: - """MusicTranslator class for translating shorter SSemantic encoding to Semantic encoding using dictionary.""" +class OutputTranslator: + """Class for translating output from shorter form to longer form using simple dictionary. + + Used for example in Optical Music Recognition to translate shorter SSemantic encoding to Semantic encoding.""" def __init__(self, dictionary: dict = None, filename: str = None): self.dictionary = self.load_dictionary(dictionary, filename) self.dictionary_reversed = {v: k for k, v in self.dictionary.items()} self.n_existing_labels = set() - def __call__(self, inputs: Union[str, list], to_longer: bool = True) -> Union[str, list]: + def __call__(self, inputs: Union[str, list], reverse: bool = False) -> Union[str, list]: if isinstance(inputs, list): if len(inputs[0]) > 1: # list of strings (lines) - return self.translate_lines(inputs, to_longer) + return self.translate_lines(inputs, reverse) else: # list of chars (one line total) - return self.translate_line(''.join(inputs), to_longer) + return self.translate_line(''.join(inputs), reverse) elif isinstance(inputs, str): # one line - return self.translate_line(inputs, to_longer) + return self.translate_line(inputs, reverse) else: - raise ValueError(f'MusicTranslator: Unsupported input type: {type(inputs)}') + raise ValueError(f'OutputTranslator: Unsupported input type: {type(inputs)}') - def translate_lines(self, lines: list, to_longer: bool = True) -> list: - return [self.translate_line(line, to_longer) for line in lines] + def translate_lines(self, lines: list, reverse: bool = False) -> list: + return [self.translate_line(line, reverse) for line in lines] - def translate_line(self, line, to_longer: bool = True): + def translate_line(self, line, reverse: bool = False): line = line.strip('"').strip() symbols = re.split(r'\s+', line) - converted_symbols = [self.translate_symbol(symbol, to_longer) for symbol in symbols] + converted_symbols = [self.translate_symbol(symbol, reverse) for symbol in symbols] return ' '.join(converted_symbols) - def translate_symbol(self, symbol: str, to_longer: bool = True): - dictionary = self.dictionary_reversed if to_longer else self.dictionary + def translate_symbol(self, symbol: str, reverse: bool = False): + dictionary = self.dictionary_reversed if reverse else self.dictionary try: return dictionary[symbol] @@ -47,18 +49,19 @@ def translate_symbol(self, symbol: str, to_longer: bool = True): logger.info(f'Not existing label: ({symbol})') return '' - def load_dictionary(self, dictionary: dict = None, filename: str = None) -> dict: + @staticmethod + def load_dictionary(dictionary: dict = None, filename: str = None) -> dict: if dictionary is not None: return dictionary elif filename is not None: - return self.read_json(filename) + return OutputTranslator.read_json(filename) else: - raise ValueError('MusicTranslator: Either dictionary or filename must be provided.') + raise ValueError('OutputTranslator: Either dictionary or filename must be provided.') @staticmethod def read_json(filename) -> dict: if not os.path.isfile(filename): - raise FileNotFoundError(f'Translator file ({filename}) not found. Cannot export music.') + raise FileNotFoundError(f'Translator file ({filename}) not found. Cannot translate output.') with open(filename) as f: data = json.load(f) diff --git a/pero_ocr/ocr_engine/line_ocr_engine.py b/pero_ocr/ocr_engine/line_ocr_engine.py index 56cf8b3..52f1d9e 100644 --- a/pero_ocr/ocr_engine/line_ocr_engine.py +++ b/pero_ocr/ocr_engine/line_ocr_engine.py @@ -10,7 +10,7 @@ from .softmax import softmax from pero_ocr.sequence_alignment import levenshtein_distance -from pero_ocr.music.music_translator import MusicTranslator +from pero_ocr.music.output_translator import OutputTranslator class BaseEngineLineOCR(object): @@ -28,9 +28,9 @@ def __init__(self, json_def, device, batch_size=8, model_type="ctc"): self.characters = tuple(self.config['characters']) - self.music_translator = None - if 'music_dictionary' in self.config: - self.music_translator = MusicTranslator(dictionary=self.config['music_dictionary']) + self.output_substitution = None + if 'output_substitution_table' in self.config: + self.output_substitution = OutputTranslator(dictionary=self.config['output_substitution_table']) self.net_name = self.config['net_name'] if "embed_num" in self.config: diff --git a/pero_ocr/ocr_engine/transformer_ocr_engine.py b/pero_ocr/ocr_engine/transformer_ocr_engine.py index 7200fd4..ba34476 100644 --- a/pero_ocr/ocr_engine/transformer_ocr_engine.py +++ b/pero_ocr/ocr_engine/transformer_ocr_engine.py @@ -6,8 +6,6 @@ from .line_ocr_engine import BaseEngineLineOCR from pero_ocr.ocr_engine import transformer -import sys - class TransformerEngineLineOCR(BaseEngineLineOCR): def __init__(self, json_def, device, batch_size=4): @@ -108,8 +106,8 @@ def decode(self, labels): for line_labels in labels: outputs.append(''.join([self.characters[c] for c in line_labels])) - if self.music_translator is not None: - outputs = self.music_translator(outputs, to_longer=True) + if self.output_substitution is not None: + outputs = self.output_substitution(outputs) return outputs From 68e7892473281a60e7e2ec042b12d3bd0f35be68 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Mon, 22 Jan 2024 17:14:44 +0100 Subject: [PATCH 41/76] Add option for rendering region categories for non-text regions. - enable by adding `--output-render-category` argument --- pero_ocr/core/layout.py | 38 ++++++++++++++++++++++++------------ user_scripts/parse_folder.py | 23 ++++++++++++++++------ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index c74c002..0a8ef72 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -852,9 +852,12 @@ def load_logits(self, file: str): line.characters = characters[line.id] line.logit_coords = logit_coords[line.id] - def render_to_image(self, image, thickness: int = 2, circles: bool = True, render_order: bool = False): + def render_to_image(self, image, thickness: int = 2, circles: bool = True, + render_order: bool = False, render_category: bool = False): """Render layout into image. :param image: image to render layout into + :param render_order: render region order number given by enumerate(regions) to the middle of given region + :param render_region_id: render region id to the upper left corner of given region """ for region_layout in self.regions: image = draw_lines( @@ -870,21 +873,30 @@ def render_to_image(self, image, thickness: int = 2, circles: bool = True, rende [region_layout.polygon], color=(255, 0, 0), circles=(circles, circles, circles), close=True, thickness=thickness) - if render_order: + if render_order or render_category: font = cv2.FONT_HERSHEY_DUPLEX - font_scale = 4 - font_thickness = 5 + font_scale = 1 + font_thickness = 1 for idx, region in enumerate(self.regions): - min = region.polygon.min(axis=0) - max = region.polygon.max(axis=0) - - text_w, text_h = cv2.getTextSize(f"{idx}", font, font_scale, font_thickness)[0] - - mid_coords = (int((min[0] + max[0]) // 2 - text_w // 2), int((min[1] + max[1]) // 2 + text_h // 2)) - - cv2.putText(image, f"{idx}", mid_coords, font, font_scale, - (0, 0, 0), thickness=font_thickness, lineType=cv2.LINE_AA) + min_p = region.polygon.min(axis=0) + max_p = region.polygon.max(axis=0) + + if render_order: + text = f"{idx}" + text_w, text_h = cv2.getTextSize(text, font, font_scale, font_thickness)[0] + mid_x = int((min_p[0] + max_p[0]) // 2 - text_w // 2) + mid_y = int((min_p[1] + max_p[1]) // 2 + text_h // 2) + cv2.putText(image, text, (mid_x, mid_y), font, font_scale, + color=(0, 0, 0), thickness=font_thickness, lineType=cv2.LINE_AA) + if render_category and region.category not in [None, 'text']: + text = f"{region.category}" + text_w, text_h = cv2.getTextSize(text, font, font_scale, font_thickness)[0] + start_point = (int(min_p[0]), int(min_p[1])) + end_point = (int(min_p[0]) + text_w, int(min_p[1]) - text_h) + cv2.rectangle(image, start_point, end_point, color=(255, 0, 0), thickness=-1) + cv2.putText(image, text, start_point, font, font_scale, + color=(255, 255, 255), thickness=font_thickness, lineType=cv2.LINE_AA) return image diff --git a/user_scripts/parse_folder.py b/user_scripts/parse_folder.py index a20ed82..694b5a5 100644 --- a/user_scripts/parse_folder.py +++ b/user_scripts/parse_folder.py @@ -33,6 +33,8 @@ def parse_arguments(): parser.add_argument('--input-logit-path', help='') parser.add_argument('--output-xml-path', help='') parser.add_argument('--output-render-path', help='') + parser.add_argument('--output-render-category', default=False, action='store_true', + help='Render category tags for every non-text region.') parser.add_argument('--output-line-path', help='') parser.add_argument('--output-logit-path', help='') parser.add_argument('--output-alto-path', help='') @@ -57,9 +59,12 @@ def setup_logging(config): logger.setLevel(level) -def get_value_or_none(config, section, key): +def get_value_or_none(config, section, key, getboolean: bool = False): if config.has_option(section, key): - value = config[section][key] + if getboolean: + value = config.getboolean(section, key) + else: + value = config[section][key] else: value = None return value @@ -139,12 +144,13 @@ def __call__(self, page_layout: PageLayout, file_id): class Computator: def __init__(self, page_parser, input_image_path, input_xml_path, input_logit_path, output_render_path, - output_logit_path, output_alto_path, output_xml_path, output_line_path): + output_render_category, output_logit_path, output_alto_path, output_xml_path, output_line_path): self.page_parser = page_parser self.input_image_path = input_image_path self.input_xml_path = input_xml_path self.input_logit_path = input_logit_path self.output_render_path = output_render_path + self.output_render_category = output_render_category self.output_logit_path = output_logit_path self.output_alto_path = output_alto_path self.output_xml_path = output_xml_path @@ -177,8 +183,9 @@ def __call__(self, image_file_name, file_id, index, ids_count): os.path.join(self.output_xml_path, file_id + '.xml')) if self.output_render_path is not None: - page_layout.render_to_image(image) - cv2.imwrite(os.path.join(self.output_render_path, file_id + '.jpg'), image, [int(cv2.IMWRITE_JPEG_QUALITY), 70]) + page_layout.render_to_image(image, render_category=self.output_render_category) + render_file = str(os.path.join(self.output_render_path, file_id + '.jpg')) + cv2.imwrite(render_file, image, [int(cv2.IMWRITE_JPEG_QUALITY), 70]) if self.output_logit_path is not None: page_layout.save_logits(os.path.join(self.output_logit_path, file_id + '.logits')) @@ -247,6 +254,8 @@ def main(): config['PARSE_FOLDER']['OUTPUT_XML_PATH'] = args.output_xml_path if args.output_render_path is not None: config['PARSE_FOLDER']['OUTPUT_RENDER_PATH'] = args.output_render_path + if args.output_render_category is not None: + config['PARSE_FOLDER']['OUTPUT_RENDER_CATEGORY'] = 'yes' if args.output_render_category else 'no' if args.output_line_path is not None: config['PARSE_FOLDER']['OUTPUT_LINE_PATH'] = args.output_line_path if args.output_logit_path is not None: @@ -266,6 +275,7 @@ def main(): input_logit_path = get_value_or_none(config, 'PARSE_FOLDER', 'INPUT_LOGIT_PATH') output_render_path = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_RENDER_PATH') + output_render_category = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_RENDER_CATEGORY', True) output_line_path = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_LINE_PATH') output_xml_path = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_XML_PATH') output_logit_path = get_value_or_none(config, 'PARSE_FOLDER', 'OUTPUT_LOGIT_PATH') @@ -326,7 +336,8 @@ def main(): images_to_process = filtered_images_to_process computator = Computator(page_parser, input_image_path, input_xml_path, input_logit_path, output_render_path, - output_logit_path, output_alto_path, output_xml_path, output_line_path) + output_render_category, output_logit_path, output_alto_path, output_xml_path, + output_line_path) t_start = time.time() results = [] From 8ac485e088ed2c560a2fddb982e3f0f34cc7cebc Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Mon, 22 Jan 2024 17:49:31 +0100 Subject: [PATCH 42/76] Tiny refactor before moving. --- pero_ocr/music/export_music.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pero_ocr/music/export_music.py b/pero_ocr/music/export_music.py index f895db0..dcb7e42 100644 --- a/pero_ocr/music/export_music.py +++ b/pero_ocr/music/export_music.py @@ -2,11 +2,18 @@ """Script to take output of pero-ocr with musical transcriptions and export it to musicxml and MIDI formats. INPUTS: -- XML PageLayout (exported directly from pero-ocr engine) using `--input-xml-path` argument +- PageLayout + - INPUT options: + - PageLayout object using `ExportMusicPage.__call__()` method + - XML PageLayout (exported directly from pero-ocr engine) using `--input-xml-path` argument - Represents one whole page of musical notation transcribed by pero-ocr engine - - OUTPUTS one musicxml file for the page - - + MIDI file for page and for individual lines (named according to IDs in PageLayout) + - OUTPUT options: + - One musicxml file for the page + - MIDI file for page and for individual lines (named according to IDs in PageLayout) - Text files with individual transcriptions and their IDs on each line using `--input-transcription-files` argument. + - e.g.: 2370961.png ">2 + kGM + E2W E3q. + |" + 1300435.png "=4 + kDM + G3z + F3z + |" + ... - OUTPUTS one musicxml file for each line with names corresponding to IDs in each line Author: Vojtěch Vlach @@ -59,7 +66,7 @@ def parseargs(): "Exports whole file as one MusicXML.")) parser.add_argument( '-v', "--verbose", action='store_true', default=False, - help="Activate verbose logging.") + help="Enable verbose logging.") return parser.parse_args() @@ -190,8 +197,8 @@ def regions_to_parts(self, regions: list[RegionLayout]) -> list[Part]: class ExportMusicLines: """Takes text files with transcriptions as individual lines and exports musicxml file for each one""" - def __init__(self, translator: Translator = None, input_files: list[str] = None, - output_folder: str = 'output_musicxml', verbose: bool = False): + def __init__(self, input_files: list[str] = None, output_folder: str = 'output_musicxml', + translator: Translator = None, verbose: bool = False): self.translator = translator self.output_folder = output_folder From 7032389da49d9adcb23565cbd87bfeae4c233b16 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Mon, 22 Jan 2024 18:10:41 +0100 Subject: [PATCH 43/76] Add minimalistic CLI for `MusicPageExporter` to `user_scripts/export_music.py`. --- .../{export_music.py => music_exporter.py} | 100 ++---------------- user_scripts/export_music.py | 85 +++++++++++++++ 2 files changed, 96 insertions(+), 89 deletions(-) rename pero_ocr/music/{export_music.py => music_exporter.py} (77%) create mode 100644 user_scripts/export_music.py diff --git a/pero_ocr/music/export_music.py b/pero_ocr/music/music_exporter.py similarity index 77% rename from pero_ocr/music/export_music.py rename to pero_ocr/music/music_exporter.py index dcb7e42..b68daaf 100644 --- a/pero_ocr/music/export_music.py +++ b/pero_ocr/music/music_exporter.py @@ -1,31 +1,7 @@ -#!/usr/bin/env python3.8 -"""Script to take output of pero-ocr with musical transcriptions and export it to musicxml and MIDI formats. - -INPUTS: -- PageLayout - - INPUT options: - - PageLayout object using `ExportMusicPage.__call__()` method - - XML PageLayout (exported directly from pero-ocr engine) using `--input-xml-path` argument - - Represents one whole page of musical notation transcribed by pero-ocr engine - - OUTPUT options: - - One musicxml file for the page - - MIDI file for page and for individual lines (named according to IDs in PageLayout) -- Text files with individual transcriptions and their IDs on each line using `--input-transcription-files` argument. - - e.g.: 2370961.png ">2 + kGM + E2W E3q. + |" - 1300435.png "=4 + kDM + G3z + F3z + |" - ... - - OUTPUTS one musicxml file for each line with names corresponding to IDs in each line - -Author: Vojtěch Vlach -Contact: xvlach22@vutbr.cz -""" - from __future__ import annotations import sys -import argparse import os import re -import time import logging import music21 as music @@ -36,61 +12,11 @@ from pero_ocr.music.output_translator import OutputTranslator as Translator -def parseargs(): - print(' '.join(sys.argv)) - print('----------------------------------------------------------------------') - parser = argparse.ArgumentParser() - - parser.add_argument( - "-i", "--input-xml-path", type=str, default='', - help="Path to input XML file with exported PageLayout.") - parser.add_argument( - '-f', '--input-transcription-files', nargs='*', default=None, - help='Input files with sequences as lines with IDs at the beginning.') - parser.add_argument( - "-t", "--translator-path", type=str, default=None, - help="JSON File containing translation dictionary from shorter encoding (exported by model) to longest " - "Check if needed by seeing start of any line in the transcription." - "(e.g. SSemantic (model output): >2 + kGM + B3z + C4z + |..." - " Semantic (stored in XML): clef-G2 + keySignature-GM + note-B3_eighth + note-C4_eighth + barline...") - parser.add_argument( - "-o", "--output-folder", default='output_page', - help="Set output file with extension. Output format is JSON") - parser.add_argument( - "-m", "--export-midi", action='store_true', - help=("Enable exporting midi file to output_folder." - "Exports whole file and individual lines with names corresponding to them TextLine IDs.")) - parser.add_argument( - "-M", "--export-musicxml", action='store_true', - help=("Enable exporting musicxml file to output_folder." - "Exports whole file as one MusicXML.")) - parser.add_argument( - '-v', "--verbose", action='store_true', default=False, - help="Enable verbose logging.") - - return parser.parse_args() - - -def main(): - """Main function for simple testing""" - args = parseargs() - - start = time.time() - ExportMusicPage( - input_xml_path=args.input_xml_path, - input_transcription_files=args.input_transcription_files, - translator_path=args.translator_path, - output_folder=args.output_folder, - export_midi=args.export_midi, - export_musicxml=args.export_musicxml, - verbose=args.verbose)() - - end = time.time() - print(f'Total time: {end - start:.2f} s') - - -class ExportMusicPage: - """Take pageLayout XML exported from pero-ocr with transcriptions and re-construct page of musical notation.""" +class MusicPageExporter: + """Take pageLayout XML exported from pero-ocr with transcriptions and re-construct page of musical notation. + + For CLI usage see user_scripts/music_exporter.py + """ def __init__(self, input_xml_path: str = '', input_transcription_files: list[str] = None, translator_path: str = None, output_folder: str = 'output_page', export_midi: bool = False, @@ -119,8 +45,8 @@ def __init__(self, input_xml_path: str = '', input_transcription_files: list[str def __call__(self, page_layout=None) -> None: if self.input_transcription_files: - ExportMusicLines(input_files=self.input_transcription_files, output_folder=self.output_folder, - translator=self.translator, verbose=self.verbose)() + MusicLinesExporter(input_files=self.input_transcription_files, output_folder=self.output_folder, + translator=self.translator, verbose=self.verbose)() if page_layout: self.process_page(page_layout) @@ -195,7 +121,7 @@ def regions_to_parts(self, regions: list[RegionLayout]) -> list[Part]: return parts -class ExportMusicLines: +class MusicLinesExporter: """Takes text files with transcriptions as individual lines and exports musicxml file for each one""" def __init__(self, input_files: list[str] = None, output_folder: str = 'output_musicxml', translator: Translator = None, verbose: bool = False): @@ -209,8 +135,8 @@ def __init__(self, input_files: list[str] = None, output_folder: str = 'output_m logging.debug('Hello World! (from ReverseConverter)') - self.input_files = ExportMusicLines.get_input_files(input_files) - ExportMusicLines.prepare_output_folder(output_folder) + self.input_files = MusicLinesExporter.get_input_files(input_files) + MusicLinesExporter.prepare_output_folder(output_folder) def __call__(self): if not self.input_files: @@ -220,7 +146,7 @@ def __call__(self): # For every file, convert it to MusicXML for input_file_name in self.input_files: logging.info(f'Reading file {input_file_name}') - lines = ExportMusicLines.read_file_lines(input_file_name) + lines = MusicLinesExporter.read_file_lines(input_file_name) for i, line in enumerate(lines): match = re.fullmatch(r'([a-zA-Z0-9_\-]+)[a-zA-Z0-9_\.]+\s+([0-9]+\s+)?\"([\S\s]+)\"', line) @@ -402,7 +328,3 @@ def write_to_file(output_file_name, xml): f.write(xml) logging.info(f'File {output_file_name} successfully written.') - - -if __name__ == "__main__": - main() diff --git a/user_scripts/export_music.py b/user_scripts/export_music.py new file mode 100644 index 0000000..5a2f4a4 --- /dev/null +++ b/user_scripts/export_music.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3.8 +# -*- coding: utf-8 -*- +"""Script to take output of pero-ocr with musical transcriptions and export it to musicxml and MIDI formats. + +INPUTS: +- PageLayout + - INPUT options: + - PageLayout object using `ExportMusicPage.__call__()` method + - XML PageLayout (exported directly from pero-ocr engine) using `--input-xml-path` argument + - Represents one whole page of musical notation transcribed by pero-ocr engine + - OUTPUT options: + - One musicxml file for the page + - MIDI file for page and for individual lines (named according to IDs in PageLayout) +- Text files with individual transcriptions and their IDs on each line using `--input-transcription-files` argument. + - e.g.: 2370961.png ">2 + kGM + E2W E3q. + |" + 1300435.png "=4 + kDM + G3z + F3z + |" + ... + - OUTPUTS one musicxml file for each line with names corresponding to IDs in each line + +Author: Vojtěch Vlach +Contact: xvlach22@vutbr.cz +""" + +import sys +import argparse +import time + +from pero_ocr.music.music_exporter import MusicPageExporter + + +def parseargs(): + print(' '.join(sys.argv)) + print('----------------------------------------------------------------------') + parser = argparse.ArgumentParser() + + parser.add_argument( + "-i", "--input-xml-path", type=str, default='', + help="Path to input XML file with exported PageLayout.") + parser.add_argument( + '-f', '--input-transcription-files', nargs='*', default=None, + help='Input files with sequences as lines with IDs at the beginning.') + parser.add_argument( + "-t", "--translator-path", type=str, default=None, + help="JSON File containing translation dictionary from shorter encoding (exported by model) to longest " + "Check if needed by seeing start of any line in the transcription." + "(e.g. SSemantic (model output): >2 + kGM + B3z + C4z + |..." + " Semantic (stored in XML): clef-G2 + keySignature-GM + note-B3_eighth + note-C4_eighth + barline...") + parser.add_argument( + "-o", "--output-folder", default='output_page', + help="Set output file with extension. Output format is JSON") + parser.add_argument( + "-m", "--export-midi", action='store_true', + help=("Enable exporting midi file to output_folder." + "Exports whole file and individual lines with names corresponding to them TextLine IDs.")) + parser.add_argument( + "-M", "--export-musicxml", action='store_true', + help=("Enable exporting musicxml file to output_folder." + "Exports whole file as one MusicXML.")) + parser.add_argument( + '-v', "--verbose", action='store_true', default=False, + help="Enable verbose logging.") + + return parser.parse_args() + + +def main(): + """Main function for simple testing""" + args = parseargs() + + start = time.time() + MusicPageExporter( + input_xml_path=args.input_xml_path, + input_transcription_files=args.input_transcription_files, + translator_path=args.translator_path, + output_folder=args.output_folder, + export_midi=args.export_midi, + export_musicxml=args.export_musicxml, + verbose=args.verbose)() + + end = time.time() + print(f'Total time: {end - start:.2f} s') + + +if __name__ == "__main__": + main() From ef5f36ffe09bbef5cfaa4f2db55432cf0ee87ecb Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Tue, 23 Jan 2024 14:08:21 +0100 Subject: [PATCH 44/76] Normalize category characters in image rendering. --- pero_ocr/core/layout.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 0a8ef72..b08a0e8 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -6,6 +6,7 @@ from datetime import datetime, timezone from enum import Enum from typing import Optional, Union +import unicodedata import numpy as np import lxml.etree as ET @@ -890,7 +891,7 @@ def render_to_image(self, image, thickness: int = 2, circles: bool = True, cv2.putText(image, text, (mid_x, mid_y), font, font_scale, color=(0, 0, 0), thickness=font_thickness, lineType=cv2.LINE_AA) if render_category and region.category not in [None, 'text']: - text = f"{region.category}" + text = f"{normalize_text(region.category)}" text_w, text_h = cv2.getTextSize(text, font, font_scale, font_thickness)[0] start_point = (int(min_p[0]), int(min_p[1])) end_point = (int(min_p[0]) + text_w, int(min_p[1]) - text_h) @@ -1074,3 +1075,7 @@ def create_ocr_processing_element(id: str = "IdOcr", return ocr_processing + +def normalize_text(text: str) -> str: + """Normalize text to ASCII characters. (e.g. Obrázek -> Obrazek)""" + return unicodedata.normalize('NFD', text).encode('ascii', 'ignore').decode('ascii') From 8c738ee0a7d7ea619162a79ba64d575c4fa1b7bf Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Tue, 23 Jan 2024 17:27:15 +0100 Subject: [PATCH 45/76] Add confidence estimation to PageOCR directly after detection. Update TextLine if new confidence is higher. - move `output_substitution` to PageOCR after confidence estimation. - add `TextLine.get_labels` for easier confidence estimation. - add `get_line_confidence_median` to save literally one line of code later... --- pero_ocr/core/confidence_estimation.py | 8 ++++- pero_ocr/core/layout.py | 33 +++++++++++-------- pero_ocr/document_ocr/page_parser.py | 28 +++++++++++++--- pero_ocr/ocr_engine/transformer_ocr_engine.py | 4 --- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/pero_ocr/core/confidence_estimation.py b/pero_ocr/core/confidence_estimation.py index 921da88..31ebe0c 100644 --- a/pero_ocr/core/confidence_estimation.py +++ b/pero_ocr/core/confidence_estimation.py @@ -69,10 +69,16 @@ def squeeze(sequence): return result +def get_line_confidence_median(line, labels=None, aligned_letters=None, log_probs=None): + confidences = get_line_confidence(line, labels, aligned_letters, log_probs) + return np.quantile(confidences, .50) -def get_line_confidence(line, labels, aligned_letters=None, log_probs=None): +def get_line_confidence(line, labels=None, aligned_letters=None, log_probs=None): # There is the same number of outputs as labels (probably transformer model was used) --> each letter has only one # possible frame in logits and thus it is not needed to align them + if labels is None: + labels = line.get_labels() + if line.logits.shape[0] == len(labels): return get_line_confidence_transformer(line, labels) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index b08a0e8..8268bc7 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -207,6 +207,23 @@ def to_altoxml(self, text_block, arabic_helper, min_line_confidence): if self.transcription_confidence is not None: string.set("WC", str(round(self.transcription_confidence, 2))) + def get_labels(self): + chars = [i for i in range(len(self.characters))] + char_to_num = dict(zip(self.characters, chars)) + + blank_idx = self.logits.shape[1] - 1 + + labels = [] + for item in self.transcription: + if item in char_to_num.keys(): + if char_to_num[item] >= blank_idx: + labels.append(0) + else: + labels.append(char_to_num[item]) + else: + labels.append(0) + return np.array(labels) + def to_altoxml_text(self, text_line, arabic_helper, text_line_height, text_line_width, text_line_vpos, text_line_hpos): arabic_line = False @@ -214,27 +231,15 @@ def to_altoxml_text(self, text_line, arabic_helper, arabic_line = True try: - chars = [i for i in range(len(self.characters))] - char_to_num = dict(zip(self.characters, chars)) - + label = self.get_labels() blank_idx = self.logits.shape[1] - 1 - label = [] - for item in self.transcription: - if item in char_to_num.keys(): - if char_to_num[item] >= blank_idx: - label.append(0) - else: - label.append(char_to_num[item]) - else: - label.append(0) - logits = self.get_dense_logits()[self.logit_coords[0]:self.logit_coords[1]] logprobs = self.get_full_logprobs()[self.logit_coords[0]:self.logit_coords[1]] aligned_letters = align_text(-logprobs, np.array(label), blank_idx) except (ValueError, IndexError, TypeError) as e: logger.warning(f'Error: Alto export, unable to align line {self.id} due to exception: {e}.') - self.transcription_confidence = 0 + self.transcription_confidence = 0.0 average_word_width = (text_line_hpos + text_line_width) / len(self.transcription.split()) for w, word in enumerate(self.transcription.split()): string = ET.SubElement(text_line, "String") diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index f015b25..e824a3c 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -12,6 +12,7 @@ from pero_ocr.utils import compose_path, config_get_list from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine import pero_ocr.core.crop_engine as cropper +from pero_ocr.core.confidence_estimation import get_line_confidence_median from pero_ocr.ocr_engine.pytorch_ocr_engine import PytorchEngineLineOCR from pero_ocr.ocr_engine.transformer_ocr_engine import TransformerEngineLineOCR from pero_ocr.layout_engines.simple_region_engine import SimpleThresholdRegion @@ -528,12 +529,31 @@ def process_page(self, img, page_layout: PageLayout): transcriptions, logits, logit_coords = self.ocr_engine.process_lines([line.crop for line in lines_to_process]) for line, line_transcription, line_logits, line_logit_coords in zip(lines_to_process, transcriptions, logits, logit_coords): - line.transcription = line_transcription - line.logits = line_logits - line.characters = self.ocr_engine.characters - line.logit_coords = line_logit_coords + new_line = TextLine(transcription=line_transcription, + logits=line_logits, + characters=self.ocr_engine.characters, + logit_coords=line_logit_coords) + + try: + new_line.transcription_confidence = get_line_confidence_median(new_line) if line_transcription else 0.0 + except ValueError as e: + logger.warning(f'Error: PageOCR is unable to get confidence of line {self.id} due to exception: {e}.') + new_line.transcription_confidence = 0.0 + + if (line.transcription_confidence is None or + line.transcription_confidence < new_line.transcription_confidence): + line.transcription = self.substitute_transcription(line_transcription) + line.logits = line_logits + line.characters = self.ocr_engine.characters + line.logit_coords = line_logit_coords + line.transcription_confidence = new_line.transcription_confidence return page_layout + def substitute_transcription(self, transcription): + if self.ocr_engine.output_substitution is not None: + return self.ocr_engine.output_substitution(transcription) + else: + return transcription def get_prob(best_ids, best_probs): last_id = -1 diff --git a/pero_ocr/ocr_engine/transformer_ocr_engine.py b/pero_ocr/ocr_engine/transformer_ocr_engine.py index ba34476..4bd0295 100644 --- a/pero_ocr/ocr_engine/transformer_ocr_engine.py +++ b/pero_ocr/ocr_engine/transformer_ocr_engine.py @@ -105,9 +105,5 @@ def decode(self, labels): outputs = [] for line_labels in labels: outputs.append(''.join([self.characters[c] for c in line_labels])) - - if self.output_substitution is not None: - outputs = self.output_substitution(outputs) - return outputs From 949829ac5271b2feaf84f98d251fd9757124c819 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 26 Jan 2024 16:19:11 +0100 Subject: [PATCH 46/76] Add `PageOCR.get_line_confidence` solving problem of wrong confidence estimation. Problem was that confidence was estimated using `confidence_estimation.py/get_line_confidence` without cutting `log_probs` according to `line.logit_coords`. New method solves it. --- pero_ocr/core/confidence_estimation.py | 7 ++---- pero_ocr/core/layout.py | 2 +- pero_ocr/document_ocr/page_parser.py | 30 ++++++++++++++++++-------- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/pero_ocr/core/confidence_estimation.py b/pero_ocr/core/confidence_estimation.py index 31ebe0c..c5176cd 100644 --- a/pero_ocr/core/confidence_estimation.py +++ b/pero_ocr/core/confidence_estimation.py @@ -69,13 +69,10 @@ def squeeze(sequence): return result -def get_line_confidence_median(line, labels=None, aligned_letters=None, log_probs=None): - confidences = get_line_confidence(line, labels, aligned_letters, log_probs) - return np.quantile(confidences, .50) def get_line_confidence(line, labels=None, aligned_letters=None, log_probs=None): # There is the same number of outputs as labels (probably transformer model was used) --> each letter has only one - # possible frame in logits and thus it is not needed to align them + # possible frame in logits thus it is not needed to align them if labels is None: labels = line.get_labels() @@ -106,7 +103,7 @@ def get_line_confidence(line, labels=None, aligned_letters=None, log_probs=None) confidences[i] = max(0, label_prob - other_prob) last_border = next_border - #confidences = confidences / 2 + 0.5 + # confidences = confidences / 2 + 0.5 return confidences diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 8268bc7..a263da3 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -447,7 +447,7 @@ def from_pagexml(cls, region_element: ET.SubElement, schema): return layout_region def to_altoxml(self, print_space, arabic_helper, min_line_confidence, print_space_coords: (int, int, int, int) - ) -> (int, int, int, int): + ) -> (int, int, int, int): print_space_height, print_space_width, print_space_vpos, print_space_hpos = print_space_coords text_block = ET.SubElement(print_space, "TextBlock") diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index e824a3c..f0e1387 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -12,7 +12,7 @@ from pero_ocr.utils import compose_path, config_get_list from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine import pero_ocr.core.crop_engine as cropper -from pero_ocr.core.confidence_estimation import get_line_confidence_median +from pero_ocr.core.confidence_estimation import get_line_confidence from pero_ocr.ocr_engine.pytorch_ocr_engine import PytorchEngineLineOCR from pero_ocr.ocr_engine.transformer_ocr_engine import TransformerEngineLineOCR from pero_ocr.layout_engines.simple_region_engine import SimpleThresholdRegion @@ -528,17 +528,14 @@ def process_page(self, img, page_layout: PageLayout): transcriptions, logits, logit_coords = self.ocr_engine.process_lines([line.crop for line in lines_to_process]) - for line, line_transcription, line_logits, line_logit_coords in zip(lines_to_process, transcriptions, logits, logit_coords): - new_line = TextLine(transcription=line_transcription, + for line, line_transcription, line_logits, line_logit_coords in zip(lines_to_process, transcriptions, + logits, logit_coords): + new_line = TextLine(id=line.id, + transcription=line_transcription, logits=line_logits, characters=self.ocr_engine.characters, logit_coords=line_logit_coords) - - try: - new_line.transcription_confidence = get_line_confidence_median(new_line) if line_transcription else 0.0 - except ValueError as e: - logger.warning(f'Error: PageOCR is unable to get confidence of line {self.id} due to exception: {e}.') - new_line.transcription_confidence = 0.0 + new_line.transcription_confidence = self.get_line_confidence(new_line) if (line.transcription_confidence is None or line.transcription_confidence < new_line.transcription_confidence): @@ -555,6 +552,21 @@ def substitute_transcription(self, transcription): else: return transcription + @staticmethod + def get_line_confidence(line): + default_confidence = 0.0 + + if line.transcription: + try: + log_probs = line.get_full_logprobs()[line.logit_coords[0]:line.logit_coords[1]] + confidences = get_line_confidence(line, log_probs=log_probs) + return np.quantile(confidences, .50) + except ValueError as e: + logger.warning(f'Error: PageOCR is unable to get confidence of line {line.id} due to exception: {e}.') + return default_confidence + return default_confidence + + def get_prob(best_ids, best_probs): last_id = -1 last_prob = 1 From 0b6e933c85ed2022ce2786c16d32ee3b4cf032bf Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 26 Jan 2024 16:53:44 +0100 Subject: [PATCH 47/76] Unify logging style. --- pero_ocr/document_ocr/page_parser.py | 10 ++++++---- pero_ocr/layout_engines/layout_helpers.py | 1 - pero_ocr/layout_engines/naive_sorter.py | 3 --- pero_ocr/layout_engines/smart_sorter.py | 3 --- pero_ocr/utils.py | 2 +- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index f0e1387..593ec4b 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -450,7 +450,7 @@ def __init__(self, config, config_path=''): def process_page(self, img, page_layout: PageLayout): if not page_layout.regions: - logger.warning(f"Skipping line post processing for page {page_layout.id}. No text region present.") + print(f"Warning: Skipping line post processing for page {page_layout.id}. No text region present.") return page_layout for region in page_layout.regions: @@ -465,7 +465,7 @@ def __init__(self, config, config_path=''): def process_page(self, img, page_layout: PageLayout): if not page_layout.regions: - logger.warning(f"Skipping layout post processing for page {page_layout.id}. No text region present.") + print(f"Warning: Skipping layout post processing for page {page_layout.id}. No text region present.") return page_layout if self.retrace_regions: @@ -492,7 +492,8 @@ def process_page(self, img, page_layout: PageLayout): except ValueError: line.crop = np.zeros( (self.crop_engine.line_height, self.crop_engine.line_height, 3)) - logger.warning(f"Failed to crop line {line.id} in page {page_layout.id}. Probably contain vertical line. Contanct Olda Kodym to fix this bug!") + print(f"WARNING: Failed to crop line {line.id} in page {page_layout.id}. " + f"Probably contain vertical line. Contanct Olda Kodym to fix this bug!") return page_layout def crop_lines(self, img, lines: list): @@ -503,7 +504,8 @@ def crop_lines(self, img, lines: list): except ValueError: line.crop = np.zeros( (self.crop_engine.line_height, self.crop_engine.line_height, 3)) - logger.warning(f"Failed to crop line {line.id}. Probably contain vertical line. Contanct Olda Kodym to fix this bug!") + print(f"WARNING: Failed to crop line {line.id}. Probably contain vertical line. " + f"Contanct Olda Kodym to fix this bug!") class PageOCR(object): diff --git a/pero_ocr/layout_engines/layout_helpers.py b/pero_ocr/layout_engines/layout_helpers.py index a122988..bd795ff 100644 --- a/pero_ocr/layout_engines/layout_helpers.py +++ b/pero_ocr/layout_engines/layout_helpers.py @@ -14,7 +14,6 @@ from pero_ocr.core.layout import PageLayout, RegionLayout, TextLine -logger = logging.getLogger(__name__) def check_line_position(baseline, page_size, margin=20, min_ratio=0.125): """Checks if line is short and very close to the page edge, which may indicate that the region actually belongs to diff --git a/pero_ocr/layout_engines/naive_sorter.py b/pero_ocr/layout_engines/naive_sorter.py index 4b9372d..3cd5932 100644 --- a/pero_ocr/layout_engines/naive_sorter.py +++ b/pero_ocr/layout_engines/naive_sorter.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import numpy as np -import logging from configparser import SectionProxy from sklearn.cluster import DBSCAN @@ -11,8 +10,6 @@ from pero_ocr.core.layout import PageLayout, RegionLayout from pero_ocr.layout_engines import layout_helpers as helpers -logger = logging.getLogger(__name__) - class Region: def __init__(self, region_layout: RegionLayout): diff --git a/pero_ocr/layout_engines/smart_sorter.py b/pero_ocr/layout_engines/smart_sorter.py index eedfb1d..03b585c 100644 --- a/pero_ocr/layout_engines/smart_sorter.py +++ b/pero_ocr/layout_engines/smart_sorter.py @@ -3,7 +3,6 @@ import cv2 import math import numpy as np -import logging from configparser import SectionProxy from copy import deepcopy @@ -15,8 +14,6 @@ from pero_ocr.utils import config_get_list from pero_ocr.layout_engines import layout_helpers as helpers -logger = logging.getLogger(__name__) - def pairwise(iterable): "s -> (s0,s1), (s1,s2), (s2, s3), ..." diff --git a/pero_ocr/utils.py b/pero_ocr/utils.py index 669f293..47b856d 100644 --- a/pero_ocr/utils.py +++ b/pero_ocr/utils.py @@ -37,7 +37,7 @@ def config_get_list(config, key, fallback=None): try: value = json.loads(config[key]) except json.decoder.JSONDecodeError as e: - logger.warning(f'Failed to parse list from config key "{key}", returning fallback {fallback}:\n{e}') + logger.info(f'Failed to parse list from config key "{key}", returning fallback {fallback}:\n{e}') return fallback else: return value From 092b04fd7af1f6ed6e111ed4cc80bcf2a7390474 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 26 Jan 2024 16:54:41 +0100 Subject: [PATCH 48/76] Update readme, remove translator.Semantic_to_SSemantic.json because it was moved to ocr/omr.json (OCR engine config jon). --- pero_ocr/music/README.md | 3 ++- pero_ocr/music/translator.Semantic_to_SSemantic.json | 12 ------------ 2 files changed, 2 insertions(+), 13 deletions(-) delete mode 100644 pero_ocr/music/translator.Semantic_to_SSemantic.json diff --git a/pero_ocr/music/README.md b/pero_ocr/music/README.md index 3b4a0cb..e790b2c 100644 --- a/pero_ocr/music/README.md +++ b/pero_ocr/music/README.md @@ -1,6 +1,7 @@ # README.md -This folder contains scripts for exporting transcribed musical pages. +This folder contains scripts for exporting transcribed musical pages. +CLI tool with documentation is in `user_scripts/export_music.py`. Main functionality is in `export_music.py/ExportMusicPage`. For older versions of these files, see [github.com/vlachvojta/polyphonic-omr-by-sachindae](https://github.com/vlachvojta/polyphonic-omr-by-sachindae/tree/main/reverse_converter) diff --git a/pero_ocr/music/translator.Semantic_to_SSemantic.json b/pero_ocr/music/translator.Semantic_to_SSemantic.json deleted file mode 100644 index 38c40ce..0000000 --- a/pero_ocr/music/translator.Semantic_to_SSemantic.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "+": "+", - "barline": "|", - "clef-C1": "<1", "clef-C2": "<2", "clef-C3": "<3", "clef-C4": "<4", "clef-C5": "<5", "clef-F3": "=3", "clef-F4": "=4", "clef-F5": "=5", "clef-G1": ">1", "clef-G2": ">2", - "gracenote-A#3_eighth": "a#3z", "gracenote-A#3_sixteenth": "a#3S", "gracenote-A#4_eighth": "a#4z", "gracenote-A#4_half": "a#4H", "gracenote-A#4_quarter": "a#4q", "gracenote-A#4_sixteenth": "a#4S", "gracenote-A#4_thirty_second": "a#4T", "gracenote-A#5_eighth": "a#5z", "gracenote-A2_eighth": "a2z", "gracenote-A2_quarter": "a2q", "gracenote-A2_sixteenth": "a2S", "gracenote-A3_eighth": "a3z", "gracenote-A3_half": "a3H", "gracenote-A3_quarter": "a3q", "gracenote-A3_sixteenth": "a3S", "gracenote-A3_thirty_second": "a3T", "gracenote-A4_eighth": "a4z", "gracenote-A4_half": "a4H", "gracenote-A4_quarter": "a4q", "gracenote-A4_sixteenth": "a4S", "gracenote-A4_thirty_second": "a4T", "gracenote-A5_eighth": "a5z", "gracenote-A5_quarter": "a5q", "gracenote-A5_sixteenth": "a5S", "gracenote-A5_sixteenth.": "a5S.", "gracenote-A5_thirty_second": "a5T", "gracenote-Ab3_double_whole": "ab3w", "gracenote-Ab3_eighth": "ab3z", "gracenote-Ab3_sixteenth": "ab3S", "gracenote-Ab3_thirty_second": "ab3T", "gracenote-Ab4_eighth": "ab4z", "gracenote-Ab4_half": "ab4H", "gracenote-Ab4_quarter": "ab4q", "gracenote-Ab4_sixteenth": "ab4S", "gracenote-Ab4_thirty_second": "ab4T", "gracenote-Ab5_eighth": "ab5z", "gracenote-Ab5_quarter": "ab5q", "gracenote-Ab5_sixteenth": "ab5S", "gracenote-Ab5_thirty_second": "ab5T", "gracenote-B#3_sixteenth": "b#3S", "gracenote-B#4_eighth": "b#4z", "gracenote-B#4_quarter": "b#4q", "gracenote-B#4_sixteenth": "b#4S", "gracenote-B2_sixteenth": "b2S", "gracenote-B3_eighth": "b3z", "gracenote-B3_quarter": "b3q", "gracenote-B3_sixteenth": "b3S", "gracenote-B3_thirty_second": "b3T", "gracenote-B4_eighth": "b4z", "gracenote-B4_quarter": "b4q", "gracenote-B4_sixteenth": "b4S", "gracenote-B4_sixteenth.": "b4S.", "gracenote-B4_thirty_second": "b4T", "gracenote-B5_eighth": "b5z", "gracenote-Bb3_eighth": "bb3z", "gracenote-Bb3_quarter": "bb3q", "gracenote-Bb3_sixteenth": "bb3S", "gracenote-Bb3_thirty_second": "bb3T", "gracenote-Bb4_eighth": "bb4z", "gracenote-Bb4_eighth.": "bb4z.", "gracenote-Bb4_half": "bb4H", "gracenote-Bb4_quarter": "bb4q", "gracenote-Bb4_sixteenth": "bb4S", "gracenote-Bb4_thirty_second": "bb4T", "gracenote-Bb5_eighth": "bb5z", "gracenote-C#3_eighth": "c#3z", "gracenote-C#3_sixteenth": "c#3S", "gracenote-C#4_eighth": "c#4z", "gracenote-C#4_eighth.": "c#4z.", "gracenote-C#4_quarter": "c#4q", "gracenote-C#4_sixteenth": "c#4S", "gracenote-C#4_thirty_second": "c#4T", "gracenote-C#5_eighth": "c#5z", "gracenote-C#5_eighth.": "c#5z.", "gracenote-C#5_half": "c#5H", "gracenote-C#5_quarter": "c#5q", "gracenote-C#5_sixteenth": "c#5S", "gracenote-C#5_sixteenth.": "c#5S.", "gracenote-C#5_thirty_second": "c#5T", "gracenote-C3_eighth": "c3z", "gracenote-C3_quarter": "c3q", "gracenote-C3_sixteenth": "c3S", "gracenote-C4_eighth": "c4z", "gracenote-C4_quarter": "c4q", "gracenote-C4_sixteenth": "c4S", "gracenote-C4_thirty_second": "c4T", "gracenote-C5_eighth": "c5z", "gracenote-C5_half": "c5H", "gracenote-C5_quarter": "c5q", "gracenote-C5_sixteenth": "c5S", "gracenote-C5_thirty_second": "c5T", "gracenote-Cb5_eighth": "cb5z", "gracenote-Cb5_quarter": "cb5q", "gracenote-Cb5_thirty_second": "cb5T", "gracenote-D#3_quarter": "d#3q", "gracenote-D#3_sixteenth": "d#3S", "gracenote-D#4_eighth": "d#4z", "gracenote-D#4_quarter": "d#4q", "gracenote-D#4_sixteenth": "d#4S", "gracenote-D#4_thirty_second": "d#4T", "gracenote-D#5_eighth": "d#5z", "gracenote-D#5_quarter": "d#5q", "gracenote-D#5_sixteenth": "d#5S", "gracenote-D#5_thirty_second": "d#5T", "gracenote-D3_eighth": "d3z", "gracenote-D3_quarter": "d3q", "gracenote-D3_sixteenth": "d3S", "gracenote-D4_eighth": "d4z", "gracenote-D4_quarter": "d4q", "gracenote-D4_sixteenth": "d4S", "gracenote-D4_thirty_second": "d4T", "gracenote-D5_eighth": "d5z", "gracenote-D5_half": "d5H", "gracenote-D5_quarter": "d5q", "gracenote-D5_sixteenth": "d5S", "gracenote-D5_sixteenth.": "d5S.", "gracenote-D5_thirty_second": "d5T", "gracenote-Db4_eighth": "db4z", "gracenote-Db4_sixteenth": "db4S", "gracenote-Db5_eighth": "db5z", "gracenote-Db5_half": "db5H", "gracenote-Db5_quarter": "db5q", "gracenote-Db5_sixteenth": "db5S", "gracenote-Db5_thirty_second": "db5T", "gracenote-E#4_eighth": "e#4z", "gracenote-E#4_sixteenth": "e#4S", "gracenote-E#5_eighth": "e#5z", "gracenote-E#5_quarter": "e#5q", "gracenote-E#5_sixteenth": "e#5S", "gracenote-E3_eighth": "e3z", "gracenote-E3_quarter": "e3q", "gracenote-E3_sixteenth": "e3S", "gracenote-E4_eighth": "e4z", "gracenote-E4_quarter": "e4q", "gracenote-E4_sixteenth": "e4S", "gracenote-E4_thirty_second": "e4T", "gracenote-E5_eighth": "e5z", "gracenote-E5_half": "e5H", "gracenote-E5_quarter": "e5q", "gracenote-E5_sixteenth": "e5S", "gracenote-E5_thirty_second": "e5T", "gracenote-Eb3_eighth": "eb3z", "gracenote-Eb3_quarter": "eb3q", "gracenote-Eb3_sixteenth": "eb3S", "gracenote-Eb4_eighth": "eb4z", "gracenote-Eb4_quarter": "eb4q", "gracenote-Eb4_sixteenth": "eb4S", "gracenote-Eb4_thirty_second": "eb4T", "gracenote-Eb5_eighth": "eb5z", "gracenote-Eb5_quarter": "eb5q", "gracenote-Eb5_quarter.": "eb5q.", "gracenote-Eb5_sixteenth": "eb5S", "gracenote-Eb5_thirty_second": "eb5T", "gracenote-F#2_quarter": "f#2q", "gracenote-F#3_eighth": "f#3z", "gracenote-F#3_quarter": "f#3q", "gracenote-F#3_sixteenth": "f#3S", "gracenote-F#4_eighth": "f#4z", "gracenote-F#4_quarter": "f#4q", "gracenote-F#4_sixteenth": "f#4S", "gracenote-F#4_thirty_second": "f#4T", "gracenote-F#5_eighth": "f#5z", "gracenote-F#5_quarter": "f#5q", "gracenote-F#5_sixteenth": "f#5S", "gracenote-F#5_thirty_second": "f#5T", "gracenote-F2_eighth": "f2z", "gracenote-F3_eighth": "f3z", "gracenote-F3_quarter": "f3q", "gracenote-F3_sixteenth": "f3S", "gracenote-F3_thirty_second": "f3T", "gracenote-F4_eighth": "f4z", "gracenote-F4_quarter": "f4q", "gracenote-F4_sixteenth": "f4S", "gracenote-F4_thirty_second": "f4T", "gracenote-F5_eighth": "f5z", "gracenote-F5_half": "f5H", "gracenote-F5_quarter": "f5q", "gracenote-F5_sixteenth": "f5S", "gracenote-F5_sixteenth.": "f5S.", "gracenote-F5_thirty_second": "f5T", "gracenote-G#3_eighth": "g#3z", "gracenote-G#3_sixteenth": "g#3S", "gracenote-G#3_thirty_second": "g#3T", "gracenote-G#4_eighth": "g#4z", "gracenote-G#4_quarter": "g#4q", "gracenote-G#4_sixteenth": "g#4S", "gracenote-G#4_thirty_second": "g#4T", "gracenote-G#5_eighth": "g#5z", "gracenote-G#5_quarter": "g#5q", "gracenote-G#5_sixteenth": "g#5S", "gracenote-G#5_thirty_second": "g#5T", "gracenote-G3_eighth": "g3z", "gracenote-G3_quarter": "g3q", "gracenote-G3_sixteenth": "g3S", "gracenote-G3_thirty_second": "g3T", "gracenote-G4_eighth": "g4z", "gracenote-G4_eighth.": "g4z.", "gracenote-G4_half": "g4H", "gracenote-G4_quarter": "g4q", "gracenote-G4_sixteenth": "g4S", "gracenote-G4_thirty_second": "g4T", "gracenote-G5_eighth": "g5z", "gracenote-G5_half": "g5H", "gracenote-G5_quarter": "g5q", "gracenote-G5_sixteenth": "g5S", "gracenote-G5_sixteenth.": "g5S.", "gracenote-G5_thirty_second": "g5T", "gracenote-Gb4_eighth": "gb4z", "gracenote-Gb4_quarter": "gb4q", "gracenote-Gb5_thirty_second": "gb5T", - "keySignature-AM": "kAM", "keySignature-AbM": "kAbM", "keySignature-BM": "kBM", "keySignature-BbM": "kBbM", "keySignature-C#M": "kC#M", "keySignature-CM": "kCM", "keySignature-CbM": "kCbM", "keySignature-DM": "kDM", "keySignature-DbM": "kDbM", "keySignature-EM": "kEM", "keySignature-EbM": "kEbM", "keySignature-F#M": "kF#M", "keySignature-FM": "kFM", "keySignature-GM": "kGM", "keySignature-GbM": "kGbM", - "multirest-1": "-1", "multirest-10": "-10", "multirest-100": "-100", "multirest-105": "-105", "multirest-107": "-107", "multirest-11": "-11", "multirest-1111": "-1111", "multirest-112": "-112", "multirest-115": "-115", "multirest-119": "-119", "multirest-12": "-12", "multirest-123": "-123", "multirest-124": "-124", "multirest-126": "-126", "multirest-128": "-128", "multirest-13": "-13", "multirest-14": "-14", "multirest-143": "-143", "multirest-15": "-15", "multirest-16": "-16", "multirest-164": "-164", "multirest-17": "-17", "multirest-18": "-18", "multirest-19": "-19", "multirest-193": "-193", "multirest-2": "-2", "multirest-20": "-20", "multirest-21": "-21", "multirest-22": "-22", "multirest-225": "-225", "multirest-23": "-23", "multirest-24": "-24", "multirest-25": "-25", "multirest-26": "-26", "multirest-27": "-27", "multirest-28": "-28", "multirest-29": "-29", "multirest-3": "-3", "multirest-30": "-30", "multirest-31": "-31", "multirest-32": "-32", "multirest-33": "-33", "multirest-34": "-34", "multirest-35": "-35", "multirest-36": "-36", "multirest-37": "-37", "multirest-38": "-38", "multirest-39": "-39", "multirest-4": "-4", "multirest-40": "-40", "multirest-41": "-41", "multirest-42": "-42", "multirest-43": "-43", "multirest-44": "-44", "multirest-45": "-45", "multirest-46": "-46", "multirest-47": "-47", "multirest-48": "-48", "multirest-49": "-49", "multirest-5": "-5", "multirest-50": "-50", "multirest-51": "-51", "multirest-52": "-52", "multirest-53": "-53", "multirest-54": "-54", "multirest-55": "-55", "multirest-56": "-56", "multirest-57": "-57", "multirest-58": "-58", "multirest-59": "-59", "multirest-6": "-6", "multirest-60": "-60", "multirest-63": "-63", "multirest-64": "-64", "multirest-65": "-65", "multirest-66": "-66", "multirest-67": "-67", "multirest-68": "-68", "multirest-69": "-69", "multirest-7": "-7", "multirest-70": "-70", "multirest-71": "-71", "multirest-72": "-72", "multirest-73": "-73", "multirest-76": "-76", "multirest-77": "-77", "multirest-79": "-79", "multirest-8": "-8", "multirest-80": "-80", "multirest-81": "-81", "multirest-88": "-88", "multirest-89": "-89", "multirest-9": "-9", "multirest-91": "-91", "multirest-94": "-94", "multirest-96": "-96", "multirest-98": "-98", "multirest-99": "-99", - "note-A##1_eighth": "A##1z", "note-A##2_eighth": "A##2z", "note-A##2_sixteenth": "A##2S", "note-A##2_thirty_second": "A##2T", "note-A##3_sixteenth": "A##3S", "note-A##3_thirty_second": "A##3T", "note-A##4_eighth": "A##4z", "note-A##4_sixteenth": "A##4S", "note-A##5_eighth": "A##5z", "note-A##5_quarter": "A##5q", "note-A##6_eighth": "A##6z", "note-A##7_eighth": "A##7z", "note-A#0_eighth": "A#0z", "note-A#0_half": "A#0H", "note-A#0_quarter": "A#0q", "note-A#0_sixteenth": "A#0S", "note-A#1_eighth": "A#1z", "note-A#1_eighth.": "A#1z.", "note-A#1_half": "A#1H", "note-A#1_half.": "A#1H.", "note-A#1_quarter": "A#1q", "note-A#1_quarter.": "A#1q.", "note-A#1_sixteenth": "A#1S", "note-A#1_thirty_second": "A#1T", "note-A#1_whole": "A#1W", "note-A#1_whole.": "A#1W.", "note-A#2_eighth": "A#2z", "note-A#2_eighth.": "A#2z.", "note-A#2_half": "A#2H", "note-A#2_half.": "A#2H.", "note-A#2_quarter": "A#2q", "note-A#2_quarter.": "A#2q.", "note-A#2_sixteenth": "A#2S", "note-A#2_sixteenth.": "A#2S.", "note-A#2_sixty_fourth": "A#2s", "note-A#2_thirty_second": "A#2T", "note-A#2_whole": "A#2W", "note-A#2_whole.": "A#2W.", "note-A#3_eighth": "A#3z", "note-A#3_eighth.": "A#3z.", "note-A#3_half": "A#3H", "note-A#3_half.": "A#3H.", "note-A#3_half_fermata": "A#3H^", "note-A#3_hundred_twenty_eighth": "A#3h", "note-A#3_quarter": "A#3q", "note-A#3_quarter.": "A#3q.", "note-A#3_sixteenth": "A#3S", "note-A#3_sixteenth.": "A#3S.", "note-A#3_sixty_fourth": "A#3s", "note-A#3_thirty_second": "A#3T", "note-A#3_whole": "A#3W", "note-A#4_eighth": "A#4z", "note-A#4_eighth.": "A#4z.", "note-A#4_half": "A#4H", "note-A#4_half.": "A#4H.", "note-A#4_half_fermata": "A#4H^", "note-A#4_quarter": "A#4q", "note-A#4_quarter.": "A#4q.", "note-A#4_quarter_fermata": "A#4q^", "note-A#4_sixteenth": "A#4S", "note-A#4_sixteenth.": "A#4S.", "note-A#4_sixty_fourth": "A#4s", "note-A#4_thirty_second": "A#4T", "note-A#4_whole": "A#4W", "note-A#4_whole.": "A#4W.", "note-A#5_eighth": "A#5z", "note-A#5_eighth.": "A#5z.", "note-A#5_half": "A#5H", "note-A#5_half.": "A#5H.", "note-A#5_quarter": "A#5q", "note-A#5_quarter.": "A#5q.", "note-A#5_sixteenth": "A#5S", "note-A#5_sixteenth.": "A#5S.", "note-A#5_sixty_fourth": "A#5s", "note-A#5_thirty_second": "A#5T", "note-A#5_whole": "A#5W", "note-A#5_whole.": "A#5W.", "note-A#6_eighth": "A#6z", "note-A#6_eighth.": "A#6z.", "note-A#6_half": "A#6H", "note-A#6_quarter": "A#6q", "note-A#6_quarter.": "A#6q.", "note-A#6_sixteenth": "A#6S", "note-A#6_sixty_fourth": "A#6s", "note-A#6_thirty_second": "A#6T", "note-A#6_whole": "A#6W", "note-A#7_eighth": "A#7z", "note-A#7_eighth.": "A#7z.", "note-A#7_half": "A#7H", "note-A#7_quarter": "A#7q", "note-A#7_sixteenth": "A#7S", "note-A#7_sixty_fourth": "A#7s", "note-A#7_thirty_second": "A#7T", "note-A0_eighth": "A0z", "note-A0_eighth.": "A0z.", "note-A0_half": "A0H", "note-A0_half.": "A0H.", "note-A0_quarter": "A0q", "note-A0_quarter.": "A0q.", "note-A0_sixteenth": "A0S", "note-A0_sixteenth.": "A0S.", "note-A0_thirty_second": "A0T", "note-A0_whole": "A0W", "note-A1_breve": "A1Y", "note-A1_eighth": "A1z", "note-A1_eighth.": "A1z.", "note-A1_half": "A1H", "note-A1_half.": "A1H.", "note-A1_hundred_twenty_eighth": "A1h", "note-A1_quarter": "A1q", "note-A1_quarter.": "A1q.", "note-A1_sixteenth": "A1S", "note-A1_sixteenth.": "A1S.", "note-A1_sixty_fourth": "A1s", "note-A1_sixty_fourth.": "A1s.", "note-A1_thirty_second": "A1T", "note-A1_thirty_second.": "A1T.", "note-A1_whole": "A1W", "note-A1_whole.": "A1W.", "note-A2_breve": "A2Y", "note-A2_double_whole": "A2w", "note-A2_double_whole.": "A2w.", "note-A2_double_whole_fermata": "A2w^", "note-A2_eighth": "A2z", "note-A2_eighth.": "A2z.", "note-A2_half": "A2H", "note-A2_half.": "A2H.", "note-A2_half_fermata": "A2H^", "note-A2_hundred_twenty_eighth": "A2h", "note-A2_quadruple_whole": "A2Q", "note-A2_quadruple_whole_fermata": "A2Q^", "note-A2_quarter": "A2q", "note-A2_quarter.": "A2q.", "note-A2_quarter_fermata": "A2q^", "note-A2_sixteenth": "A2S", "note-A2_sixteenth.": "A2S.", "note-A2_sixty_fourth": "A2s", "note-A2_sixty_fourth.": "A2s.", "note-A2_thirty_second": "A2T", "note-A2_thirty_second.": "A2T.", "note-A2_whole": "A2W", "note-A2_whole.": "A2W.", "note-A2_whole_fermata": "A2W^", "note-A3_breve": "A3Y", "note-A3_breve.": "A3Y.", "note-A3_double_whole": "A3w", "note-A3_double_whole.": "A3w.", "note-A3_double_whole_fermata": "A3w^", "note-A3_eighth": "A3z", "note-A3_eighth.": "A3z.", "note-A3_eighth..": "A3z..", "note-A3_eighth._fermata": "A3z.^", "note-A3_eighth_fermata": "A3z^", "note-A3_half": "A3H", "note-A3_half.": "A3H.", "note-A3_half._fermata": "A3H.^", "note-A3_half_fermata": "A3H^", "note-A3_hundred_twenty_eighth": "A3h", "note-A3_quadruple_whole": "A3Q", "note-A3_quarter": "A3q", "note-A3_quarter.": "A3q.", "note-A3_quarter..": "A3q..", "note-A3_quarter_fermata": "A3q^", "note-A3_sixteenth": "A3S", "note-A3_sixteenth.": "A3S.", "note-A3_sixty_fourth": "A3s", "note-A3_sixty_fourth.": "A3s.", "note-A3_thirty_second": "A3T", "note-A3_thirty_second.": "A3T.", "note-A3_whole": "A3W", "note-A3_whole.": "A3W.", "note-A3_whole_fermata": "A3W^", "note-A4_breve": "A4Y", "note-A4_double_whole": "A4w", "note-A4_double_whole.": "A4w.", "note-A4_double_whole_fermata": "A4w^", "note-A4_eighth": "A4z", "note-A4_eighth.": "A4z.", "note-A4_eighth..": "A4z..", "note-A4_eighth_fermata": "A4z^", "note-A4_half": "A4H", "note-A4_half.": "A4H.", "note-A4_half..": "A4H..", "note-A4_half._fermata": "A4H.^", "note-A4_half_fermata": "A4H^", "note-A4_hundred_twenty_eighth": "A4h", "note-A4_long": "A4long", "note-A4_quadruple_whole": "A4Q", "note-A4_quadruple_whole.": "A4Q.", "note-A4_quarter": "A4q", "note-A4_quarter.": "A4q.", "note-A4_quarter..": "A4q..", "note-A4_quarter._fermata": "A4q.^", "note-A4_quarter_fermata": "A4q^", "note-A4_sixteenth": "A4S", "note-A4_sixteenth.": "A4S.", "note-A4_sixty_fourth": "A4s", "note-A4_sixty_fourth.": "A4s.", "note-A4_thirty_second": "A4T", "note-A4_thirty_second.": "A4T.", "note-A4_whole": "A4W", "note-A4_whole.": "A4W.", "note-A4_whole._fermata": "A4W.^", "note-A4_whole_fermata": "A4W^", "note-A5_breve": "A5Y", "note-A5_double_whole": "A5w", "note-A5_eighth": "A5z", "note-A5_eighth.": "A5z.", "note-A5_eighth..": "A5z..", "note-A5_eighth_fermata": "A5z^", "note-A5_half": "A5H", "note-A5_half.": "A5H.", "note-A5_half._fermata": "A5H.^", "note-A5_half_fermata": "A5H^", "note-A5_hundred_twenty_eighth": "A5h", "note-A5_quarter": "A5q", "note-A5_quarter.": "A5q.", "note-A5_quarter..": "A5q..", "note-A5_quarter._fermata": "A5q.^", "note-A5_quarter_fermata": "A5q^", "note-A5_sixteenth": "A5S", "note-A5_sixteenth.": "A5S.", "note-A5_sixty_fourth": "A5s", "note-A5_sixty_fourth.": "A5s.", "note-A5_thirty_second": "A5T", "note-A5_thirty_second.": "A5T.", "note-A5_whole": "A5W", "note-A5_whole.": "A5W.", "note-A5_whole_fermata": "A5W^", "note-A6_eighth": "A6z", "note-A6_eighth.": "A6z.", "note-A6_half": "A6H", "note-A6_half.": "A6H.", "note-A6_hundred_twenty_eighth": "A6h", "note-A6_quarter": "A6q", "note-A6_quarter.": "A6q.", "note-A6_sixteenth": "A6S", "note-A6_sixteenth.": "A6S.", "note-A6_sixty_fourth": "A6s", "note-A6_sixty_fourth.": "A6s.", "note-A6_thirty_second": "A6T", "note-A6_thirty_second.": "A6T.", "note-A6_whole": "A6W", "note-A6_whole.": "A6W.", "note-A7_eighth": "A7z", "note-A7_eighth.": "A7z.", "note-A7_half": "A7H", "note-A7_half.": "A7H.", "note-A7_hundred_twenty_eighth": "A7h", "note-A7_quarter": "A7q", "note-A7_quarter.": "A7q.", "note-A7_sixteenth": "A7S", "note-A7_sixteenth.": "A7S.", "note-A7_sixty_fourth": "A7s", "note-A7_thirty_second": "A7T", "note-A7_thirty_second.": "A7T.", "note-A7_whole": "A7W", "note-A8_eighth": "A8z", "note-A8_eighth.": "A8z.", "note-A8_hundred_twenty_eighth": "A8h", "note-A8_quarter": "A8q", "note-A8_sixteenth": "A8S", "note-A8_sixteenth.": "A8S.", "note-A8_sixty_fourth.": "A8s.", "note-A8_thirty_second": "A8T", "note-AN0_eighth": "AN0z", "note-AN0_half.": "AN0H.", "note-AN0_sixteenth": "AN0S", "note-AN0_whole": "AN0W", "note-AN1_eighth": "AN1z", "note-AN1_eighth.": "AN1z.", "note-AN1_half": "AN1H", "note-AN1_half.": "AN1H.", "note-AN1_hundred_twenty_eighth": "AN1h", "note-AN1_quarter": "AN1q", "note-AN1_quarter.": "AN1q.", "note-AN1_sixteenth": "AN1S", "note-AN1_sixty_fourth.": "AN1s.", "note-AN1_thirty_second": "AN1T", "note-AN1_whole": "AN1W", "note-AN2_eighth": "AN2z", "note-AN2_eighth.": "AN2z.", "note-AN2_half": "AN2H", "note-AN2_half.": "AN2H.", "note-AN2_quarter": "AN2q", "note-AN2_quarter.": "AN2q.", "note-AN2_sixteenth": "AN2S", "note-AN2_sixteenth.": "AN2S.", "note-AN2_sixty_fourth": "AN2s", "note-AN2_thirty_second": "AN2T", "note-AN2_whole": "AN2W", "note-AN2_whole.": "AN2W.", "note-AN3_eighth": "AN3z", "note-AN3_eighth.": "AN3z.", "note-AN3_half": "AN3H", "note-AN3_half.": "AN3H.", "note-AN3_quarter": "AN3q", "note-AN3_quarter.": "AN3q.", "note-AN3_sixteenth": "AN3S", "note-AN3_sixteenth.": "AN3S.", "note-AN3_sixty_fourth": "AN3s", "note-AN3_thirty_second": "AN3T", "note-AN3_thirty_second.": "AN3T.", "note-AN3_whole": "AN3W", "note-AN3_whole.": "AN3W.", "note-AN4_eighth": "AN4z", "note-AN4_eighth.": "AN4z.", "note-AN4_half": "AN4H", "note-AN4_half.": "AN4H.", "note-AN4_hundred_twenty_eighth": "AN4h", "note-AN4_quarter": "AN4q", "note-AN4_quarter.": "AN4q.", "note-AN4_sixteenth": "AN4S", "note-AN4_sixteenth.": "AN4S.", "note-AN4_sixty_fourth": "AN4s", "note-AN4_thirty_second": "AN4T", "note-AN4_thirty_second.": "AN4T.", "note-AN4_whole": "AN4W", "note-AN4_whole.": "AN4W.", "note-AN5_eighth": "AN5z", "note-AN5_eighth.": "AN5z.", "note-AN5_half": "AN5H", "note-AN5_half.": "AN5H.", "note-AN5_hundred_twenty_eighth": "AN5h", "note-AN5_quarter": "AN5q", "note-AN5_quarter.": "AN5q.", "note-AN5_sixteenth": "AN5S", "note-AN5_sixteenth.": "AN5S.", "note-AN5_sixty_fourth": "AN5s", "note-AN5_sixty_fourth.": "AN5s.", "note-AN5_thirty_second": "AN5T", "note-AN5_thirty_second.": "AN5T.", "note-AN5_whole": "AN5W", "note-AN5_whole.": "AN5W.", "note-AN6_eighth": "AN6z", "note-AN6_eighth.": "AN6z.", "note-AN6_half": "AN6H", "note-AN6_half.": "AN6H.", "note-AN6_hundred_twenty_eighth": "AN6h", "note-AN6_quarter": "AN6q", "note-AN6_quarter.": "AN6q.", "note-AN6_sixteenth": "AN6S", "note-AN6_sixteenth.": "AN6S.", "note-AN6_sixty_fourth": "AN6s", "note-AN6_sixty_fourth.": "AN6s.", "note-AN6_thirty_second": "AN6T", "note-AN6_thirty_second.": "AN6T.", "note-AN6_whole": "AN6W", "note-AN7_eighth": "AN7z", "note-AN7_quarter": "AN7q", "note-AN7_sixteenth": "AN7S", "note-AN7_thirty_second": "AN7T", "note-Ab0_half": "Ab0H", "note-Ab0_quarter.": "Ab0q.", "note-Ab1_eighth": "Ab1z", "note-Ab1_eighth.": "Ab1z.", "note-Ab1_half": "Ab1H", "note-Ab1_half.": "Ab1H.", "note-Ab1_quarter": "Ab1q", "note-Ab1_quarter.": "Ab1q.", "note-Ab1_sixteenth": "Ab1S", "note-Ab1_thirty_second": "Ab1T", "note-Ab1_thirty_second.": "Ab1T.", "note-Ab1_whole": "Ab1W", "note-Ab1_whole.": "Ab1W.", "note-Ab2_breve": "Ab2Y", "note-Ab2_eighth": "Ab2z", "note-Ab2_eighth.": "Ab2z.", "note-Ab2_half": "Ab2H", "note-Ab2_half.": "Ab2H.", "note-Ab2_half_fermata": "Ab2H^", "note-Ab2_quarter": "Ab2q", "note-Ab2_quarter.": "Ab2q.", "note-Ab2_sixteenth": "Ab2S", "note-Ab2_sixty_fourth": "Ab2s", "note-Ab2_thirty_second": "Ab2T", "note-Ab2_whole": "Ab2W", "note-Ab2_whole.": "Ab2W.", "note-Ab3_breve": "Ab3Y", "note-Ab3_eighth": "Ab3z", "note-Ab3_eighth.": "Ab3z.", "note-Ab3_half": "Ab3H", "note-Ab3_half.": "Ab3H.", "note-Ab3_quarter": "Ab3q", "note-Ab3_quarter.": "Ab3q.", "note-Ab3_quarter..": "Ab3q..", "note-Ab3_sixteenth": "Ab3S", "note-Ab3_sixteenth.": "Ab3S.", "note-Ab3_sixty_fourth": "Ab3s", "note-Ab3_thirty_second": "Ab3T", "note-Ab3_thirty_second.": "Ab3T.", "note-Ab3_whole": "Ab3W", "note-Ab3_whole.": "Ab3W.", "note-Ab4_breve": "Ab4Y", "note-Ab4_eighth": "Ab4z", "note-Ab4_eighth.": "Ab4z.", "note-Ab4_eighth..": "Ab4z..", "note-Ab4_half": "Ab4H", "note-Ab4_half.": "Ab4H.", "note-Ab4_half._fermata": "Ab4H.^", "note-Ab4_half_fermata": "Ab4H^", "note-Ab4_hundred_twenty_eighth": "Ab4h", "note-Ab4_quarter": "Ab4q", "note-Ab4_quarter.": "Ab4q.", "note-Ab4_quarter..": "Ab4q..", "note-Ab4_quarter_fermata": "Ab4q^", "note-Ab4_sixteenth": "Ab4S", "note-Ab4_sixteenth.": "Ab4S.", "note-Ab4_sixty_fourth": "Ab4s", "note-Ab4_thirty_second": "Ab4T", "note-Ab4_thirty_second.": "Ab4T.", "note-Ab4_whole": "Ab4W", "note-Ab4_whole.": "Ab4W.", "note-Ab4_whole_fermata": "Ab4W^", "note-Ab5_breve": "Ab5Y", "note-Ab5_eighth": "Ab5z", "note-Ab5_eighth.": "Ab5z.", "note-Ab5_eighth..": "Ab5z..", "note-Ab5_half": "Ab5H", "note-Ab5_half.": "Ab5H.", "note-Ab5_half._fermata": "Ab5H.^", "note-Ab5_hundred_twenty_eighth": "Ab5h", "note-Ab5_quarter": "Ab5q", "note-Ab5_quarter.": "Ab5q.", "note-Ab5_quarter_fermata": "Ab5q^", "note-Ab5_sixteenth": "Ab5S", "note-Ab5_sixteenth.": "Ab5S.", "note-Ab5_sixty_fourth": "Ab5s", "note-Ab5_sixty_fourth.": "Ab5s.", "note-Ab5_thirty_second": "Ab5T", "note-Ab5_thirty_second.": "Ab5T.", "note-Ab5_whole": "Ab5W", "note-Ab5_whole.": "Ab5W.", "note-Ab6_eighth": "Ab6z", "note-Ab6_eighth.": "Ab6z.", "note-Ab6_half": "Ab6H", "note-Ab6_half.": "Ab6H.", "note-Ab6_hundred_twenty_eighth": "Ab6h", "note-Ab6_quarter": "Ab6q", "note-Ab6_quarter.": "Ab6q.", "note-Ab6_sixteenth": "Ab6S", "note-Ab6_sixty_fourth": "Ab6s", "note-Ab6_thirty_second": "Ab6T", "note-Ab6_whole": "Ab6W", "note-Ab6_whole.": "Ab6W.", "note-Ab7_eighth": "Ab7z", "note-Ab7_hundred_twenty_eighth": "Ab7h", "note-Ab7_quarter": "Ab7q", "note-Ab7_sixteenth": "Ab7S", "note-Ab7_thirty_second": "Ab7T", "note-Abb1_half": "Abb1H", "note-Abb1_sixteenth": "Abb1S", "note-Abb1_whole": "Abb1W", "note-Abb2_eighth": "Abb2z", "note-Abb2_half": "Abb2H", "note-Abb2_quarter": "Abb2q", "note-Abb2_sixteenth": "Abb2S", "note-Abb2_whole": "Abb2W", "note-Abb3_eighth": "Abb3z", "note-Abb3_half": "Abb3H", "note-Abb3_quarter": "Abb3q", "note-Abb3_sixteenth": "Abb3S", "note-Abb4_eighth": "Abb4z", "note-Abb4_quarter": "Abb4q", "note-Abb4_sixteenth": "Abb4S", "note-Abb4_thirty_second": "Abb4T", "note-Abb5_eighth": "Abb5z", "note-Abb5_quarter": "Abb5q", "note-Abb5_sixteenth": "Abb5S", "note-Abb5_thirty_second": "Abb5T", "note-Abb6_eighth": "Abb6z", "note-Abb6_quarter": "Abb6q", "note-Abb6_sixteenth": "Abb6S", "note-Abb6_sixty_fourth": "Abb6s", "note-Abb6_thirty_second": "Abb6T", "note-B##2_eighth": "B##2z", "note-B##2_sixteenth": "B##2S", "note-B##4_eighth": "B##4z", "note-B##4_quarter": "B##4q", "note-B##4_sixteenth": "B##4S", "note-B#0_eighth": "B#0z", "note-B#0_whole": "B#0W", "note-B#1_eighth": "B#1z", "note-B#1_eighth.": "B#1z.", "note-B#1_half": "B#1H", "note-B#1_half.": "B#1H.", "note-B#1_quarter": "B#1q", "note-B#1_quarter.": "B#1q.", "note-B#1_sixteenth": "B#1S", "note-B#1_sixty_fourth": "B#1s", "note-B#1_thirty_second": "B#1T", "note-B#1_thirty_second.": "B#1T.", "note-B#1_whole": "B#1W", "note-B#1_whole.": "B#1W.", "note-B#2_eighth": "B#2z", "note-B#2_eighth.": "B#2z.", "note-B#2_half": "B#2H", "note-B#2_half.": "B#2H.", "note-B#2_quarter": "B#2q", "note-B#2_quarter.": "B#2q.", "note-B#2_sixteenth": "B#2S", "note-B#2_sixty_fourth": "B#2s", "note-B#2_thirty_second": "B#2T", "note-B#2_thirty_second.": "B#2T.", "note-B#2_whole": "B#2W", "note-B#2_whole.": "B#2W.", "note-B#3_double_whole": "B#3w", "note-B#3_double_whole.": "B#3w.", "note-B#3_eighth": "B#3z", "note-B#3_eighth.": "B#3z.", "note-B#3_half": "B#3H", "note-B#3_half.": "B#3H.", "note-B#3_quarter": "B#3q", "note-B#3_quarter.": "B#3q.", "note-B#3_sixteenth": "B#3S", "note-B#3_sixteenth.": "B#3S.", "note-B#3_sixty_fourth": "B#3s", "note-B#3_thirty_second": "B#3T", "note-B#3_whole": "B#3W", "note-B#4_double_whole": "B#4w", "note-B#4_double_whole_fermata": "B#4w^", "note-B#4_eighth": "B#4z", "note-B#4_eighth.": "B#4z.", "note-B#4_half": "B#4H", "note-B#4_half.": "B#4H.", "note-B#4_quarter": "B#4q", "note-B#4_quarter.": "B#4q.", "note-B#4_sixteenth": "B#4S", "note-B#4_sixteenth.": "B#4S.", "note-B#4_sixty_fourth": "B#4s", "note-B#4_thirty_second": "B#4T", "note-B#4_whole": "B#4W", "note-B#4_whole.": "B#4W.", "note-B#5_eighth": "B#5z", "note-B#5_eighth.": "B#5z.", "note-B#5_half": "B#5H", "note-B#5_half.": "B#5H.", "note-B#5_quarter": "B#5q", "note-B#5_quarter.": "B#5q.", "note-B#5_sixteenth": "B#5S", "note-B#5_sixteenth.": "B#5S.", "note-B#5_sixty_fourth": "B#5s", "note-B#5_thirty_second": "B#5T", "note-B#5_whole": "B#5W", "note-B#5_whole.": "B#5W.", "note-B#6_eighth": "B#6z", "note-B#6_quarter": "B#6q", "note-B#6_quarter.": "B#6q.", "note-B#6_sixteenth": "B#6S", "note-B#6_sixty_fourth": "B#6s", "note-B#6_thirty_second": "B#6T", "note-B#6_whole": "B#6W", "note-B#7_eighth": "B#7z", "note-B#7_sixteenth": "B#7S", "note-B#7_thirty_second": "B#7T", "note-B0_eighth": "B0z", "note-B0_eighth.": "B0z.", "note-B0_half": "B0H", "note-B0_half.": "B0H.", "note-B0_quarter": "B0q", "note-B0_quarter.": "B0q.", "note-B0_sixteenth": "B0S", "note-B0_sixteenth.": "B0S.", "note-B0_thirty_second": "B0T", "note-B0_whole": "B0W", "note-B0_whole.": "B0W.", "note-B1_breve": "B1Y", "note-B1_eighth": "B1z", "note-B1_eighth.": "B1z.", "note-B1_half": "B1H", "note-B1_half.": "B1H.", "note-B1_hundred_twenty_eighth": "B1h", "note-B1_quarter": "B1q", "note-B1_quarter.": "B1q.", "note-B1_sixteenth": "B1S", "note-B1_sixteenth.": "B1S.", "note-B1_sixty_fourth": "B1s", "note-B1_sixty_fourth.": "B1s.", "note-B1_thirty_second": "B1T", "note-B1_thirty_second.": "B1T.", "note-B1_whole": "B1W", "note-B1_whole.": "B1W.", "note-B2_breve": "B2Y", "note-B2_double_whole": "B2w", "note-B2_eighth": "B2z", "note-B2_eighth.": "B2z.", "note-B2_half": "B2H", "note-B2_half.": "B2H.", "note-B2_half_fermata": "B2H^", "note-B2_hundred_twenty_eighth": "B2h", "note-B2_quarter": "B2q", "note-B2_quarter.": "B2q.", "note-B2_sixteenth": "B2S", "note-B2_sixteenth.": "B2S.", "note-B2_sixty_fourth": "B2s", "note-B2_sixty_fourth.": "B2s.", "note-B2_thirty_second": "B2T", "note-B2_thirty_second.": "B2T.", "note-B2_whole": "B2W", "note-B2_whole.": "B2W.", "note-B3_breve": "B3Y", "note-B3_breve.": "B3Y.", "note-B3_double_whole": "B3w", "note-B3_double_whole.": "B3w.", "note-B3_double_whole_fermata": "B3w^", "note-B3_eighth": "B3z", "note-B3_eighth.": "B3z.", "note-B3_eighth_fermata": "B3z^", "note-B3_half": "B3H", "note-B3_half.": "B3H.", "note-B3_half_fermata": "B3H^", "note-B3_hundred_twenty_eighth": "B3h", "note-B3_quarter": "B3q", "note-B3_quarter.": "B3q.", "note-B3_quarter..": "B3q..", "note-B3_quarter_fermata": "B3q^", "note-B3_sixteenth": "B3S", "note-B3_sixteenth.": "B3S.", "note-B3_sixty_fourth": "B3s", "note-B3_sixty_fourth.": "B3s.", "note-B3_thirty_second": "B3T", "note-B3_thirty_second.": "B3T.", "note-B3_whole": "B3W", "note-B3_whole.": "B3W.", "note-B3_whole_fermata": "B3W^", "note-B4_breve": "B4Y", "note-B4_breve.": "B4Y.", "note-B4_double_whole": "B4w", "note-B4_double_whole.": "B4w.", "note-B4_double_whole_fermata": "B4w^", "note-B4_eighth": "B4z", "note-B4_eighth.": "B4z.", "note-B4_eighth..": "B4z..", "note-B4_eighth._fermata": "B4z.^", "note-B4_eighth_fermata": "B4z^", "note-B4_half": "B4H", "note-B4_half.": "B4H.", "note-B4_half._fermata": "B4H.^", "note-B4_half_fermata": "B4H^", "note-B4_hundred_twenty_eighth": "B4h", "note-B4_long": "B4long", "note-B4_quadruple_whole": "B4Q", "note-B4_quarter": "B4q", "note-B4_quarter.": "B4q.", "note-B4_quarter..": "B4q..", "note-B4_quarter._fermata": "B4q.^", "note-B4_quarter_fermata": "B4q^", "note-B4_sixteenth": "B4S", "note-B4_sixteenth.": "B4S.", "note-B4_sixteenth._fermata": "B4S.^", "note-B4_sixteenth_fermata": "B4S^", "note-B4_sixty_fourth": "B4s", "note-B4_sixty_fourth.": "B4s.", "note-B4_thirty_second": "B4T", "note-B4_thirty_second.": "B4T.", "note-B4_whole": "B4W", "note-B4_whole.": "B4W.", "note-B4_whole._fermata": "B4W.^", "note-B4_whole_fermata": "B4W^", "note-B5_breve": "B5Y", "note-B5_breve.": "B5Y.", "note-B5_double_whole": "B5w", "note-B5_eighth": "B5z", "note-B5_eighth.": "B5z.", "note-B5_eighth..": "B5z..", "note-B5_half": "B5H", "note-B5_half.": "B5H.", "note-B5_half_fermata": "B5H^", "note-B5_hundred_twenty_eighth": "B5h", "note-B5_quarter": "B5q", "note-B5_quarter.": "B5q.", "note-B5_quarter..": "B5q..", "note-B5_sixteenth": "B5S", "note-B5_sixteenth.": "B5S.", "note-B5_sixty_fourth": "B5s", "note-B5_sixty_fourth.": "B5s.", "note-B5_thirty_second": "B5T", "note-B5_thirty_second.": "B5T.", "note-B5_whole": "B5W", "note-B5_whole.": "B5W.", "note-B6_breve": "B6Y", "note-B6_breve.": "B6Y.", "note-B6_eighth": "B6z", "note-B6_eighth.": "B6z.", "note-B6_half": "B6H", "note-B6_half.": "B6H.", "note-B6_hundred_twenty_eighth": "B6h", "note-B6_quarter": "B6q", "note-B6_quarter.": "B6q.", "note-B6_sixteenth": "B6S", "note-B6_sixteenth.": "B6S.", "note-B6_sixty_fourth": "B6s", "note-B6_sixty_fourth.": "B6s.", "note-B6_thirty_second": "B6T", "note-B6_thirty_second.": "B6T.", "note-B6_whole": "B6W", "note-B6_whole.": "B6W.", "note-B7_eighth": "B7z", "note-B7_half": "B7H", "note-B7_half.": "B7H.", "note-B7_hundred_twenty_eighth": "B7h", "note-B7_quarter": "B7q", "note-B7_quarter.": "B7q.", "note-B7_sixteenth": "B7S", "note-B7_sixty_fourth": "B7s", "note-B7_thirty_second": "B7T", "note-B7_whole": "B7W", "note-B8_eighth": "B8z", "note-B8_eighth.": "B8z.", "note-B8_quarter": "B8q", "note-B8_sixteenth": "B8S", "note-B8_sixty_fourth.": "B8s.", "note-BN0_eighth": "BN0z", "note-BN0_eighth.": "BN0z.", "note-BN0_half": "BN0H", "note-BN0_quarter": "BN0q", "note-BN0_sixteenth": "BN0S", "note-BN0_whole": "BN0W", "note-BN1_eighth": "BN1z", "note-BN1_eighth.": "BN1z.", "note-BN1_half": "BN1H", "note-BN1_half.": "BN1H.", "note-BN1_hundred_twenty_eighth": "BN1h", "note-BN1_quarter": "BN1q", "note-BN1_quarter.": "BN1q.", "note-BN1_sixteenth": "BN1S", "note-BN1_sixty_fourth": "BN1s", "note-BN1_sixty_fourth.": "BN1s.", "note-BN1_thirty_second": "BN1T", "note-BN1_thirty_second.": "BN1T.", "note-BN1_whole": "BN1W", "note-BN1_whole.": "BN1W.", "note-BN2_eighth": "BN2z", "note-BN2_eighth.": "BN2z.", "note-BN2_half": "BN2H", "note-BN2_half.": "BN2H.", "note-BN2_hundred_twenty_eighth": "BN2h", "note-BN2_quarter": "BN2q", "note-BN2_quarter.": "BN2q.", "note-BN2_sixteenth": "BN2S", "note-BN2_sixteenth.": "BN2S.", "note-BN2_sixty_fourth": "BN2s", "note-BN2_sixty_fourth.": "BN2s.", "note-BN2_thirty_second": "BN2T", "note-BN2_thirty_second.": "BN2T.", "note-BN2_whole": "BN2W", "note-BN2_whole.": "BN2W.", "note-BN3_breve": "BN3Y", "note-BN3_eighth": "BN3z", "note-BN3_eighth.": "BN3z.", "note-BN3_half": "BN3H", "note-BN3_half.": "BN3H.", "note-BN3_quarter": "BN3q", "note-BN3_quarter.": "BN3q.", "note-BN3_sixteenth": "BN3S", "note-BN3_sixteenth.": "BN3S.", "note-BN3_sixty_fourth": "BN3s", "note-BN3_sixty_fourth.": "BN3s.", "note-BN3_thirty_second": "BN3T", "note-BN3_whole": "BN3W", "note-BN3_whole.": "BN3W.", "note-BN4_eighth": "BN4z", "note-BN4_eighth.": "BN4z.", "note-BN4_half": "BN4H", "note-BN4_half.": "BN4H.", "note-BN4_quarter": "BN4q", "note-BN4_quarter.": "BN4q.", "note-BN4_sixteenth": "BN4S", "note-BN4_sixteenth.": "BN4S.", "note-BN4_sixty_fourth": "BN4s", "note-BN4_sixty_fourth.": "BN4s.", "note-BN4_thirty_second": "BN4T", "note-BN4_thirty_second.": "BN4T.", "note-BN4_whole": "BN4W", "note-BN4_whole.": "BN4W.", "note-BN5_eighth": "BN5z", "note-BN5_eighth.": "BN5z.", "note-BN5_half": "BN5H", "note-BN5_half.": "BN5H.", "note-BN5_quarter": "BN5q", "note-BN5_quarter.": "BN5q.", "note-BN5_sixteenth": "BN5S", "note-BN5_sixteenth.": "BN5S.", "note-BN5_sixty_fourth": "BN5s", "note-BN5_thirty_second": "BN5T", "note-BN5_thirty_second.": "BN5T.", "note-BN5_whole": "BN5W", "note-BN5_whole.": "BN5W.", "note-BN6_eighth": "BN6z", "note-BN6_eighth.": "BN6z.", "note-BN6_half": "BN6H", "note-BN6_hundred_twenty_eighth": "BN6h", "note-BN6_quarter": "BN6q", "note-BN6_quarter.": "BN6q.", "note-BN6_sixteenth": "BN6S", "note-BN6_sixty_fourth": "BN6s", "note-BN6_sixty_fourth.": "BN6s.", "note-BN6_thirty_second": "BN6T", "note-BN6_whole": "BN6W", "note-BN7_sixteenth": "BN7S", "note-BN8_quarter": "BN8q", "note-Bb0_eighth": "Bb0z", "note-Bb0_eighth.": "Bb0z.", "note-Bb0_half": "Bb0H", "note-Bb0_half.": "Bb0H.", "note-Bb0_quarter": "Bb0q", "note-Bb0_quarter.": "Bb0q.", "note-Bb0_sixteenth": "Bb0S", "note-Bb0_thirty_second": "Bb0T", "note-Bb0_whole": "Bb0W", "note-Bb1_eighth": "Bb1z", "note-Bb1_eighth.": "Bb1z.", "note-Bb1_half": "Bb1H", "note-Bb1_half.": "Bb1H.", "note-Bb1_quarter": "Bb1q", "note-Bb1_quarter.": "Bb1q.", "note-Bb1_sixteenth": "Bb1S", "note-Bb1_sixteenth.": "Bb1S.", "note-Bb1_sixty_fourth": "Bb1s", "note-Bb1_thirty_second": "Bb1T", "note-Bb1_thirty_second.": "Bb1T.", "note-Bb1_whole": "Bb1W", "note-Bb1_whole.": "Bb1W.", "note-Bb2_double_whole": "Bb2w", "note-Bb2_eighth": "Bb2z", "note-Bb2_eighth.": "Bb2z.", "note-Bb2_half": "Bb2H", "note-Bb2_half.": "Bb2H.", "note-Bb2_quarter": "Bb2q", "note-Bb2_quarter.": "Bb2q.", "note-Bb2_quarter._fermata": "Bb2q.^", "note-Bb2_quarter_fermata": "Bb2q^", "note-Bb2_sixteenth": "Bb2S", "note-Bb2_sixteenth.": "Bb2S.", "note-Bb2_sixteenth_fermata": "Bb2S^", "note-Bb2_sixty_fourth": "Bb2s", "note-Bb2_sixty_fourth.": "Bb2s.", "note-Bb2_thirty_second": "Bb2T", "note-Bb2_thirty_second.": "Bb2T.", "note-Bb2_whole": "Bb2W", "note-Bb2_whole.": "Bb2W.", "note-Bb3_double_whole": "Bb3w", "note-Bb3_double_whole.": "Bb3w.", "note-Bb3_eighth": "Bb3z", "note-Bb3_eighth.": "Bb3z.", "note-Bb3_eighth..": "Bb3z..", "note-Bb3_half": "Bb3H", "note-Bb3_half.": "Bb3H.", "note-Bb3_half._fermata": "Bb3H.^", "note-Bb3_half_fermata": "Bb3H^", "note-Bb3_quadruple_whole": "Bb3Q", "note-Bb3_quarter": "Bb3q", "note-Bb3_quarter.": "Bb3q.", "note-Bb3_quarter..": "Bb3q..", "note-Bb3_quarter._fermata": "Bb3q.^", "note-Bb3_quarter_fermata": "Bb3q^", "note-Bb3_sixteenth": "Bb3S", "note-Bb3_sixteenth.": "Bb3S.", "note-Bb3_sixty_fourth": "Bb3s", "note-Bb3_sixty_fourth.": "Bb3s.", "note-Bb3_thirty_second": "Bb3T", "note-Bb3_thirty_second.": "Bb3T.", "note-Bb3_whole": "Bb3W", "note-Bb3_whole.": "Bb3W.", "note-Bb3_whole_fermata": "Bb3W^", "note-Bb4_double_whole": "Bb4w", "note-Bb4_double_whole.": "Bb4w.", "note-Bb4_eighth": "Bb4z", "note-Bb4_eighth.": "Bb4z.", "note-Bb4_eighth..": "Bb4z..", "note-Bb4_eighth_fermata": "Bb4z^", "note-Bb4_half": "Bb4H", "note-Bb4_half.": "Bb4H.", "note-Bb4_half._fermata": "Bb4H.^", "note-Bb4_half_fermata": "Bb4H^", "note-Bb4_hundred_twenty_eighth": "Bb4h", "note-Bb4_quadruple_whole": "Bb4Q", "note-Bb4_quarter": "Bb4q", "note-Bb4_quarter.": "Bb4q.", "note-Bb4_quarter..": "Bb4q..", "note-Bb4_quarter._fermata": "Bb4q.^", "note-Bb4_quarter_fermata": "Bb4q^", "note-Bb4_sixteenth": "Bb4S", "note-Bb4_sixteenth.": "Bb4S.", "note-Bb4_sixty_fourth": "Bb4s", "note-Bb4_sixty_fourth.": "Bb4s.", "note-Bb4_thirty_second": "Bb4T", "note-Bb4_thirty_second.": "Bb4T.", "note-Bb4_whole": "Bb4W", "note-Bb4_whole.": "Bb4W.", "note-Bb4_whole._fermata": "Bb4W.^", "note-Bb4_whole_fermata": "Bb4W^", "note-Bb5_double_whole": "Bb5w", "note-Bb5_eighth": "Bb5z", "note-Bb5_eighth.": "Bb5z.", "note-Bb5_eighth..": "Bb5z..", "note-Bb5_half": "Bb5H", "note-Bb5_half.": "Bb5H.", "note-Bb5_half_fermata": "Bb5H^", "note-Bb5_quarter": "Bb5q", "note-Bb5_quarter.": "Bb5q.", "note-Bb5_quarter..": "Bb5q..", "note-Bb5_quarter_fermata": "Bb5q^", "note-Bb5_sixteenth": "Bb5S", "note-Bb5_sixteenth.": "Bb5S.", "note-Bb5_sixty_fourth": "Bb5s", "note-Bb5_sixty_fourth.": "Bb5s.", "note-Bb5_thirty_second": "Bb5T", "note-Bb5_thirty_second.": "Bb5T.", "note-Bb5_whole": "Bb5W", "note-Bb5_whole.": "Bb5W.", "note-Bb5_whole_fermata": "Bb5W^", "note-Bb6_eighth": "Bb6z", "note-Bb6_eighth.": "Bb6z.", "note-Bb6_half": "Bb6H", "note-Bb6_half.": "Bb6H.", "note-Bb6_quarter": "Bb6q", "note-Bb6_quarter.": "Bb6q.", "note-Bb6_sixteenth": "Bb6S", "note-Bb6_sixty_fourth": "Bb6s", "note-Bb6_thirty_second": "Bb6T", "note-Bb6_whole": "Bb6W", "note-Bb7_eighth": "Bb7z", "note-Bb7_quarter": "Bb7q", "note-Bb7_sixteenth": "Bb7S", "note-Bb7_thirty_second": "Bb7T", "note-Bb8_eighth": "Bb8z", "note-Bb8_eighth.": "Bb8z.", "note-Bb8_quarter": "Bb8q", "note-Bbb0_sixteenth": "Bbb0S", "note-Bbb1_eighth": "Bbb1z", "note-Bbb1_half": "Bbb1H", "note-Bbb1_half.": "Bbb1H.", "note-Bbb1_quarter": "Bbb1q", "note-Bbb1_quarter.": "Bbb1q.", "note-Bbb1_sixteenth": "Bbb1S", "note-Bbb1_whole": "Bbb1W", "note-Bbb2_eighth": "Bbb2z", "note-Bbb2_eighth.": "Bbb2z.", "note-Bbb2_half": "Bbb2H", "note-Bbb2_half.": "Bbb2H.", "note-Bbb2_quarter": "Bbb2q", "note-Bbb2_quarter.": "Bbb2q.", "note-Bbb2_sixteenth": "Bbb2S", "note-Bbb2_thirty_second": "Bbb2T", "note-Bbb2_thirty_second.": "Bbb2T.", "note-Bbb2_whole": "Bbb2W", "note-Bbb3_eighth": "Bbb3z", "note-Bbb3_half": "Bbb3H", "note-Bbb3_half.": "Bbb3H.", "note-Bbb3_quarter": "Bbb3q", "note-Bbb3_quarter.": "Bbb3q.", "note-Bbb3_sixteenth": "Bbb3S", "note-Bbb3_sixteenth.": "Bbb3S.", "note-Bbb3_thirty_second": "Bbb3T", "note-Bbb3_whole": "Bbb3W", "note-Bbb4_eighth": "Bbb4z", "note-Bbb4_eighth.": "Bbb4z.", "note-Bbb4_half": "Bbb4H", "note-Bbb4_half.": "Bbb4H.", "note-Bbb4_quarter": "Bbb4q", "note-Bbb4_quarter.": "Bbb4q.", "note-Bbb4_sixteenth": "Bbb4S", "note-Bbb4_thirty_second": "Bbb4T", "note-Bbb4_whole": "Bbb4W", "note-Bbb5_eighth": "Bbb5z", "note-Bbb5_eighth.": "Bbb5z.", "note-Bbb5_half": "Bbb5H", "note-Bbb5_half.": "Bbb5H.", "note-Bbb5_quarter": "Bbb5q", "note-Bbb5_quarter.": "Bbb5q.", "note-Bbb5_sixteenth": "Bbb5S", "note-Bbb5_thirty_second": "Bbb5T", "note-Bbb5_whole": "Bbb5W", "note-Bbb6_eighth": "Bbb6z", "note-Bbb6_eighth.": "Bbb6z.", "note-Bbb6_sixteenth": "Bbb6S", "note-Bbb6_sixty_fourth": "Bbb6s", "note-Bbb6_thirty_second": "Bbb6T", "note-C##1_sixteenth": "C##1S", "note-C##2_eighth": "C##2z", "note-C##2_quarter": "C##2q", "note-C##2_quarter.": "C##2q.", "note-C##2_sixteenth": "C##2S", "note-C##2_whole": "C##2W", "note-C##3_eighth": "C##3z", "note-C##3_half": "C##3H", "note-C##3_quarter": "C##3q", "note-C##3_quarter.": "C##3q.", "note-C##3_sixteenth": "C##3S", "note-C##3_sixty_fourth": "C##3s", "note-C##4_eighth": "C##4z", "note-C##4_eighth.": "C##4z.", "note-C##4_half": "C##4H", "note-C##4_half.": "C##4H.", "note-C##4_quarter": "C##4q", "note-C##4_quarter.": "C##4q.", "note-C##4_sixteenth": "C##4S", "note-C##4_sixty_fourth": "C##4s", "note-C##4_thirty_second": "C##4T", "note-C##4_whole": "C##4W", "note-C##5_eighth": "C##5z", "note-C##5_eighth.": "C##5z.", "note-C##5_half.": "C##5H.", "note-C##5_quarter": "C##5q", "note-C##5_quarter.": "C##5q.", "note-C##5_sixteenth": "C##5S", "note-C##5_thirty_second": "C##5T", "note-C##5_whole": "C##5W", "note-C##6_eighth": "C##6z", "note-C##6_quarter": "C##6q", "note-C##6_quarter.": "C##6q.", "note-C##6_sixteenth": "C##6S", "note-C##6_thirty_second": "C##6T", "note-C##7_eighth": "C##7z", "note-C#1_eighth": "C#1z", "note-C#1_eighth.": "C#1z.", "note-C#1_half.": "C#1H.", "note-C#1_quarter": "C#1q", "note-C#1_sixteenth": "C#1S", "note-C#1_thirty_second": "C#1T", "note-C#1_whole": "C#1W", "note-C#2_eighth": "C#2z", "note-C#2_eighth.": "C#2z.", "note-C#2_half": "C#2H", "note-C#2_half.": "C#2H.", "note-C#2_quarter": "C#2q", "note-C#2_quarter.": "C#2q.", "note-C#2_sixteenth": "C#2S", "note-C#2_sixteenth.": "C#2S.", "note-C#2_sixty_fourth": "C#2s", "note-C#2_thirty_second": "C#2T", "note-C#2_whole": "C#2W", "note-C#2_whole.": "C#2W.", "note-C#3_double_whole": "C#3w", "note-C#3_eighth": "C#3z", "note-C#3_eighth.": "C#3z.", "note-C#3_half": "C#3H", "note-C#3_half.": "C#3H.", "note-C#3_quarter": "C#3q", "note-C#3_quarter.": "C#3q.", "note-C#3_sixteenth": "C#3S", "note-C#3_sixteenth.": "C#3S.", "note-C#3_sixty_fourth": "C#3s", "note-C#3_thirty_second": "C#3T", "note-C#3_whole": "C#3W", "note-C#4_eighth": "C#4z", "note-C#4_eighth.": "C#4z.", "note-C#4_eighth..": "C#4z..", "note-C#4_eighth_fermata": "C#4z^", "note-C#4_half": "C#4H", "note-C#4_half.": "C#4H.", "note-C#4_half_fermata": "C#4H^", "note-C#4_quadruple_whole_fermata": "C#4Q^", "note-C#4_quarter": "C#4q", "note-C#4_quarter.": "C#4q.", "note-C#4_quarter..": "C#4q..", "note-C#4_quarter._fermata": "C#4q.^", "note-C#4_quarter_fermata": "C#4q^", "note-C#4_sixteenth": "C#4S", "note-C#4_sixteenth.": "C#4S.", "note-C#4_sixty_fourth": "C#4s", "note-C#4_thirty_second": "C#4T", "note-C#4_thirty_second.": "C#4T.", "note-C#4_whole": "C#4W", "note-C#4_whole.": "C#4W.", "note-C#4_whole_fermata": "C#4W^", "note-C#5_double_whole": "C#5w", "note-C#5_eighth": "C#5z", "note-C#5_eighth.": "C#5z.", "note-C#5_eighth..": "C#5z..", "note-C#5_eighth._fermata": "C#5z.^", "note-C#5_eighth_fermata": "C#5z^", "note-C#5_half": "C#5H", "note-C#5_half.": "C#5H.", "note-C#5_half._fermata": "C#5H.^", "note-C#5_half_fermata": "C#5H^", "note-C#5_quarter": "C#5q", "note-C#5_quarter.": "C#5q.", "note-C#5_quarter..": "C#5q..", "note-C#5_quarter._fermata": "C#5q.^", "note-C#5_quarter_fermata": "C#5q^", "note-C#5_sixteenth": "C#5S", "note-C#5_sixteenth.": "C#5S.", "note-C#5_sixteenth._fermata": "C#5S.^", "note-C#5_sixty_fourth": "C#5s", "note-C#5_sixty_fourth.": "C#5s.", "note-C#5_thirty_second": "C#5T", "note-C#5_thirty_second.": "C#5T.", "note-C#5_whole": "C#5W", "note-C#5_whole.": "C#5W.", "note-C#5_whole_fermata": "C#5W^", "note-C#6_eighth": "C#6z", "note-C#6_eighth.": "C#6z.", "note-C#6_half": "C#6H", "note-C#6_half.": "C#6H.", "note-C#6_half_fermata": "C#6H^", "note-C#6_quarter": "C#6q", "note-C#6_quarter.": "C#6q.", "note-C#6_quarter..": "C#6q..", "note-C#6_sixteenth": "C#6S", "note-C#6_sixteenth.": "C#6S.", "note-C#6_sixty_fourth": "C#6s", "note-C#6_thirty_second": "C#6T", "note-C#6_whole": "C#6W", "note-C#6_whole_fermata": "C#6W^", "note-C#7_eighth": "C#7z", "note-C#7_eighth.": "C#7z.", "note-C#7_half": "C#7H", "note-C#7_half.": "C#7H.", "note-C#7_quarter": "C#7q", "note-C#7_quarter.": "C#7q.", "note-C#7_sixteenth": "C#7S", "note-C#7_sixty_fourth": "C#7s", "note-C#7_thirty_second": "C#7T", "note-C#7_whole": "C#7W", "note-C#8_sixteenth": "C#8S", "note-C1_eighth": "C1z", "note-C1_eighth.": "C1z.", "note-C1_half": "C1H", "note-C1_half.": "C1H.", "note-C1_quarter": "C1q", "note-C1_quarter.": "C1q.", "note-C1_sixteenth": "C1S", "note-C1_sixteenth.": "C1S.", "note-C1_thirty_second": "C1T", "note-C1_whole": "C1W", "note-C2_breve": "C2Y", "note-C2_double_whole.": "C2w.", "note-C2_eighth": "C2z", "note-C2_eighth.": "C2z.", "note-C2_half": "C2H", "note-C2_half.": "C2H.", "note-C2_half_fermata": "C2H^", "note-C2_hundred_twenty_eighth": "C2h", "note-C2_quarter": "C2q", "note-C2_quarter.": "C2q.", "note-C2_sixteenth": "C2S", "note-C2_sixteenth.": "C2S.", "note-C2_sixty_fourth": "C2s", "note-C2_sixty_fourth.": "C2s.", "note-C2_thirty_second": "C2T", "note-C2_thirty_second.": "C2T.", "note-C2_whole": "C2W", "note-C2_whole.": "C2W.", "note-C3_breve": "C3Y", "note-C3_double_whole": "C3w", "note-C3_double_whole.": "C3w.", "note-C3_double_whole_fermata": "C3w^", "note-C3_eighth": "C3z", "note-C3_eighth.": "C3z.", "note-C3_half": "C3H", "note-C3_half.": "C3H.", "note-C3_half_fermata": "C3H^", "note-C3_hundred_twenty_eighth": "C3h", "note-C3_quadruple_whole": "C3Q", "note-C3_quadruple_whole.": "C3Q.", "note-C3_quarter": "C3q", "note-C3_quarter.": "C3q.", "note-C3_quarter_fermata": "C3q^", "note-C3_sixteenth": "C3S", "note-C3_sixteenth.": "C3S.", "note-C3_sixty_fourth": "C3s", "note-C3_sixty_fourth.": "C3s.", "note-C3_thirty_second": "C3T", "note-C3_thirty_second.": "C3T.", "note-C3_whole": "C3W", "note-C3_whole.": "C3W.", "note-C3_whole_fermata": "C3W^", "note-C4_breve": "C4Y", "note-C4_double_whole": "C4w", "note-C4_double_whole.": "C4w.", "note-C4_double_whole_fermata": "C4w^", "note-C4_eighth": "C4z", "note-C4_eighth.": "C4z.", "note-C4_eighth..": "C4z..", "note-C4_eighth_fermata": "C4z^", "note-C4_half": "C4H", "note-C4_half.": "C4H.", "note-C4_half._fermata": "C4H.^", "note-C4_half_fermata": "C4H^", "note-C4_hundred_twenty_eighth": "C4h", "note-C4_quadruple_whole": "C4Q", "note-C4_quadruple_whole.": "C4Q.", "note-C4_quarter": "C4q", "note-C4_quarter.": "C4q.", "note-C4_quarter..": "C4q..", "note-C4_quarter._fermata": "C4q.^", "note-C4_quarter_fermata": "C4q^", "note-C4_sixteenth": "C4S", "note-C4_sixteenth.": "C4S.", "note-C4_sixty_fourth": "C4s", "note-C4_sixty_fourth.": "C4s.", "note-C4_thirty_second": "C4T", "note-C4_thirty_second.": "C4T.", "note-C4_whole": "C4W", "note-C4_whole.": "C4W.", "note-C4_whole_fermata": "C4W^", "note-C5_breve": "C5Y", "note-C5_double_whole": "C5w", "note-C5_double_whole.": "C5w.", "note-C5_double_whole._fermata": "C5w.^", "note-C5_double_whole_fermata": "C5w^", "note-C5_eighth": "C5z", "note-C5_eighth.": "C5z.", "note-C5_eighth..": "C5z..", "note-C5_eighth._fermata": "C5z.^", "note-C5_eighth_fermata": "C5z^", "note-C5_half": "C5H", "note-C5_half.": "C5H.", "note-C5_half._fermata": "C5H.^", "note-C5_half_fermata": "C5H^", "note-C5_hundred_twenty_eighth": "C5h", "note-C5_quadruple_whole": "C5Q", "note-C5_quadruple_whole.": "C5Q.", "note-C5_quadruple_whole_fermata": "C5Q^", "note-C5_quarter": "C5q", "note-C5_quarter.": "C5q.", "note-C5_quarter..": "C5q..", "note-C5_quarter._fermata": "C5q.^", "note-C5_quarter_fermata": "C5q^", "note-C5_sixteenth": "C5S", "note-C5_sixteenth.": "C5S.", "note-C5_sixteenth_fermata": "C5S^", "note-C5_sixty_fourth": "C5s", "note-C5_sixty_fourth.": "C5s.", "note-C5_thirty_second": "C5T", "note-C5_thirty_second.": "C5T.", "note-C5_whole": "C5W", "note-C5_whole.": "C5W.", "note-C5_whole._fermata": "C5W.^", "note-C5_whole_fermata": "C5W^", "note-C6_breve": "C6Y", "note-C6_eighth": "C6z", "note-C6_eighth.": "C6z.", "note-C6_eighth..": "C6z..", "note-C6_half": "C6H", "note-C6_half.": "C6H.", "note-C6_half..": "C6H..", "note-C6_half._fermata": "C6H.^", "note-C6_half_fermata": "C6H^", "note-C6_hundred_twenty_eighth": "C6h", "note-C6_quarter": "C6q", "note-C6_quarter.": "C6q.", "note-C6_quarter..": "C6q..", "note-C6_sixteenth": "C6S", "note-C6_sixteenth.": "C6S.", "note-C6_sixty_fourth": "C6s", "note-C6_sixty_fourth.": "C6s.", "note-C6_thirty_second": "C6T", "note-C6_thirty_second.": "C6T.", "note-C6_whole": "C6W", "note-C6_whole.": "C6W.", "note-C6_whole_fermata": "C6W^", "note-C7_eighth": "C7z", "note-C7_eighth.": "C7z.", "note-C7_half": "C7H", "note-C7_half.": "C7H.", "note-C7_hundred_twenty_eighth": "C7h", "note-C7_quarter": "C7q", "note-C7_quarter.": "C7q.", "note-C7_sixteenth": "C7S", "note-C7_sixteenth.": "C7S.", "note-C7_sixty_fourth": "C7s", "note-C7_sixty_fourth.": "C7s.", "note-C7_thirty_second": "C7T", "note-C7_thirty_second.": "C7T.", "note-C7_whole": "C7W", "note-C7_whole.": "C7W.", "note-C8_breve": "C8Y", "note-C8_eighth": "C8z", "note-C8_eighth.": "C8z.", "note-C8_half.": "C8H.", "note-C8_hundred_twenty_eighth": "C8h", "note-C8_quarter": "C8q", "note-C8_sixteenth": "C8S", "note-C8_sixteenth.": "C8S.", "note-C8_sixty_fourth": "C8s", "note-C8_thirty_second": "C8T", "note-C9_eighth": "C9z", "note-C9_quarter": "C9q", "note-C9_sixteenth.": "C9S.", "note-CN1_eighth": "CN1z", "note-CN1_half": "CN1H", "note-CN1_half.": "CN1H.", "note-CN1_quarter": "CN1q", "note-CN1_quarter.": "CN1q.", "note-CN1_sixteenth": "CN1S", "note-CN1_thirty_second": "CN1T", "note-CN1_whole": "CN1W", "note-CN2_eighth": "CN2z", "note-CN2_eighth.": "CN2z.", "note-CN2_half": "CN2H", "note-CN2_half.": "CN2H.", "note-CN2_quarter": "CN2q", "note-CN2_quarter.": "CN2q.", "note-CN2_sixteenth": "CN2S", "note-CN2_sixty_fourth": "CN2s", "note-CN2_thirty_second": "CN2T", "note-CN2_whole": "CN2W", "note-CN2_whole.": "CN2W.", "note-CN3_eighth": "CN3z", "note-CN3_eighth.": "CN3z.", "note-CN3_half": "CN3H", "note-CN3_half.": "CN3H.", "note-CN3_quarter": "CN3q", "note-CN3_quarter.": "CN3q.", "note-CN3_sixteenth": "CN3S", "note-CN3_sixteenth.": "CN3S.", "note-CN3_sixty_fourth": "CN3s", "note-CN3_thirty_second": "CN3T", "note-CN3_whole": "CN3W", "note-CN3_whole.": "CN3W.", "note-CN4_eighth": "CN4z", "note-CN4_eighth.": "CN4z.", "note-CN4_half": "CN4H", "note-CN4_half.": "CN4H.", "note-CN4_hundred_twenty_eighth": "CN4h", "note-CN4_quarter": "CN4q", "note-CN4_quarter.": "CN4q.", "note-CN4_sixteenth": "CN4S", "note-CN4_sixteenth.": "CN4S.", "note-CN4_sixty_fourth": "CN4s", "note-CN4_thirty_second": "CN4T", "note-CN4_whole": "CN4W", "note-CN5_eighth": "CN5z", "note-CN5_eighth.": "CN5z.", "note-CN5_half": "CN5H", "note-CN5_half.": "CN5H.", "note-CN5_quarter": "CN5q", "note-CN5_quarter.": "CN5q.", "note-CN5_sixteenth": "CN5S", "note-CN5_sixteenth.": "CN5S.", "note-CN5_sixty_fourth": "CN5s", "note-CN5_thirty_second": "CN5T", "note-CN5_whole": "CN5W", "note-CN5_whole.": "CN5W.", "note-CN6_eighth": "CN6z", "note-CN6_eighth.": "CN6z.", "note-CN6_half": "CN6H", "note-CN6_half.": "CN6H.", "note-CN6_quarter": "CN6q", "note-CN6_quarter.": "CN6q.", "note-CN6_sixteenth": "CN6S", "note-CN6_sixteenth.": "CN6S.", "note-CN6_sixty_fourth": "CN6s", "note-CN6_thirty_second": "CN6T", "note-CN6_whole": "CN6W", "note-CN7_eighth": "CN7z", "note-CN7_eighth.": "CN7z.", "note-CN7_half": "CN7H", "note-CN7_half.": "CN7H.", "note-CN7_quarter": "CN7q", "note-CN7_quarter.": "CN7q.", "note-CN7_sixteenth": "CN7S", "note-CN7_sixty_fourth": "CN7s", "note-CN7_thirty_second": "CN7T", "note-CN8_eighth": "CN8z", "note-CN8_quarter": "CN8q", "note-CN8_sixteenth": "CN8S", "note-CN8_thirty_second": "CN8T", "note-Cb1_half": "Cb1H", "note-Cb1_sixteenth": "Cb1S", "note-Cb1_whole": "Cb1W", "note-Cb2_eighth": "Cb2z", "note-Cb2_eighth.": "Cb2z.", "note-Cb2_half": "Cb2H", "note-Cb2_half.": "Cb2H.", "note-Cb2_quarter": "Cb2q", "note-Cb2_quarter.": "Cb2q.", "note-Cb2_sixteenth": "Cb2S", "note-Cb2_thirty_second": "Cb2T", "note-Cb2_thirty_second.": "Cb2T.", "note-Cb2_whole": "Cb2W", "note-Cb3_eighth": "Cb3z", "note-Cb3_eighth.": "Cb3z.", "note-Cb3_half": "Cb3H", "note-Cb3_half.": "Cb3H.", "note-Cb3_quarter": "Cb3q", "note-Cb3_quarter.": "Cb3q.", "note-Cb3_sixteenth": "Cb3S", "note-Cb3_sixteenth.": "Cb3S.", "note-Cb3_sixty_fourth": "Cb3s", "note-Cb3_sixty_fourth.": "Cb3s.", "note-Cb3_thirty_second": "Cb3T", "note-Cb3_thirty_second.": "Cb3T.", "note-Cb3_whole": "Cb3W", "note-Cb4_eighth": "Cb4z", "note-Cb4_eighth.": "Cb4z.", "note-Cb4_half": "Cb4H", "note-Cb4_half.": "Cb4H.", "note-Cb4_hundred_twenty_eighth": "Cb4h", "note-Cb4_quarter": "Cb4q", "note-Cb4_quarter.": "Cb4q.", "note-Cb4_sixteenth": "Cb4S", "note-Cb4_sixteenth.": "Cb4S.", "note-Cb4_sixty_fourth": "Cb4s", "note-Cb4_sixty_fourth.": "Cb4s.", "note-Cb4_thirty_second": "Cb4T", "note-Cb4_thirty_second.": "Cb4T.", "note-Cb4_whole": "Cb4W", "note-Cb4_whole.": "Cb4W.", "note-Cb5_eighth": "Cb5z", "note-Cb5_eighth.": "Cb5z.", "note-Cb5_half": "Cb5H", "note-Cb5_half.": "Cb5H.", "note-Cb5_quarter": "Cb5q", "note-Cb5_quarter.": "Cb5q.", "note-Cb5_sixteenth": "Cb5S", "note-Cb5_sixteenth.": "Cb5S.", "note-Cb5_sixty_fourth": "Cb5s", "note-Cb5_thirty_second": "Cb5T", "note-Cb5_whole": "Cb5W", "note-Cb5_whole.": "Cb5W.", "note-Cb6_eighth": "Cb6z", "note-Cb6_eighth.": "Cb6z.", "note-Cb6_half": "Cb6H", "note-Cb6_half.": "Cb6H.", "note-Cb6_quarter": "Cb6q", "note-Cb6_quarter.": "Cb6q.", "note-Cb6_sixteenth": "Cb6S", "note-Cb6_sixteenth.": "Cb6S.", "note-Cb6_sixty_fourth": "Cb6s", "note-Cb6_thirty_second": "Cb6T", "note-Cb6_thirty_second.": "Cb6T.", "note-Cb6_whole": "Cb6W", "note-Cb6_whole.": "Cb6W.", "note-Cb7_eighth": "Cb7z", "note-Cb7_half": "Cb7H", "note-Cb7_half.": "Cb7H.", "note-Cb7_quarter": "Cb7q", "note-Cb7_quarter.": "Cb7q.", "note-Cb7_sixteenth": "Cb7S", "note-Cb7_sixteenth.": "Cb7S.", "note-Cb7_sixty_fourth": "Cb7s", "note-Cb7_thirty_second": "Cb7T", "note-Cb8_eighth": "Cb8z", "note-Cbb3_quarter": "Cbb3q", "note-D##1_sixteenth": "D##1S", "note-D##2_sixteenth": "D##2S", "note-D##3_eighth": "D##3z", "note-D##3_quarter": "D##3q", "note-D##3_sixteenth": "D##3S", "note-D##4_eighth": "D##4z", "note-D##4_half": "D##4H", "note-D##4_sixteenth": "D##4S", "note-D##5_eighth": "D##5z", "note-D##5_half": "D##5H", "note-D##5_half.": "D##5H.", "note-D##5_quarter": "D##5q", "note-D##5_sixteenth": "D##5S", "note-D##6_eighth": "D##6z", "note-D##6_sixteenth": "D##6S", "note-D##7_eighth": "D##7z", "note-D#1_eighth": "D#1z", "note-D#1_half": "D#1H", "note-D#1_quarter": "D#1q", "note-D#1_sixteenth": "D#1S", "note-D#1_thirty_second": "D#1T", "note-D#1_whole": "D#1W", "note-D#2_eighth": "D#2z", "note-D#2_eighth.": "D#2z.", "note-D#2_half": "D#2H", "note-D#2_half.": "D#2H.", "note-D#2_quarter": "D#2q", "note-D#2_quarter.": "D#2q.", "note-D#2_sixteenth": "D#2S", "note-D#2_sixty_fourth": "D#2s", "note-D#2_thirty_second": "D#2T", "note-D#2_whole": "D#2W", "note-D#2_whole.": "D#2W.", "note-D#3_eighth": "D#3z", "note-D#3_eighth.": "D#3z.", "note-D#3_half": "D#3H", "note-D#3_half.": "D#3H.", "note-D#3_half_fermata": "D#3H^", "note-D#3_quarter": "D#3q", "note-D#3_quarter.": "D#3q.", "note-D#3_sixteenth": "D#3S", "note-D#3_sixteenth.": "D#3S.", "note-D#3_sixty_fourth": "D#3s", "note-D#3_thirty_second": "D#3T", "note-D#3_whole": "D#3W", "note-D#3_whole.": "D#3W.", "note-D#4_breve": "D#4Y", "note-D#4_eighth": "D#4z", "note-D#4_eighth.": "D#4z.", "note-D#4_half": "D#4H", "note-D#4_half.": "D#4H.", "note-D#4_hundred_twenty_eighth": "D#4h", "note-D#4_quarter": "D#4q", "note-D#4_quarter.": "D#4q.", "note-D#4_sixteenth": "D#4S", "note-D#4_sixteenth.": "D#4S.", "note-D#4_sixty_fourth": "D#4s", "note-D#4_thirty_second": "D#4T", "note-D#4_thirty_second.": "D#4T.", "note-D#4_whole": "D#4W", "note-D#4_whole.": "D#4W.", "note-D#5_double_whole": "D#5w", "note-D#5_eighth": "D#5z", "note-D#5_eighth.": "D#5z.", "note-D#5_eighth..": "D#5z..", "note-D#5_eighth_fermata": "D#5z^", "note-D#5_half": "D#5H", "note-D#5_half.": "D#5H.", "note-D#5_half_fermata": "D#5H^", "note-D#5_quarter": "D#5q", "note-D#5_quarter.": "D#5q.", "note-D#5_quarter..": "D#5q..", "note-D#5_quarter_fermata": "D#5q^", "note-D#5_sixteenth": "D#5S", "note-D#5_sixteenth.": "D#5S.", "note-D#5_sixty_fourth": "D#5s", "note-D#5_thirty_second": "D#5T", "note-D#5_thirty_second.": "D#5T.", "note-D#5_whole": "D#5W", "note-D#5_whole.": "D#5W.", "note-D#5_whole_fermata": "D#5W^", "note-D#6_eighth": "D#6z", "note-D#6_eighth.": "D#6z.", "note-D#6_eighth..": "D#6z..", "note-D#6_half": "D#6H", "note-D#6_half.": "D#6H.", "note-D#6_quarter": "D#6q", "note-D#6_quarter.": "D#6q.", "note-D#6_sixteenth": "D#6S", "note-D#6_sixty_fourth": "D#6s", "note-D#6_thirty_second": "D#6T", "note-D#6_thirty_second.": "D#6T.", "note-D#6_whole": "D#6W", "note-D#7_eighth": "D#7z", "note-D#7_eighth.": "D#7z.", "note-D#7_half": "D#7H", "note-D#7_half.": "D#7H.", "note-D#7_quarter": "D#7q", "note-D#7_quarter.": "D#7q.", "note-D#7_sixteenth": "D#7S", "note-D#7_sixty_fourth": "D#7s", "note-D#7_thirty_second": "D#7T", "note-D#7_whole": "D#7W", "note-D#8_eighth": "D#8z", "note-D#8_quarter": "D#8q", "note-D#8_sixteenth": "D#8S", "note-D#9_sixteenth": "D#9S", "note-D1_eighth": "D1z", "note-D1_eighth.": "D1z.", "note-D1_half": "D1H", "note-D1_half.": "D1H.", "note-D1_hundred_twenty_eighth": "D1h", "note-D1_quarter": "D1q", "note-D1_quarter.": "D1q.", "note-D1_sixteenth": "D1S", "note-D1_sixteenth.": "D1S.", "note-D1_sixty_fourth": "D1s", "note-D1_thirty_second": "D1T", "note-D1_whole": "D1W", "note-D1_whole.": "D1W.", "note-D2_breve.": "D2Y.", "note-D2_double_whole": "D2w", "note-D2_eighth": "D2z", "note-D2_eighth.": "D2z.", "note-D2_half": "D2H", "note-D2_half.": "D2H.", "note-D2_half_fermata": "D2H^", "note-D2_hundred_twenty_eighth": "D2h", "note-D2_quarter": "D2q", "note-D2_quarter.": "D2q.", "note-D2_quarter._fermata": "D2q.^", "note-D2_sixteenth": "D2S", "note-D2_sixteenth.": "D2S.", "note-D2_sixty_fourth": "D2s", "note-D2_sixty_fourth.": "D2s.", "note-D2_thirty_second": "D2T", "note-D2_thirty_second.": "D2T.", "note-D2_whole": "D2W", "note-D2_whole.": "D2W.", "note-D3_breve": "D3Y", "note-D3_breve.": "D3Y.", "note-D3_double_whole": "D3w", "note-D3_double_whole.": "D3w.", "note-D3_double_whole_fermata": "D3w^", "note-D3_eighth": "D3z", "note-D3_eighth.": "D3z.", "note-D3_eighth_fermata": "D3z^", "note-D3_half": "D3H", "note-D3_half.": "D3H.", "note-D3_half_fermata": "D3H^", "note-D3_hundred_twenty_eighth": "D3h", "note-D3_quadruple_whole": "D3Q", "note-D3_quadruple_whole_fermata": "D3Q^", "note-D3_quarter": "D3q", "note-D3_quarter.": "D3q.", "note-D3_quarter..": "D3q..", "note-D3_quarter._fermata": "D3q.^", "note-D3_quarter_fermata": "D3q^", "note-D3_sixteenth": "D3S", "note-D3_sixteenth.": "D3S.", "note-D3_sixty_fourth": "D3s", "note-D3_sixty_fourth.": "D3s.", "note-D3_thirty_second": "D3T", "note-D3_thirty_second.": "D3T.", "note-D3_whole": "D3W", "note-D3_whole.": "D3W.", "note-D3_whole_fermata": "D3W^", "note-D4_breve": "D4Y", "note-D4_double_whole": "D4w", "note-D4_double_whole.": "D4w.", "note-D4_double_whole_fermata": "D4w^", "note-D4_eighth": "D4z", "note-D4_eighth.": "D4z.", "note-D4_eighth..": "D4z..", "note-D4_eighth._fermata": "D4z.^", "note-D4_eighth_fermata": "D4z^", "note-D4_half": "D4H", "note-D4_half.": "D4H.", "note-D4_half._fermata": "D4H.^", "note-D4_half_fermata": "D4H^", "note-D4_hundred_twenty_eighth": "D4h", "note-D4_quadruple_whole": "D4Q", "note-D4_quadruple_whole_fermata": "D4Q^", "note-D4_quarter": "D4q", "note-D4_quarter.": "D4q.", "note-D4_quarter..": "D4q..", "note-D4_quarter.._fermata": "D4q..^", "note-D4_quarter._fermata": "D4q.^", "note-D4_quarter_fermata": "D4q^", "note-D4_sixteenth": "D4S", "note-D4_sixteenth.": "D4S.", "note-D4_sixteenth_fermata": "D4S^", "note-D4_sixty_fourth": "D4s", "note-D4_sixty_fourth.": "D4s.", "note-D4_thirty_second": "D4T", "note-D4_thirty_second.": "D4T.", "note-D4_whole": "D4W", "note-D4_whole.": "D4W.", "note-D4_whole_fermata": "D4W^", "note-D5_breve": "D5Y", "note-D5_double_whole": "D5w", "note-D5_double_whole.": "D5w.", "note-D5_double_whole_fermata": "D5w^", "note-D5_eighth": "D5z", "note-D5_eighth.": "D5z.", "note-D5_eighth..": "D5z..", "note-D5_eighth._fermata": "D5z.^", "note-D5_eighth_fermata": "D5z^", "note-D5_half": "D5H", "note-D5_half.": "D5H.", "note-D5_half._fermata": "D5H.^", "note-D5_half_fermata": "D5H^", "note-D5_hundred_twenty_eighth": "D5h", "note-D5_long": "D5long", "note-D5_quadruple_whole": "D5Q", "note-D5_quadruple_whole_fermata": "D5Q^", "note-D5_quarter": "D5q", "note-D5_quarter.": "D5q.", "note-D5_quarter..": "D5q..", "note-D5_quarter._fermata": "D5q.^", "note-D5_quarter_fermata": "D5q^", "note-D5_sixteenth": "D5S", "note-D5_sixteenth.": "D5S.", "note-D5_sixty_fourth": "D5s", "note-D5_sixty_fourth.": "D5s.", "note-D5_thirty_second": "D5T", "note-D5_thirty_second.": "D5T.", "note-D5_whole": "D5W", "note-D5_whole.": "D5W.", "note-D5_whole._fermata": "D5W.^", "note-D5_whole_fermata": "D5W^", "note-D6_eighth": "D6z", "note-D6_eighth.": "D6z.", "note-D6_eighth..": "D6z..", "note-D6_eighth_fermata": "D6z^", "note-D6_half": "D6H", "note-D6_half.": "D6H.", "note-D6_half..": "D6H..", "note-D6_half_fermata": "D6H^", "note-D6_hundred_twenty_eighth": "D6h", "note-D6_quarter": "D6q", "note-D6_quarter.": "D6q.", "note-D6_quarter..": "D6q..", "note-D6_quarter._fermata": "D6q.^", "note-D6_quarter_fermata": "D6q^", "note-D6_sixteenth": "D6S", "note-D6_sixteenth.": "D6S.", "note-D6_sixty_fourth": "D6s", "note-D6_sixty_fourth.": "D6s.", "note-D6_thirty_second": "D6T", "note-D6_thirty_second.": "D6T.", "note-D6_whole": "D6W", "note-D6_whole.": "D6W.", "note-D6_whole_fermata": "D6W^", "note-D7_eighth": "D7z", "note-D7_eighth.": "D7z.", "note-D7_half": "D7H", "note-D7_half.": "D7H.", "note-D7_hundred_twenty_eighth": "D7h", "note-D7_quarter": "D7q", "note-D7_quarter.": "D7q.", "note-D7_sixteenth": "D7S", "note-D7_sixteenth.": "D7S.", "note-D7_sixty_fourth": "D7s", "note-D7_sixty_fourth.": "D7s.", "note-D7_thirty_second": "D7T", "note-D7_thirty_second.": "D7T.", "note-D7_whole": "D7W", "note-D7_whole.": "D7W.", "note-D8_eighth": "D8z", "note-D8_hundred_twenty_eighth": "D8h", "note-D8_quarter": "D8q", "note-D8_sixteenth": "D8S", "note-D8_sixteenth.": "D8S.", "note-D8_sixty_fourth": "D8s", "note-D8_thirty_second": "D8T", "note-DN1_eighth": "DN1z", "note-DN1_eighth.": "DN1z.", "note-DN1_half": "DN1H", "note-DN1_half.": "DN1H.", "note-DN1_quarter": "DN1q", "note-DN1_quarter.": "DN1q.", "note-DN1_sixteenth": "DN1S", "note-DN1_thirty_second": "DN1T", "note-DN1_whole": "DN1W", "note-DN2_eighth": "DN2z", "note-DN2_eighth.": "DN2z.", "note-DN2_half": "DN2H", "note-DN2_half.": "DN2H.", "note-DN2_quarter": "DN2q", "note-DN2_quarter.": "DN2q.", "note-DN2_sixteenth": "DN2S", "note-DN2_sixty_fourth": "DN2s", "note-DN2_thirty_second": "DN2T", "note-DN2_thirty_second.": "DN2T.", "note-DN2_whole": "DN2W", "note-DN2_whole.": "DN2W.", "note-DN3_eighth": "DN3z", "note-DN3_eighth.": "DN3z.", "note-DN3_half": "DN3H", "note-DN3_half.": "DN3H.", "note-DN3_quarter": "DN3q", "note-DN3_quarter.": "DN3q.", "note-DN3_sixteenth": "DN3S", "note-DN3_sixteenth.": "DN3S.", "note-DN3_sixty_fourth": "DN3s", "note-DN3_sixty_fourth.": "DN3s.", "note-DN3_thirty_second": "DN3T", "note-DN3_thirty_second.": "DN3T.", "note-DN3_whole": "DN3W", "note-DN3_whole.": "DN3W.", "note-DN4_eighth": "DN4z", "note-DN4_eighth.": "DN4z.", "note-DN4_half": "DN4H", "note-DN4_half.": "DN4H.", "note-DN4_hundred_twenty_eighth": "DN4h", "note-DN4_quarter": "DN4q", "note-DN4_quarter.": "DN4q.", "note-DN4_sixteenth": "DN4S", "note-DN4_sixteenth.": "DN4S.", "note-DN4_sixty_fourth": "DN4s", "note-DN4_sixty_fourth.": "DN4s.", "note-DN4_thirty_second": "DN4T", "note-DN4_whole": "DN4W", "note-DN5_eighth": "DN5z", "note-DN5_eighth.": "DN5z.", "note-DN5_half": "DN5H", "note-DN5_half.": "DN5H.", "note-DN5_hundred_twenty_eighth": "DN5h", "note-DN5_quarter": "DN5q", "note-DN5_quarter.": "DN5q.", "note-DN5_sixteenth": "DN5S", "note-DN5_sixteenth.": "DN5S.", "note-DN5_sixty_fourth": "DN5s", "note-DN5_sixty_fourth.": "DN5s.", "note-DN5_thirty_second": "DN5T", "note-DN5_thirty_second.": "DN5T.", "note-DN5_whole": "DN5W", "note-DN5_whole.": "DN5W.", "note-DN6_eighth": "DN6z", "note-DN6_eighth.": "DN6z.", "note-DN6_half": "DN6H", "note-DN6_half.": "DN6H.", "note-DN6_quarter": "DN6q", "note-DN6_quarter.": "DN6q.", "note-DN6_sixteenth": "DN6S", "note-DN6_sixteenth.": "DN6S.", "note-DN6_sixty_fourth": "DN6s", "note-DN6_thirty_second": "DN6T", "note-DN6_thirty_second.": "DN6T.", "note-DN6_whole": "DN6W", "note-DN6_whole.": "DN6W.", "note-DN7_eighth": "DN7z", "note-DN7_eighth.": "DN7z.", "note-DN7_half": "DN7H", "note-DN7_half.": "DN7H.", "note-DN7_quarter": "DN7q", "note-DN7_sixteenth": "DN7S", "note-DN7_sixty_fourth": "DN7s", "note-DN7_thirty_second": "DN7T", "note-DN7_whole": "DN7W", "note-DN7_whole.": "DN7W.", "note-Db1_eighth": "Db1z", "note-Db1_eighth.": "Db1z.", "note-Db1_half": "Db1H", "note-Db1_quarter": "Db1q", "note-Db1_sixteenth": "Db1S", "note-Db1_whole": "Db1W", "note-Db2_eighth": "Db2z", "note-Db2_eighth.": "Db2z.", "note-Db2_half": "Db2H", "note-Db2_half.": "Db2H.", "note-Db2_hundred_twenty_eighth": "Db2h", "note-Db2_quarter": "Db2q", "note-Db2_quarter.": "Db2q.", "note-Db2_sixteenth": "Db2S", "note-Db2_sixteenth.": "Db2S.", "note-Db2_sixty_fourth": "Db2s", "note-Db2_sixty_fourth.": "Db2s.", "note-Db2_thirty_second": "Db2T", "note-Db2_thirty_second.": "Db2T.", "note-Db2_whole": "Db2W", "note-Db3_breve": "Db3Y", "note-Db3_eighth": "Db3z", "note-Db3_eighth.": "Db3z.", "note-Db3_half": "Db3H", "note-Db3_half.": "Db3H.", "note-Db3_quarter": "Db3q", "note-Db3_quarter.": "Db3q.", "note-Db3_sixteenth": "Db3S", "note-Db3_sixteenth.": "Db3S.", "note-Db3_sixty_fourth": "Db3s", "note-Db3_thirty_second": "Db3T", "note-Db3_whole": "Db3W", "note-Db3_whole.": "Db3W.", "note-Db4_breve": "Db4Y", "note-Db4_double_whole": "Db4w", "note-Db4_eighth": "Db4z", "note-Db4_eighth.": "Db4z.", "note-Db4_eighth..": "Db4z..", "note-Db4_half": "Db4H", "note-Db4_half.": "Db4H.", "note-Db4_hundred_twenty_eighth": "Db4h", "note-Db4_quarter": "Db4q", "note-Db4_quarter.": "Db4q.", "note-Db4_sixteenth": "Db4S", "note-Db4_sixteenth.": "Db4S.", "note-Db4_sixty_fourth": "Db4s", "note-Db4_sixty_fourth.": "Db4s.", "note-Db4_thirty_second": "Db4T", "note-Db4_thirty_second.": "Db4T.", "note-Db4_whole": "Db4W", "note-Db4_whole.": "Db4W.", "note-Db4_whole._fermata": "Db4W.^", "note-Db5_breve": "Db5Y", "note-Db5_double_whole": "Db5w", "note-Db5_eighth": "Db5z", "note-Db5_eighth.": "Db5z.", "note-Db5_eighth..": "Db5z..", "note-Db5_half": "Db5H", "note-Db5_half.": "Db5H.", "note-Db5_half_fermata": "Db5H^", "note-Db5_quarter": "Db5q", "note-Db5_quarter.": "Db5q.", "note-Db5_quarter..": "Db5q..", "note-Db5_sixteenth": "Db5S", "note-Db5_sixteenth.": "Db5S.", "note-Db5_sixty_fourth": "Db5s", "note-Db5_thirty_second": "Db5T", "note-Db5_thirty_second.": "Db5T.", "note-Db5_whole": "Db5W", "note-Db5_whole.": "Db5W.", "note-Db5_whole_fermata": "Db5W^", "note-Db6_eighth": "Db6z", "note-Db6_eighth.": "Db6z.", "note-Db6_half": "Db6H", "note-Db6_half.": "Db6H.", "note-Db6_hundred_twenty_eighth": "Db6h", "note-Db6_quarter": "Db6q", "note-Db6_quarter.": "Db6q.", "note-Db6_sixteenth": "Db6S", "note-Db6_sixteenth.": "Db6S.", "note-Db6_sixty_fourth": "Db6s", "note-Db6_thirty_second": "Db6T", "note-Db6_whole": "Db6W", "note-Db6_whole.": "Db6W.", "note-Db7_eighth": "Db7z", "note-Db7_eighth.": "Db7z.", "note-Db7_half": "Db7H", "note-Db7_hundred_twenty_eighth": "Db7h", "note-Db7_quarter": "Db7q", "note-Db7_quarter.": "Db7q.", "note-Db7_sixteenth": "Db7S", "note-Db7_sixteenth.": "Db7S.", "note-Db7_sixty_fourth.": "Db7s.", "note-Db7_thirty_second": "Db7T", "note-Db7_thirty_second.": "Db7T.", "note-Db7_whole": "Db7W", "note-Dbb2_half.": "Dbb2H.", "note-Dbb3_eighth": "Dbb3z", "note-Dbb3_half.": "Dbb3H.", "note-Dbb3_sixteenth": "Dbb3S", "note-Dbb4_eighth": "Dbb4z", "note-Dbb4_half": "Dbb4H", "note-Dbb4_quarter": "Dbb4q", "note-Dbb4_sixteenth": "Dbb4S", "note-Dbb4_thirty_second": "Dbb4T", "note-Dbb5_eighth": "Dbb5z", "note-Dbb5_half": "Dbb5H", "note-Dbb5_sixteenth": "Dbb5S", "note-Dbb5_thirty_second": "Dbb5T", "note-Dbb6_half.": "Dbb6H.", "note-Dbb6_thirty_second": "Dbb6T", "note-E##3_eighth": "E##3z", "note-E##3_quarter": "E##3q", "note-E#1_eighth": "E#1z", "note-E#1_eighth.": "E#1z.", "note-E#1_half": "E#1H", "note-E#1_half.": "E#1H.", "note-E#1_quarter": "E#1q", "note-E#1_quarter.": "E#1q.", "note-E#1_sixteenth": "E#1S", "note-E#1_thirty_second": "E#1T", "note-E#2_eighth": "E#2z", "note-E#2_eighth.": "E#2z.", "note-E#2_half": "E#2H", "note-E#2_half.": "E#2H.", "note-E#2_quarter": "E#2q", "note-E#2_quarter.": "E#2q.", "note-E#2_sixteenth": "E#2S", "note-E#2_sixteenth.": "E#2S.", "note-E#2_sixty_fourth": "E#2s", "note-E#2_thirty_second": "E#2T", "note-E#2_whole": "E#2W", "note-E#2_whole.": "E#2W.", "note-E#3_eighth": "E#3z", "note-E#3_eighth.": "E#3z.", "note-E#3_half": "E#3H", "note-E#3_half.": "E#3H.", "note-E#3_quarter": "E#3q", "note-E#3_quarter.": "E#3q.", "note-E#3_sixteenth": "E#3S", "note-E#3_sixty_fourth": "E#3s", "note-E#3_thirty_second": "E#3T", "note-E#3_whole": "E#3W", "note-E#3_whole.": "E#3W.", "note-E#4_eighth": "E#4z", "note-E#4_eighth.": "E#4z.", "note-E#4_eighth..": "E#4z..", "note-E#4_half": "E#4H", "note-E#4_half.": "E#4H.", "note-E#4_quarter": "E#4q", "note-E#4_quarter.": "E#4q.", "note-E#4_sixteenth": "E#4S", "note-E#4_sixteenth.": "E#4S.", "note-E#4_sixty_fourth": "E#4s", "note-E#4_thirty_second": "E#4T", "note-E#4_thirty_second.": "E#4T.", "note-E#4_whole": "E#4W", "note-E#4_whole.": "E#4W.", "note-E#5_breve": "E#5Y", "note-E#5_eighth": "E#5z", "note-E#5_eighth.": "E#5z.", "note-E#5_half": "E#5H", "note-E#5_half.": "E#5H.", "note-E#5_half_fermata": "E#5H^", "note-E#5_quarter": "E#5q", "note-E#5_quarter.": "E#5q.", "note-E#5_sixteenth": "E#5S", "note-E#5_sixteenth.": "E#5S.", "note-E#5_sixty_fourth": "E#5s", "note-E#5_thirty_second": "E#5T", "note-E#5_thirty_second.": "E#5T.", "note-E#5_whole": "E#5W", "note-E#5_whole.": "E#5W.", "note-E#6_eighth": "E#6z", "note-E#6_eighth.": "E#6z.", "note-E#6_half": "E#6H", "note-E#6_half.": "E#6H.", "note-E#6_quarter": "E#6q", "note-E#6_quarter.": "E#6q.", "note-E#6_sixteenth": "E#6S", "note-E#6_sixteenth.": "E#6S.", "note-E#6_sixty_fourth": "E#6s", "note-E#6_thirty_second": "E#6T", "note-E#6_whole": "E#6W", "note-E#6_whole.": "E#6W.", "note-E#7_eighth": "E#7z", "note-E#7_half.": "E#7H.", "note-E#7_quarter": "E#7q", "note-E#7_quarter.": "E#7q.", "note-E#7_sixteenth": "E#7S", "note-E#7_thirty_second": "E#7T", "note-E0_eighth.": "E0z.", "note-E0_quarter": "E0q", "note-E0_whole": "E0W", "note-E1_eighth": "E1z", "note-E1_eighth.": "E1z.", "note-E1_half": "E1H", "note-E1_half.": "E1H.", "note-E1_quarter": "E1q", "note-E1_quarter.": "E1q.", "note-E1_sixteenth": "E1S", "note-E1_sixteenth.": "E1S.", "note-E1_sixty_fourth": "E1s", "note-E1_thirty_second": "E1T", "note-E1_whole": "E1W", "note-E1_whole.": "E1W.", "note-E2_breve": "E2Y", "note-E2_breve.": "E2Y.", "note-E2_eighth": "E2z", "note-E2_eighth.": "E2z.", "note-E2_half": "E2H", "note-E2_half.": "E2H.", "note-E2_hundred_twenty_eighth": "E2h", "note-E2_quarter": "E2q", "note-E2_quarter.": "E2q.", "note-E2_quarter_fermata": "E2q^", "note-E2_sixteenth": "E2S", "note-E2_sixteenth.": "E2S.", "note-E2_sixty_fourth": "E2s", "note-E2_sixty_fourth.": "E2s.", "note-E2_thirty_second": "E2T", "note-E2_thirty_second.": "E2T.", "note-E2_whole": "E2W", "note-E2_whole.": "E2W.", "note-E2_whole_fermata": "E2W^", "note-E3_breve": "E3Y", "note-E3_breve.": "E3Y.", "note-E3_double_whole": "E3w", "note-E3_double_whole.": "E3w.", "note-E3_double_whole_fermata": "E3w^", "note-E3_eighth": "E3z", "note-E3_eighth.": "E3z.", "note-E3_half": "E3H", "note-E3_half.": "E3H.", "note-E3_half._fermata": "E3H.^", "note-E3_half_fermata": "E3H^", "note-E3_hundred_twenty_eighth": "E3h", "note-E3_quadruple_whole": "E3Q", "note-E3_quarter": "E3q", "note-E3_quarter.": "E3q.", "note-E3_quarter._fermata": "E3q.^", "note-E3_quarter_fermata": "E3q^", "note-E3_sixteenth": "E3S", "note-E3_sixteenth.": "E3S.", "note-E3_sixty_fourth": "E3s", "note-E3_sixty_fourth.": "E3s.", "note-E3_thirty_second": "E3T", "note-E3_thirty_second.": "E3T.", "note-E3_whole": "E3W", "note-E3_whole.": "E3W.", "note-E3_whole_fermata": "E3W^", "note-E4_breve": "E4Y", "note-E4_double_whole": "E4w", "note-E4_double_whole.": "E4w.", "note-E4_double_whole._fermata": "E4w.^", "note-E4_double_whole_fermata": "E4w^", "note-E4_eighth": "E4z", "note-E4_eighth.": "E4z.", "note-E4_eighth..": "E4z..", "note-E4_eighth_fermata": "E4z^", "note-E4_half": "E4H", "note-E4_half.": "E4H.", "note-E4_half._fermata": "E4H.^", "note-E4_half_fermata": "E4H^", "note-E4_hundred_twenty_eighth": "E4h", "note-E4_quadruple_whole": "E4Q", "note-E4_quadruple_whole.": "E4Q.", "note-E4_quadruple_whole_fermata": "E4Q^", "note-E4_quarter": "E4q", "note-E4_quarter.": "E4q.", "note-E4_quarter..": "E4q..", "note-E4_quarter._fermata": "E4q.^", "note-E4_quarter_fermata": "E4q^", "note-E4_sixteenth": "E4S", "note-E4_sixteenth.": "E4S.", "note-E4_sixty_fourth": "E4s", "note-E4_sixty_fourth.": "E4s.", "note-E4_thirty_second": "E4T", "note-E4_thirty_second.": "E4T.", "note-E4_whole": "E4W", "note-E4_whole.": "E4W.", "note-E4_whole_fermata": "E4W^", "note-E5_breve": "E5Y", "note-E5_breve.": "E5Y.", "note-E5_double_whole": "E5w", "note-E5_double_whole.": "E5w.", "note-E5_double_whole_fermata": "E5w^", "note-E5_eighth": "E5z", "note-E5_eighth.": "E5z.", "note-E5_eighth..": "E5z..", "note-E5_eighth_fermata": "E5z^", "note-E5_half": "E5H", "note-E5_half.": "E5H.", "note-E5_half..": "E5H..", "note-E5_half._fermata": "E5H.^", "note-E5_half_fermata": "E5H^", "note-E5_hundred_twenty_eighth": "E5h", "note-E5_quadruple_whole_fermata": "E5Q^", "note-E5_quarter": "E5q", "note-E5_quarter.": "E5q.", "note-E5_quarter..": "E5q..", "note-E5_quarter._fermata": "E5q.^", "note-E5_quarter_fermata": "E5q^", "note-E5_sixteenth": "E5S", "note-E5_sixteenth.": "E5S.", "note-E5_sixteenth_fermata": "E5S^", "note-E5_sixty_fourth": "E5s", "note-E5_sixty_fourth.": "E5s.", "note-E5_thirty_second": "E5T", "note-E5_thirty_second.": "E5T.", "note-E5_whole": "E5W", "note-E5_whole.": "E5W.", "note-E5_whole._fermata": "E5W.^", "note-E5_whole_fermata": "E5W^", "note-E6_breve": "E6Y", "note-E6_breve.": "E6Y.", "note-E6_eighth": "E6z", "note-E6_eighth.": "E6z.", "note-E6_eighth..": "E6z..", "note-E6_half": "E6H", "note-E6_half.": "E6H.", "note-E6_hundred_twenty_eighth": "E6h", "note-E6_quarter": "E6q", "note-E6_quarter.": "E6q.", "note-E6_sixteenth": "E6S", "note-E6_sixteenth.": "E6S.", "note-E6_sixty_fourth": "E6s", "note-E6_sixty_fourth.": "E6s.", "note-E6_thirty_second": "E6T", "note-E6_thirty_second.": "E6T.", "note-E6_whole": "E6W", "note-E6_whole.": "E6W.", "note-E7_eighth": "E7z", "note-E7_eighth.": "E7z.", "note-E7_half": "E7H", "note-E7_half.": "E7H.", "note-E7_hundred_twenty_eighth": "E7h", "note-E7_quarter": "E7q", "note-E7_quarter.": "E7q.", "note-E7_sixteenth": "E7S", "note-E7_sixteenth.": "E7S.", "note-E7_sixty_fourth": "E7s", "note-E7_thirty_second": "E7T", "note-E7_thirty_second.": "E7T.", "note-E7_whole": "E7W", "note-E7_whole.": "E7W.", "note-E8_eighth": "E8z", "note-E8_hundred_twenty_eighth": "E8h", "note-E8_quarter": "E8q", "note-E8_sixteenth": "E8S", "note-E8_sixteenth.": "E8S.", "note-E8_sixty_fourth": "E8s", "note-E8_thirty_second": "E8T", "note-E8_thirty_second.": "E8T.", "note-E8_whole": "E8W", "note-E9_eighth": "E9z", "note-E9_sixteenth": "E9S", "note-EN1_eighth": "EN1z", "note-EN1_half": "EN1H", "note-EN1_half.": "EN1H.", "note-EN1_quarter": "EN1q", "note-EN1_quarter.": "EN1q.", "note-EN1_sixteenth": "EN1S", "note-EN1_sixty_fourth": "EN1s", "note-EN1_thirty_second": "EN1T", "note-EN1_whole": "EN1W", "note-EN2_eighth": "EN2z", "note-EN2_eighth.": "EN2z.", "note-EN2_half": "EN2H", "note-EN2_half.": "EN2H.", "note-EN2_hundred_twenty_eighth": "EN2h", "note-EN2_quarter": "EN2q", "note-EN2_quarter.": "EN2q.", "note-EN2_sixteenth": "EN2S", "note-EN2_sixteenth.": "EN2S.", "note-EN2_sixty_fourth": "EN2s", "note-EN2_sixty_fourth.": "EN2s.", "note-EN2_thirty_second": "EN2T", "note-EN2_whole": "EN2W", "note-EN2_whole.": "EN2W.", "note-EN3_eighth": "EN3z", "note-EN3_eighth.": "EN3z.", "note-EN3_half": "EN3H", "note-EN3_half.": "EN3H.", "note-EN3_hundred_twenty_eighth": "EN3h", "note-EN3_quarter": "EN3q", "note-EN3_quarter.": "EN3q.", "note-EN3_sixteenth": "EN3S", "note-EN3_sixteenth.": "EN3S.", "note-EN3_sixty_fourth": "EN3s", "note-EN3_sixty_fourth.": "EN3s.", "note-EN3_thirty_second": "EN3T", "note-EN3_thirty_second.": "EN3T.", "note-EN3_whole": "EN3W", "note-EN3_whole.": "EN3W.", "note-EN4_eighth": "EN4z", "note-EN4_eighth.": "EN4z.", "note-EN4_half": "EN4H", "note-EN4_half.": "EN4H.", "note-EN4_hundred_twenty_eighth": "EN4h", "note-EN4_quarter": "EN4q", "note-EN4_quarter.": "EN4q.", "note-EN4_sixteenth": "EN4S", "note-EN4_sixteenth.": "EN4S.", "note-EN4_sixty_fourth": "EN4s", "note-EN4_sixty_fourth.": "EN4s.", "note-EN4_thirty_second": "EN4T", "note-EN4_thirty_second.": "EN4T.", "note-EN4_whole": "EN4W", "note-EN4_whole.": "EN4W.", "note-EN5_eighth": "EN5z", "note-EN5_eighth.": "EN5z.", "note-EN5_half": "EN5H", "note-EN5_half.": "EN5H.", "note-EN5_hundred_twenty_eighth": "EN5h", "note-EN5_quarter": "EN5q", "note-EN5_quarter.": "EN5q.", "note-EN5_sixteenth": "EN5S", "note-EN5_sixteenth.": "EN5S.", "note-EN5_sixty_fourth": "EN5s", "note-EN5_sixty_fourth.": "EN5s.", "note-EN5_thirty_second": "EN5T", "note-EN5_whole": "EN5W", "note-EN5_whole.": "EN5W.", "note-EN6_eighth": "EN6z", "note-EN6_eighth.": "EN6z.", "note-EN6_half": "EN6H", "note-EN6_half.": "EN6H.", "note-EN6_hundred_twenty_eighth": "EN6h", "note-EN6_quarter": "EN6q", "note-EN6_quarter.": "EN6q.", "note-EN6_sixteenth": "EN6S", "note-EN6_sixteenth.": "EN6S.", "note-EN6_sixty_fourth": "EN6s", "note-EN6_sixty_fourth.": "EN6s.", "note-EN6_thirty_second": "EN6T", "note-EN6_thirty_second.": "EN6T.", "note-EN6_whole": "EN6W", "note-EN6_whole.": "EN6W.", "note-EN7_eighth": "EN7z", "note-EN7_eighth.": "EN7z.", "note-EN7_half": "EN7H", "note-EN7_half.": "EN7H.", "note-EN7_quarter": "EN7q", "note-EN7_quarter.": "EN7q.", "note-EN7_sixteenth": "EN7S", "note-EN7_thirty_second": "EN7T", "note-EN7_thirty_second.": "EN7T.", "note-EN7_whole": "EN7W", "note-EN8_quarter": "EN8q", "note-Eb0_half": "Eb0H", "note-Eb1_eighth": "Eb1z", "note-Eb1_eighth.": "Eb1z.", "note-Eb1_half": "Eb1H", "note-Eb1_half.": "Eb1H.", "note-Eb1_quarter": "Eb1q", "note-Eb1_quarter.": "Eb1q.", "note-Eb1_sixteenth": "Eb1S", "note-Eb1_thirty_second": "Eb1T", "note-Eb1_whole": "Eb1W", "note-Eb2_eighth": "Eb2z", "note-Eb2_eighth.": "Eb2z.", "note-Eb2_half": "Eb2H", "note-Eb2_half.": "Eb2H.", "note-Eb2_quarter": "Eb2q", "note-Eb2_quarter.": "Eb2q.", "note-Eb2_quarter._fermata": "Eb2q.^", "note-Eb2_sixteenth": "Eb2S", "note-Eb2_sixteenth.": "Eb2S.", "note-Eb2_sixty_fourth": "Eb2s", "note-Eb2_thirty_second": "Eb2T", "note-Eb2_whole": "Eb2W", "note-Eb2_whole.": "Eb2W.", "note-Eb3_eighth": "Eb3z", "note-Eb3_eighth.": "Eb3z.", "note-Eb3_half": "Eb3H", "note-Eb3_half.": "Eb3H.", "note-Eb3_half._fermata": "Eb3H.^", "note-Eb3_half_fermata": "Eb3H^", "note-Eb3_quarter": "Eb3q", "note-Eb3_quarter.": "Eb3q.", "note-Eb3_quarter_fermata": "Eb3q^", "note-Eb3_sixteenth": "Eb3S", "note-Eb3_sixteenth.": "Eb3S.", "note-Eb3_sixty_fourth": "Eb3s", "note-Eb3_sixty_fourth.": "Eb3s.", "note-Eb3_thirty_second": "Eb3T", "note-Eb3_thirty_second.": "Eb3T.", "note-Eb3_whole": "Eb3W", "note-Eb3_whole.": "Eb3W.", "note-Eb4_double_whole": "Eb4w", "note-Eb4_eighth": "Eb4z", "note-Eb4_eighth.": "Eb4z.", "note-Eb4_eighth..": "Eb4z..", "note-Eb4_eighth._fermata": "Eb4z.^", "note-Eb4_eighth_fermata": "Eb4z^", "note-Eb4_half": "Eb4H", "note-Eb4_half.": "Eb4H.", "note-Eb4_half._fermata": "Eb4H.^", "note-Eb4_half_fermata": "Eb4H^", "note-Eb4_hundred_twenty_eighth": "Eb4h", "note-Eb4_quarter": "Eb4q", "note-Eb4_quarter.": "Eb4q.", "note-Eb4_quarter..": "Eb4q..", "note-Eb4_quarter._fermata": "Eb4q.^", "note-Eb4_quarter_fermata": "Eb4q^", "note-Eb4_sixteenth": "Eb4S", "note-Eb4_sixteenth.": "Eb4S.", "note-Eb4_sixty_fourth": "Eb4s", "note-Eb4_sixty_fourth.": "Eb4s.", "note-Eb4_thirty_second": "Eb4T", "note-Eb4_thirty_second.": "Eb4T.", "note-Eb4_whole": "Eb4W", "note-Eb4_whole.": "Eb4W.", "note-Eb4_whole_fermata": "Eb4W^", "note-Eb5_double_whole": "Eb5w", "note-Eb5_eighth": "Eb5z", "note-Eb5_eighth.": "Eb5z.", "note-Eb5_eighth..": "Eb5z..", "note-Eb5_eighth_fermata": "Eb5z^", "note-Eb5_half": "Eb5H", "note-Eb5_half.": "Eb5H.", "note-Eb5_half..": "Eb5H..", "note-Eb5_half._fermata": "Eb5H.^", "note-Eb5_half_fermata": "Eb5H^", "note-Eb5_hundred_twenty_eighth": "Eb5h", "note-Eb5_quarter": "Eb5q", "note-Eb5_quarter.": "Eb5q.", "note-Eb5_quarter..": "Eb5q..", "note-Eb5_quarter._fermata": "Eb5q.^", "note-Eb5_quarter_fermata": "Eb5q^", "note-Eb5_sixteenth": "Eb5S", "note-Eb5_sixteenth.": "Eb5S.", "note-Eb5_sixteenth_fermata": "Eb5S^", "note-Eb5_sixty_fourth": "Eb5s", "note-Eb5_sixty_fourth.": "Eb5s.", "note-Eb5_thirty_second": "Eb5T", "note-Eb5_thirty_second.": "Eb5T.", "note-Eb5_whole": "Eb5W", "note-Eb5_whole.": "Eb5W.", "note-Eb5_whole._fermata": "Eb5W.^", "note-Eb5_whole_fermata": "Eb5W^", "note-Eb6_eighth": "Eb6z", "note-Eb6_eighth.": "Eb6z.", "note-Eb6_eighth..": "Eb6z..", "note-Eb6_half": "Eb6H", "note-Eb6_half.": "Eb6H.", "note-Eb6_quarter": "Eb6q", "note-Eb6_quarter.": "Eb6q.", "note-Eb6_quarter..": "Eb6q..", "note-Eb6_sixteenth": "Eb6S", "note-Eb6_sixteenth.": "Eb6S.", "note-Eb6_thirty_second": "Eb6T", "note-Eb6_whole": "Eb6W", "note-Eb6_whole.": "Eb6W.", "note-Eb7_eighth": "Eb7z", "note-Eb7_eighth.": "Eb7z.", "note-Eb7_half": "Eb7H", "note-Eb7_hundred_twenty_eighth": "Eb7h", "note-Eb7_quarter": "Eb7q", "note-Eb7_sixteenth": "Eb7S", "note-Eb7_thirty_second": "Eb7T", "note-Eb7_whole": "Eb7W", "note-Eb8_quarter": "Eb8q", "note-Ebb1_half": "Ebb1H", "note-Ebb2_eighth": "Ebb2z", "note-Ebb2_half": "Ebb2H", "note-Ebb2_quarter": "Ebb2q", "note-Ebb2_quarter.": "Ebb2q.", "note-Ebb2_sixteenth": "Ebb2S", "note-Ebb2_whole": "Ebb2W", "note-Ebb3_eighth": "Ebb3z", "note-Ebb3_half": "Ebb3H", "note-Ebb3_half.": "Ebb3H.", "note-Ebb3_quarter": "Ebb3q", "note-Ebb3_quarter.": "Ebb3q.", "note-Ebb3_sixteenth": "Ebb3S", "note-Ebb3_whole": "Ebb3W", "note-Ebb4_eighth": "Ebb4z", "note-Ebb4_half": "Ebb4H", "note-Ebb4_quarter": "Ebb4q", "note-Ebb4_sixteenth": "Ebb4S", "note-Ebb4_thirty_second": "Ebb4T", "note-Ebb4_whole": "Ebb4W", "note-Ebb4_whole.": "Ebb4W.", "note-Ebb5_eighth": "Ebb5z", "note-Ebb5_half": "Ebb5H", "note-Ebb5_quarter": "Ebb5q", "note-Ebb5_sixteenth": "Ebb5S", "note-Ebb5_thirty_second": "Ebb5T", "note-Ebb5_whole.": "Ebb5W.", "note-Ebb6_eighth": "Ebb6z", "note-Ebb6_half.": "Ebb6H.", "note-Ebb6_quarter": "Ebb6q", "note-Ebb6_sixteenth": "Ebb6S", "note-Ebb6_sixty_fourth": "Ebb6s", "note-Ebb6_thirty_second": "Ebb6T", "note-Ebb6_whole.": "Ebb6W.", "note-Ebb7_eighth": "Ebb7z", "note-F##1_eighth": "F##1z", "note-F##1_half": "F##1H", "note-F##1_half.": "F##1H.", "note-F##1_quarter": "F##1q", "note-F##1_quarter.": "F##1q.", "note-F##1_sixteenth": "F##1S", "note-F##1_whole": "F##1W", "note-F##2_eighth": "F##2z", "note-F##2_half": "F##2H", "note-F##2_half.": "F##2H.", "note-F##2_quarter": "F##2q", "note-F##2_quarter.": "F##2q.", "note-F##2_sixteenth": "F##2S", "note-F##2_sixty_fourth": "F##2s", "note-F##2_thirty_second": "F##2T", "note-F##2_whole": "F##2W", "note-F##3_eighth": "F##3z", "note-F##3_half": "F##3H", "note-F##3_half.": "F##3H.", "note-F##3_hundred_twenty_eighth": "F##3h", "note-F##3_quarter": "F##3q", "note-F##3_quarter.": "F##3q.", "note-F##3_sixteenth": "F##3S", "note-F##3_sixty_fourth": "F##3s", "note-F##3_thirty_second": "F##3T", "note-F##3_whole": "F##3W", "note-F##4_eighth": "F##4z", "note-F##4_eighth.": "F##4z.", "note-F##4_half": "F##4H", "note-F##4_half.": "F##4H.", "note-F##4_quarter": "F##4q", "note-F##4_quarter.": "F##4q.", "note-F##4_sixteenth": "F##4S", "note-F##4_sixty_fourth": "F##4s", "note-F##4_thirty_second": "F##4T", "note-F##4_whole": "F##4W", "note-F##4_whole.": "F##4W.", "note-F##5_eighth": "F##5z", "note-F##5_eighth.": "F##5z.", "note-F##5_half": "F##5H", "note-F##5_half.": "F##5H.", "note-F##5_quarter": "F##5q", "note-F##5_quarter.": "F##5q.", "note-F##5_sixteenth": "F##5S", "note-F##5_sixty_fourth": "F##5s", "note-F##5_thirty_second": "F##5T", "note-F##5_whole": "F##5W", "note-F##5_whole.": "F##5W.", "note-F##6_eighth": "F##6z", "note-F##6_half": "F##6H", "note-F##6_half.": "F##6H.", "note-F##6_quarter": "F##6q", "note-F##6_quarter.": "F##6q.", "note-F##6_sixteenth": "F##6S", "note-F##6_sixty_fourth": "F##6s", "note-F##6_thirty_second": "F##6T", "note-F##6_whole": "F##6W", "note-F##7_eighth": "F##7z", "note-F##7_sixteenth": "F##7S", "note-F##7_thirty_second": "F##7T", "note-F#0_half": "F#0H", "note-F#1_eighth": "F#1z", "note-F#1_eighth.": "F#1z.", "note-F#1_half": "F#1H", "note-F#1_half.": "F#1H.", "note-F#1_quarter": "F#1q", "note-F#1_quarter.": "F#1q.", "note-F#1_sixteenth": "F#1S", "note-F#1_sixty_fourth": "F#1s", "note-F#1_thirty_second": "F#1T", "note-F#1_whole": "F#1W", "note-F#1_whole.": "F#1W.", "note-F#2_breve": "F#2Y", "note-F#2_eighth": "F#2z", "note-F#2_eighth.": "F#2z.", "note-F#2_half": "F#2H", "note-F#2_half.": "F#2H.", "note-F#2_quarter": "F#2q", "note-F#2_quarter.": "F#2q.", "note-F#2_sixteenth": "F#2S", "note-F#2_sixteenth.": "F#2S.", "note-F#2_sixty_fourth": "F#2s", "note-F#2_thirty_second": "F#2T", "note-F#2_whole": "F#2W", "note-F#2_whole.": "F#2W.", "note-F#3_breve": "F#3Y", "note-F#3_double_whole": "F#3w", "note-F#3_eighth": "F#3z", "note-F#3_eighth.": "F#3z.", "note-F#3_half": "F#3H", "note-F#3_half.": "F#3H.", "note-F#3_half_fermata": "F#3H^", "note-F#3_quarter": "F#3q", "note-F#3_quarter.": "F#3q.", "note-F#3_quarter_fermata": "F#3q^", "note-F#3_sixteenth": "F#3S", "note-F#3_sixteenth.": "F#3S.", "note-F#3_sixty_fourth": "F#3s", "note-F#3_sixty_fourth.": "F#3s.", "note-F#3_thirty_second": "F#3T", "note-F#3_whole": "F#3W", "note-F#3_whole.": "F#3W.", "note-F#4_double_whole": "F#4w", "note-F#4_double_whole_fermata": "F#4w^", "note-F#4_eighth": "F#4z", "note-F#4_eighth.": "F#4z.", "note-F#4_eighth..": "F#4z..", "note-F#4_eighth_fermata": "F#4z^", "note-F#4_half": "F#4H", "note-F#4_half.": "F#4H.", "note-F#4_half_fermata": "F#4H^", "note-F#4_quadruple_whole_fermata": "F#4Q^", "note-F#4_quarter": "F#4q", "note-F#4_quarter.": "F#4q.", "note-F#4_quarter..": "F#4q..", "note-F#4_quarter._fermata": "F#4q.^", "note-F#4_quarter_fermata": "F#4q^", "note-F#4_sixteenth": "F#4S", "note-F#4_sixteenth.": "F#4S.", "note-F#4_sixty_fourth": "F#4s", "note-F#4_sixty_fourth.": "F#4s.", "note-F#4_thirty_second": "F#4T", "note-F#4_thirty_second.": "F#4T.", "note-F#4_whole": "F#4W", "note-F#4_whole.": "F#4W.", "note-F#4_whole._fermata": "F#4W.^", "note-F#4_whole_fermata": "F#4W^", "note-F#5_double_whole": "F#5w", "note-F#5_eighth": "F#5z", "note-F#5_eighth.": "F#5z.", "note-F#5_eighth..": "F#5z..", "note-F#5_eighth._fermata": "F#5z.^", "note-F#5_eighth_fermata": "F#5z^", "note-F#5_half": "F#5H", "note-F#5_half.": "F#5H.", "note-F#5_half._fermata": "F#5H.^", "note-F#5_half_fermata": "F#5H^", "note-F#5_quarter": "F#5q", "note-F#5_quarter.": "F#5q.", "note-F#5_quarter..": "F#5q..", "note-F#5_quarter._fermata": "F#5q.^", "note-F#5_quarter_fermata": "F#5q^", "note-F#5_sixteenth": "F#5S", "note-F#5_sixteenth.": "F#5S.", "note-F#5_sixty_fourth": "F#5s", "note-F#5_thirty_second": "F#5T", "note-F#5_thirty_second.": "F#5T.", "note-F#5_whole": "F#5W", "note-F#5_whole.": "F#5W.", "note-F#5_whole_fermata": "F#5W^", "note-F#6_eighth": "F#6z", "note-F#6_eighth.": "F#6z.", "note-F#6_half": "F#6H", "note-F#6_half.": "F#6H.", "note-F#6_quarter": "F#6q", "note-F#6_quarter.": "F#6q.", "note-F#6_sixteenth": "F#6S", "note-F#6_sixteenth.": "F#6S.", "note-F#6_sixty_fourth": "F#6s", "note-F#6_thirty_second": "F#6T", "note-F#6_thirty_second.": "F#6T.", "note-F#6_whole": "F#6W", "note-F#7_eighth": "F#7z", "note-F#7_half": "F#7H", "note-F#7_quarter": "F#7q", "note-F#7_quarter.": "F#7q.", "note-F#7_sixteenth": "F#7S", "note-F#7_sixty_fourth": "F#7s", "note-F#7_thirty_second": "F#7T", "note-F#7_whole": "F#7W", "note-F#8_quarter": "F#8q", "note-F0_eighth": "F0z", "note-F0_eighth.": "F0z.", "note-F0_half": "F0H", "note-F0_quarter": "F0q", "note-F0_quarter.": "F0q.", "note-F0_whole": "F0W", "note-F1_eighth": "F1z", "note-F1_eighth.": "F1z.", "note-F1_half": "F1H", "note-F1_half.": "F1H.", "note-F1_hundred_twenty_eighth": "F1h", "note-F1_quarter": "F1q", "note-F1_quarter.": "F1q.", "note-F1_sixteenth": "F1S", "note-F1_sixteenth.": "F1S.", "note-F1_sixty_fourth": "F1s", "note-F1_thirty_second": "F1T", "note-F1_whole": "F1W", "note-F1_whole.": "F1W.", "note-F2_breve": "F2Y", "note-F2_double_whole": "F2w", "note-F2_double_whole.": "F2w.", "note-F2_double_whole_fermata": "F2w^", "note-F2_eighth": "F2z", "note-F2_eighth.": "F2z.", "note-F2_half": "F2H", "note-F2_half.": "F2H.", "note-F2_half_fermata": "F2H^", "note-F2_hundred_twenty_eighth": "F2h", "note-F2_quadruple_whole": "F2Q", "note-F2_quarter": "F2q", "note-F2_quarter.": "F2q.", "note-F2_quarter..": "F2q..", "note-F2_sixteenth": "F2S", "note-F2_sixteenth.": "F2S.", "note-F2_sixty_fourth": "F2s", "note-F2_sixty_fourth.": "F2s.", "note-F2_thirty_second": "F2T", "note-F2_thirty_second.": "F2T.", "note-F2_whole": "F2W", "note-F2_whole.": "F2W.", "note-F3_breve": "F3Y", "note-F3_breve.": "F3Y.", "note-F3_double_whole": "F3w", "note-F3_double_whole.": "F3w.", "note-F3_double_whole_fermata": "F3w^", "note-F3_eighth": "F3z", "note-F3_eighth.": "F3z.", "note-F3_half": "F3H", "note-F3_half.": "F3H.", "note-F3_half_fermata": "F3H^", "note-F3_hundred_twenty_eighth": "F3h", "note-F3_quadruple_whole": "F3Q", "note-F3_quarter": "F3q", "note-F3_quarter.": "F3q.", "note-F3_quarter..": "F3q..", "note-F3_sixteenth": "F3S", "note-F3_sixteenth.": "F3S.", "note-F3_sixty_fourth": "F3s", "note-F3_sixty_fourth.": "F3s.", "note-F3_thirty_second": "F3T", "note-F3_thirty_second.": "F3T.", "note-F3_whole": "F3W", "note-F3_whole.": "F3W.", "note-F3_whole_fermata": "F3W^", "note-F4_breve": "F4Y", "note-F4_double_whole": "F4w", "note-F4_double_whole.": "F4w.", "note-F4_double_whole_fermata": "F4w^", "note-F4_eighth": "F4z", "note-F4_eighth.": "F4z.", "note-F4_eighth..": "F4z..", "note-F4_eighth_fermata": "F4z^", "note-F4_half": "F4H", "note-F4_half.": "F4H.", "note-F4_half..": "F4H..", "note-F4_half._fermata": "F4H.^", "note-F4_half_fermata": "F4H^", "note-F4_hundred_twenty_eighth": "F4h", "note-F4_quadruple_whole": "F4Q", "note-F4_quadruple_whole.": "F4Q.", "note-F4_quadruple_whole_fermata": "F4Q^", "note-F4_quarter": "F4q", "note-F4_quarter.": "F4q.", "note-F4_quarter..": "F4q..", "note-F4_quarter._fermata": "F4q.^", "note-F4_quarter_fermata": "F4q^", "note-F4_sixteenth": "F4S", "note-F4_sixteenth.": "F4S.", "note-F4_sixty_fourth": "F4s", "note-F4_sixty_fourth.": "F4s.", "note-F4_thirty_second": "F4T", "note-F4_thirty_second.": "F4T.", "note-F4_whole": "F4W", "note-F4_whole.": "F4W.", "note-F4_whole_fermata": "F4W^", "note-F5_breve": "F5Y", "note-F5_double_whole": "F5w", "note-F5_eighth": "F5z", "note-F5_eighth.": "F5z.", "note-F5_eighth..": "F5z..", "note-F5_half": "F5H", "note-F5_half.": "F5H.", "note-F5_half._fermata": "F5H.^", "note-F5_half_fermata": "F5H^", "note-F5_hundred_twenty_eighth": "F5h", "note-F5_quarter": "F5q", "note-F5_quarter.": "F5q.", "note-F5_quarter..": "F5q..", "note-F5_quarter._fermata": "F5q.^", "note-F5_quarter_fermata": "F5q^", "note-F5_sixteenth": "F5S", "note-F5_sixteenth.": "F5S.", "note-F5_sixty_fourth": "F5s", "note-F5_sixty_fourth.": "F5s.", "note-F5_thirty_second": "F5T", "note-F5_thirty_second.": "F5T.", "note-F5_whole": "F5W", "note-F5_whole.": "F5W.", "note-F5_whole_fermata": "F5W^", "note-F6_eighth": "F6z", "note-F6_eighth.": "F6z.", "note-F6_half": "F6H", "note-F6_half.": "F6H.", "note-F6_hundred_twenty_eighth": "F6h", "note-F6_quarter": "F6q", "note-F6_quarter.": "F6q.", "note-F6_sixteenth": "F6S", "note-F6_sixteenth.": "F6S.", "note-F6_sixty_fourth": "F6s", "note-F6_sixty_fourth.": "F6s.", "note-F6_thirty_second": "F6T", "note-F6_thirty_second.": "F6T.", "note-F6_whole": "F6W", "note-F6_whole.": "F6W.", "note-F7_eighth": "F7z", "note-F7_eighth.": "F7z.", "note-F7_half": "F7H", "note-F7_half.": "F7H.", "note-F7_hundred_twenty_eighth": "F7h", "note-F7_quarter": "F7q", "note-F7_quarter.": "F7q.", "note-F7_sixteenth": "F7S", "note-F7_sixteenth.": "F7S.", "note-F7_sixty_fourth": "F7s", "note-F7_thirty_second": "F7T", "note-F7_whole": "F7W", "note-F7_whole.": "F7W.", "note-F8_eighth": "F8z", "note-F8_eighth.": "F8z.", "note-F8_hundred_twenty_eighth": "F8h", "note-F8_quarter": "F8q", "note-F8_sixteenth": "F8S", "note-F8_sixteenth.": "F8S.", "note-F8_thirty_second": "F8T", "note-FN1_eighth": "FN1z", "note-FN1_eighth.": "FN1z.", "note-FN1_half": "FN1H", "note-FN1_half.": "FN1H.", "note-FN1_quarter": "FN1q", "note-FN1_quarter.": "FN1q.", "note-FN1_sixteenth": "FN1S", "note-FN1_whole": "FN1W", "note-FN1_whole.": "FN1W.", "note-FN2_eighth": "FN2z", "note-FN2_eighth.": "FN2z.", "note-FN2_half": "FN2H", "note-FN2_half.": "FN2H.", "note-FN2_quarter": "FN2q", "note-FN2_quarter.": "FN2q.", "note-FN2_sixteenth": "FN2S", "note-FN2_sixty_fourth": "FN2s", "note-FN2_thirty_second": "FN2T", "note-FN2_whole": "FN2W", "note-FN2_whole.": "FN2W.", "note-FN3_eighth": "FN3z", "note-FN3_eighth.": "FN3z.", "note-FN3_half": "FN3H", "note-FN3_half.": "FN3H.", "note-FN3_quarter": "FN3q", "note-FN3_quarter.": "FN3q.", "note-FN3_sixteenth": "FN3S", "note-FN3_sixty_fourth": "FN3s", "note-FN3_thirty_second": "FN3T", "note-FN3_whole": "FN3W", "note-FN4_eighth": "FN4z", "note-FN4_eighth.": "FN4z.", "note-FN4_half": "FN4H", "note-FN4_half.": "FN4H.", "note-FN4_quarter": "FN4q", "note-FN4_quarter.": "FN4q.", "note-FN4_sixteenth": "FN4S", "note-FN4_sixteenth.": "FN4S.", "note-FN4_sixty_fourth": "FN4s", "note-FN4_thirty_second": "FN4T", "note-FN4_whole": "FN4W", "note-FN4_whole.": "FN4W.", "note-FN5_breve": "FN5Y", "note-FN5_eighth": "FN5z", "note-FN5_eighth.": "FN5z.", "note-FN5_half": "FN5H", "note-FN5_half.": "FN5H.", "note-FN5_quarter": "FN5q", "note-FN5_quarter.": "FN5q.", "note-FN5_sixteenth": "FN5S", "note-FN5_sixteenth.": "FN5S.", "note-FN5_sixty_fourth": "FN5s", "note-FN5_thirty_second": "FN5T", "note-FN5_whole": "FN5W", "note-FN5_whole.": "FN5W.", "note-FN6_breve": "FN6Y", "note-FN6_eighth": "FN6z", "note-FN6_eighth.": "FN6z.", "note-FN6_half": "FN6H", "note-FN6_half.": "FN6H.", "note-FN6_quarter": "FN6q", "note-FN6_quarter.": "FN6q.", "note-FN6_sixteenth": "FN6S", "note-FN6_sixteenth.": "FN6S.", "note-FN6_sixty_fourth": "FN6s", "note-FN6_thirty_second": "FN6T", "note-FN6_whole": "FN6W", "note-FN7_eighth": "FN7z", "note-FN7_half.": "FN7H.", "note-FN7_quarter": "FN7q", "note-FN7_sixteenth": "FN7S", "note-FN7_sixty_fourth": "FN7s", "note-FN7_thirty_second": "FN7T", "note-FN7_whole": "FN7W", "note-FN8_eighth": "FN8z", "note-FN8_eighth.": "FN8z.", "note-FN8_quarter": "FN8q", "note-Fb1_eighth": "Fb1z", "note-Fb1_half.": "Fb1H.", "note-Fb1_quarter": "Fb1q", "note-Fb1_quarter.": "Fb1q.", "note-Fb1_sixteenth": "Fb1S", "note-Fb1_sixty_fourth": "Fb1s", "note-Fb1_thirty_second": "Fb1T", "note-Fb2_eighth": "Fb2z", "note-Fb2_eighth.": "Fb2z.", "note-Fb2_half": "Fb2H", "note-Fb2_half.": "Fb2H.", "note-Fb2_quarter": "Fb2q", "note-Fb2_quarter.": "Fb2q.", "note-Fb2_sixteenth": "Fb2S", "note-Fb2_thirty_second": "Fb2T", "note-Fb2_whole": "Fb2W", "note-Fb2_whole.": "Fb2W.", "note-Fb3_eighth": "Fb3z", "note-Fb3_eighth.": "Fb3z.", "note-Fb3_half": "Fb3H", "note-Fb3_half.": "Fb3H.", "note-Fb3_quarter": "Fb3q", "note-Fb3_quarter.": "Fb3q.", "note-Fb3_sixteenth": "Fb3S", "note-Fb3_thirty_second": "Fb3T", "note-Fb3_whole": "Fb3W", "note-Fb3_whole.": "Fb3W.", "note-Fb4_eighth": "Fb4z", "note-Fb4_eighth.": "Fb4z.", "note-Fb4_half": "Fb4H", "note-Fb4_half.": "Fb4H.", "note-Fb4_hundred_twenty_eighth": "Fb4h", "note-Fb4_quarter": "Fb4q", "note-Fb4_quarter.": "Fb4q.", "note-Fb4_sixteenth": "Fb4S", "note-Fb4_sixteenth.": "Fb4S.", "note-Fb4_thirty_second": "Fb4T", "note-Fb4_whole": "Fb4W", "note-Fb5_eighth": "Fb5z", "note-Fb5_eighth.": "Fb5z.", "note-Fb5_half": "Fb5H", "note-Fb5_half.": "Fb5H.", "note-Fb5_hundred_twenty_eighth": "Fb5h", "note-Fb5_quarter": "Fb5q", "note-Fb5_quarter.": "Fb5q.", "note-Fb5_sixteenth": "Fb5S", "note-Fb5_sixty_fourth": "Fb5s", "note-Fb5_thirty_second": "Fb5T", "note-Fb5_whole": "Fb5W", "note-Fb6_eighth": "Fb6z", "note-Fb6_half": "Fb6H", "note-Fb6_half.": "Fb6H.", "note-Fb6_quarter": "Fb6q", "note-Fb6_quarter.": "Fb6q.", "note-Fb6_sixteenth": "Fb6S", "note-Fb6_sixty_fourth": "Fb6s", "note-Fb6_thirty_second": "Fb6T", "note-Fb6_whole": "Fb6W", "note-Fb6_whole.": "Fb6W.", "note-Fb7_eighth": "Fb7z", "note-Fb7_half.": "Fb7H.", "note-Fb7_thirty_second": "Fb7T", "note-Fbb2_sixteenth": "Fbb2S", "note-Fbb4_eighth": "Fbb4z", "note-G##1_eighth": "G##1z", "note-G##1_quarter": "G##1q", "note-G##1_sixteenth": "G##1S", "note-G##2_eighth": "G##2z", "note-G##2_quarter.": "G##2q.", "note-G##2_sixteenth": "G##2S", "note-G##3_eighth": "G##3z", "note-G##3_half": "G##3H", "note-G##3_quarter": "G##3q", "note-G##3_quarter.": "G##3q.", "note-G##3_sixteenth": "G##3S", "note-G##3_sixty_fourth": "G##3s", "note-G##3_thirty_second": "G##3T", "note-G##3_whole": "G##3W", "note-G##4_eighth": "G##4z", "note-G##4_half": "G##4H", "note-G##4_quarter": "G##4q", "note-G##4_quarter.": "G##4q.", "note-G##4_sixteenth": "G##4S", "note-G##4_thirty_second": "G##4T", "note-G##4_whole": "G##4W", "note-G##5_eighth": "G##5z", "note-G##5_eighth.": "G##5z.", "note-G##5_quarter": "G##5q", "note-G##5_quarter.": "G##5q.", "note-G##5_sixteenth": "G##5S", "note-G##5_thirty_second": "G##5T", "note-G##5_whole": "G##5W", "note-G##6_eighth": "G##6z", "note-G##6_sixteenth": "G##6S", "note-G##7_sixteenth": "G##7S", "note-G#0_eighth": "G#0z", "note-G#0_sixteenth": "G#0S", "note-G#1_eighth": "G#1z", "note-G#1_eighth.": "G#1z.", "note-G#1_half": "G#1H", "note-G#1_half.": "G#1H.", "note-G#1_quarter": "G#1q", "note-G#1_quarter.": "G#1q.", "note-G#1_sixteenth": "G#1S", "note-G#1_thirty_second": "G#1T", "note-G#1_whole": "G#1W", "note-G#1_whole.": "G#1W.", "note-G#2_eighth": "G#2z", "note-G#2_eighth.": "G#2z.", "note-G#2_half": "G#2H", "note-G#2_half.": "G#2H.", "note-G#2_quarter": "G#2q", "note-G#2_quarter.": "G#2q.", "note-G#2_sixteenth": "G#2S", "note-G#2_sixteenth.": "G#2S.", "note-G#2_sixty_fourth": "G#2s", "note-G#2_thirty_second": "G#2T", "note-G#2_whole": "G#2W", "note-G#2_whole.": "G#2W.", "note-G#3_breve": "G#3Y", "note-G#3_eighth": "G#3z", "note-G#3_eighth.": "G#3z.", "note-G#3_eighth..": "G#3z..", "note-G#3_half": "G#3H", "note-G#3_half.": "G#3H.", "note-G#3_half_fermata": "G#3H^", "note-G#3_hundred_twenty_eighth": "G#3h", "note-G#3_quarter": "G#3q", "note-G#3_quarter.": "G#3q.", "note-G#3_sixteenth": "G#3S", "note-G#3_sixteenth.": "G#3S.", "note-G#3_sixty_fourth": "G#3s", "note-G#3_sixty_fourth.": "G#3s.", "note-G#3_thirty_second": "G#3T", "note-G#3_thirty_second.": "G#3T.", "note-G#3_whole": "G#3W", "note-G#4_double_whole": "G#4w", "note-G#4_double_whole_fermata": "G#4w^", "note-G#4_eighth": "G#4z", "note-G#4_eighth.": "G#4z.", "note-G#4_eighth..": "G#4z..", "note-G#4_eighth._fermata": "G#4z.^", "note-G#4_half": "G#4H", "note-G#4_half.": "G#4H.", "note-G#4_half._fermata": "G#4H.^", "note-G#4_half_fermata": "G#4H^", "note-G#4_quarter": "G#4q", "note-G#4_quarter.": "G#4q.", "note-G#4_quarter_fermata": "G#4q^", "note-G#4_sixteenth": "G#4S", "note-G#4_sixteenth.": "G#4S.", "note-G#4_sixty_fourth": "G#4s", "note-G#4_thirty_second": "G#4T", "note-G#4_thirty_second.": "G#4T.", "note-G#4_whole": "G#4W", "note-G#4_whole.": "G#4W.", "note-G#4_whole_fermata": "G#4W^", "note-G#5_eighth": "G#5z", "note-G#5_eighth.": "G#5z.", "note-G#5_eighth_fermata": "G#5z^", "note-G#5_half": "G#5H", "note-G#5_half.": "G#5H.", "note-G#5_half_fermata": "G#5H^", "note-G#5_hundred_twenty_eighth": "G#5h", "note-G#5_quarter": "G#5q", "note-G#5_quarter.": "G#5q.", "note-G#5_quarter..": "G#5q..", "note-G#5_quarter_fermata": "G#5q^", "note-G#5_sixteenth": "G#5S", "note-G#5_sixteenth.": "G#5S.", "note-G#5_sixty_fourth": "G#5s", "note-G#5_sixty_fourth.": "G#5s.", "note-G#5_thirty_second": "G#5T", "note-G#5_thirty_second.": "G#5T.", "note-G#5_whole": "G#5W", "note-G#5_whole.": "G#5W.", "note-G#6_eighth": "G#6z", "note-G#6_eighth.": "G#6z.", "note-G#6_half": "G#6H", "note-G#6_half.": "G#6H.", "note-G#6_quarter": "G#6q", "note-G#6_quarter.": "G#6q.", "note-G#6_sixteenth": "G#6S", "note-G#6_sixteenth.": "G#6S.", "note-G#6_sixty_fourth": "G#6s", "note-G#6_sixty_fourth.": "G#6s.", "note-G#6_thirty_second": "G#6T", "note-G#6_whole": "G#6W", "note-G#6_whole.": "G#6W.", "note-G#7_eighth": "G#7z", "note-G#7_eighth.": "G#7z.", "note-G#7_half": "G#7H", "note-G#7_quarter": "G#7q", "note-G#7_quarter.": "G#7q.", "note-G#7_sixteenth": "G#7S", "note-G#7_sixty_fourth": "G#7s", "note-G#7_thirty_second": "G#7T", "note-G#8_eighth": "G#8z", "note-G0_eighth": "G0z", "note-G0_half": "G0H", "note-G0_quarter": "G0q", "note-G0_quarter.": "G0q.", "note-G0_sixteenth": "G0S", "note-G0_whole": "G0W", "note-G1_eighth": "G1z", "note-G1_eighth.": "G1z.", "note-G1_half": "G1H", "note-G1_half.": "G1H.", "note-G1_hundred_twenty_eighth": "G1h", "note-G1_quarter": "G1q", "note-G1_quarter.": "G1q.", "note-G1_sixteenth": "G1S", "note-G1_sixteenth.": "G1S.", "note-G1_sixty_fourth": "G1s", "note-G1_sixty_fourth.": "G1s.", "note-G1_thirty_second": "G1T", "note-G1_thirty_second.": "G1T.", "note-G1_whole": "G1W", "note-G1_whole.": "G1W.", "note-G2_breve": "G2Y", "note-G2_breve.": "G2Y.", "note-G2_double_whole": "G2w", "note-G2_double_whole.": "G2w.", "note-G2_double_whole_fermata": "G2w^", "note-G2_eighth": "G2z", "note-G2_eighth.": "G2z.", "note-G2_half": "G2H", "note-G2_half.": "G2H.", "note-G2_half_fermata": "G2H^", "note-G2_hundred_twenty_eighth": "G2h", "note-G2_quadruple_whole": "G2Q", "note-G2_quarter": "G2q", "note-G2_quarter.": "G2q.", "note-G2_quarter_fermata": "G2q^", "note-G2_sixteenth": "G2S", "note-G2_sixteenth.": "G2S.", "note-G2_sixty_fourth": "G2s", "note-G2_sixty_fourth.": "G2s.", "note-G2_thirty_second": "G2T", "note-G2_thirty_second.": "G2T.", "note-G2_whole": "G2W", "note-G2_whole.": "G2W.", "note-G2_whole_fermata": "G2W^", "note-G3_breve": "G3Y", "note-G3_breve.": "G3Y.", "note-G3_double_whole": "G3w", "note-G3_double_whole.": "G3w.", "note-G3_eighth": "G3z", "note-G3_eighth.": "G3z.", "note-G3_eighth..": "G3z..", "note-G3_eighth._fermata": "G3z.^", "note-G3_eighth_fermata": "G3z^", "note-G3_half": "G3H", "note-G3_half.": "G3H.", "note-G3_half._fermata": "G3H.^", "note-G3_half_fermata": "G3H^", "note-G3_hundred_twenty_eighth": "G3h", "note-G3_quadruple_whole": "G3Q", "note-G3_quarter": "G3q", "note-G3_quarter.": "G3q.", "note-G3_quarter..": "G3q..", "note-G3_quarter._fermata": "G3q.^", "note-G3_quarter_fermata": "G3q^", "note-G3_sixteenth": "G3S", "note-G3_sixteenth.": "G3S.", "note-G3_sixty_fourth": "G3s", "note-G3_sixty_fourth.": "G3s.", "note-G3_thirty_second": "G3T", "note-G3_thirty_second.": "G3T.", "note-G3_whole": "G3W", "note-G3_whole.": "G3W.", "note-G3_whole_fermata": "G3W^", "note-G4_breve": "G4Y", "note-G4_double_whole": "G4w", "note-G4_double_whole.": "G4w.", "note-G4_double_whole_fermata": "G4w^", "note-G4_eighth": "G4z", "note-G4_eighth.": "G4z.", "note-G4_eighth..": "G4z..", "note-G4_eighth_fermata": "G4z^", "note-G4_half": "G4H", "note-G4_half.": "G4H.", "note-G4_half._fermata": "G4H.^", "note-G4_half_fermata": "G4H^", "note-G4_hundred_twenty_eighth": "G4h", "note-G4_long": "G4long", "note-G4_quadruple_whole": "G4Q", "note-G4_quadruple_whole.": "G4Q.", "note-G4_quadruple_whole_fermata": "G4Q^", "note-G4_quarter": "G4q", "note-G4_quarter.": "G4q.", "note-G4_quarter..": "G4q..", "note-G4_quarter._fermata": "G4q.^", "note-G4_quarter_fermata": "G4q^", "note-G4_sixteenth": "G4S", "note-G4_sixteenth.": "G4S.", "note-G4_sixteenth..": "G4S..", "note-G4_sixteenth._fermata": "G4S.^", "note-G4_sixty_fourth": "G4s", "note-G4_sixty_fourth.": "G4s.", "note-G4_thirty_second": "G4T", "note-G4_thirty_second.": "G4T.", "note-G4_whole": "G4W", "note-G4_whole.": "G4W.", "note-G4_whole._fermata": "G4W.^", "note-G4_whole_fermata": "G4W^", "note-G5_breve": "G5Y", "note-G5_breve.": "G5Y.", "note-G5_double_whole": "G5w", "note-G5_double_whole.": "G5w.", "note-G5_eighth": "G5z", "note-G5_eighth.": "G5z.", "note-G5_eighth..": "G5z..", "note-G5_eighth_fermata": "G5z^", "note-G5_half": "G5H", "note-G5_half.": "G5H.", "note-G5_half..": "G5H..", "note-G5_half._fermata": "G5H.^", "note-G5_half_fermata": "G5H^", "note-G5_hundred_twenty_eighth": "G5h", "note-G5_quadruple_whole.": "G5Q.", "note-G5_quarter": "G5q", "note-G5_quarter.": "G5q.", "note-G5_quarter..": "G5q..", "note-G5_quarter._fermata": "G5q.^", "note-G5_quarter_fermata": "G5q^", "note-G5_sixteenth": "G5S", "note-G5_sixteenth.": "G5S.", "note-G5_sixty_fourth": "G5s", "note-G5_sixty_fourth.": "G5s.", "note-G5_thirty_second": "G5T", "note-G5_thirty_second.": "G5T.", "note-G5_whole": "G5W", "note-G5_whole.": "G5W.", "note-G5_whole_fermata": "G5W^", "note-G6_breve": "G6Y", "note-G6_breve.": "G6Y.", "note-G6_eighth": "G6z", "note-G6_eighth.": "G6z.", "note-G6_half": "G6H", "note-G6_half.": "G6H.", "note-G6_hundred_twenty_eighth": "G6h", "note-G6_quarter": "G6q", "note-G6_quarter.": "G6q.", "note-G6_sixteenth": "G6S", "note-G6_sixteenth.": "G6S.", "note-G6_sixty_fourth": "G6s", "note-G6_sixty_fourth.": "G6s.", "note-G6_thirty_second": "G6T", "note-G6_thirty_second.": "G6T.", "note-G6_whole": "G6W", "note-G6_whole.": "G6W.", "note-G7_eighth": "G7z", "note-G7_eighth.": "G7z.", "note-G7_half": "G7H", "note-G7_half.": "G7H.", "note-G7_hundred_twenty_eighth": "G7h", "note-G7_quarter": "G7q", "note-G7_quarter.": "G7q.", "note-G7_sixteenth": "G7S", "note-G7_sixteenth.": "G7S.", "note-G7_sixty_fourth": "G7s", "note-G7_thirty_second": "G7T", "note-G7_thirty_second.": "G7T.", "note-G7_whole": "G7W", "note-G7_whole.": "G7W.", "note-G8_eighth": "G8z", "note-G8_eighth.": "G8z.", "note-G8_hundred_twenty_eighth": "G8h", "note-G8_quarter": "G8q", "note-G8_sixteenth": "G8S", "note-G8_sixteenth.": "G8S.", "note-G8_sixty_fourth.": "G8s.", "note-G8_thirty_second": "G8T", "note-G9_quarter": "G9q", "note-GN0_eighth": "GN0z", "note-GN1_eighth": "GN1z", "note-GN1_half": "GN1H", "note-GN1_half.": "GN1H.", "note-GN1_quarter": "GN1q", "note-GN1_quarter.": "GN1q.", "note-GN1_sixteenth": "GN1S", "note-GN1_sixteenth.": "GN1S.", "note-GN1_thirty_second": "GN1T", "note-GN1_whole": "GN1W", "note-GN2_eighth": "GN2z", "note-GN2_eighth.": "GN2z.", "note-GN2_half": "GN2H", "note-GN2_half.": "GN2H.", "note-GN2_quarter": "GN2q", "note-GN2_quarter.": "GN2q.", "note-GN2_sixteenth": "GN2S", "note-GN2_sixty_fourth": "GN2s", "note-GN2_thirty_second": "GN2T", "note-GN2_whole": "GN2W", "note-GN3_eighth": "GN3z", "note-GN3_eighth.": "GN3z.", "note-GN3_half": "GN3H", "note-GN3_half.": "GN3H.", "note-GN3_hundred_twenty_eighth": "GN3h", "note-GN3_quarter": "GN3q", "note-GN3_quarter.": "GN3q.", "note-GN3_sixteenth": "GN3S", "note-GN3_sixteenth.": "GN3S.", "note-GN3_sixty_fourth": "GN3s", "note-GN3_thirty_second": "GN3T", "note-GN3_whole": "GN3W", "note-GN3_whole.": "GN3W.", "note-GN4_eighth": "GN4z", "note-GN4_eighth.": "GN4z.", "note-GN4_half": "GN4H", "note-GN4_half.": "GN4H.", "note-GN4_hundred_twenty_eighth": "GN4h", "note-GN4_quarter": "GN4q", "note-GN4_quarter.": "GN4q.", "note-GN4_sixteenth": "GN4S", "note-GN4_sixteenth.": "GN4S.", "note-GN4_sixty_fourth": "GN4s", "note-GN4_thirty_second": "GN4T", "note-GN4_thirty_second.": "GN4T.", "note-GN4_whole": "GN4W", "note-GN4_whole.": "GN4W.", "note-GN5_eighth": "GN5z", "note-GN5_eighth.": "GN5z.", "note-GN5_half": "GN5H", "note-GN5_half.": "GN5H.", "note-GN5_quarter": "GN5q", "note-GN5_quarter.": "GN5q.", "note-GN5_sixteenth": "GN5S", "note-GN5_sixteenth.": "GN5S.", "note-GN5_sixty_fourth": "GN5s", "note-GN5_thirty_second": "GN5T", "note-GN5_whole": "GN5W", "note-GN5_whole.": "GN5W.", "note-GN6_eighth": "GN6z", "note-GN6_eighth.": "GN6z.", "note-GN6_half": "GN6H", "note-GN6_half.": "GN6H.", "note-GN6_quarter": "GN6q", "note-GN6_quarter.": "GN6q.", "note-GN6_sixteenth": "GN6S", "note-GN6_sixteenth.": "GN6S.", "note-GN6_sixty_fourth": "GN6s", "note-GN6_thirty_second": "GN6T", "note-GN6_whole": "GN6W", "note-GN7_eighth": "GN7z", "note-GN7_sixteenth": "GN7S", "note-GN7_sixty_fourth": "GN7s", "note-GN7_thirty_second": "GN7T", "note-Gb1_eighth": "Gb1z", "note-Gb1_eighth.": "Gb1z.", "note-Gb1_half": "Gb1H", "note-Gb1_half.": "Gb1H.", "note-Gb1_quarter": "Gb1q", "note-Gb1_quarter.": "Gb1q.", "note-Gb1_sixteenth": "Gb1S", "note-Gb1_thirty_second": "Gb1T", "note-Gb1_thirty_second.": "Gb1T.", "note-Gb1_whole": "Gb1W", "note-Gb2_eighth": "Gb2z", "note-Gb2_eighth.": "Gb2z.", "note-Gb2_half": "Gb2H", "note-Gb2_half.": "Gb2H.", "note-Gb2_quarter": "Gb2q", "note-Gb2_quarter.": "Gb2q.", "note-Gb2_sixteenth": "Gb2S", "note-Gb2_sixteenth.": "Gb2S.", "note-Gb2_sixty_fourth": "Gb2s", "note-Gb2_sixty_fourth.": "Gb2s.", "note-Gb2_thirty_second": "Gb2T", "note-Gb2_thirty_second.": "Gb2T.", "note-Gb2_whole": "Gb2W", "note-Gb2_whole.": "Gb2W.", "note-Gb3_eighth": "Gb3z", "note-Gb3_eighth.": "Gb3z.", "note-Gb3_half": "Gb3H", "note-Gb3_half.": "Gb3H.", "note-Gb3_quarter": "Gb3q", "note-Gb3_quarter.": "Gb3q.", "note-Gb3_quarter..": "Gb3q..", "note-Gb3_sixteenth": "Gb3S", "note-Gb3_sixteenth.": "Gb3S.", "note-Gb3_sixty_fourth": "Gb3s", "note-Gb3_sixty_fourth.": "Gb3s.", "note-Gb3_thirty_second": "Gb3T", "note-Gb3_thirty_second.": "Gb3T.", "note-Gb3_whole": "Gb3W", "note-Gb3_whole.": "Gb3W.", "note-Gb4_eighth": "Gb4z", "note-Gb4_eighth.": "Gb4z.", "note-Gb4_eighth..": "Gb4z..", "note-Gb4_half": "Gb4H", "note-Gb4_half.": "Gb4H.", "note-Gb4_quarter": "Gb4q", "note-Gb4_quarter.": "Gb4q.", "note-Gb4_sixteenth": "Gb4S", "note-Gb4_sixteenth.": "Gb4S.", "note-Gb4_sixty_fourth": "Gb4s", "note-Gb4_sixty_fourth.": "Gb4s.", "note-Gb4_thirty_second": "Gb4T", "note-Gb4_thirty_second.": "Gb4T.", "note-Gb4_whole": "Gb4W", "note-Gb4_whole.": "Gb4W.", "note-Gb5_eighth": "Gb5z", "note-Gb5_eighth.": "Gb5z.", "note-Gb5_half": "Gb5H", "note-Gb5_half.": "Gb5H.", "note-Gb5_hundred_twenty_eighth": "Gb5h", "note-Gb5_quarter": "Gb5q", "note-Gb5_quarter.": "Gb5q.", "note-Gb5_quarter..": "Gb5q..", "note-Gb5_sixteenth": "Gb5S", "note-Gb5_sixteenth.": "Gb5S.", "note-Gb5_sixty_fourth": "Gb5s", "note-Gb5_thirty_second": "Gb5T", "note-Gb5_thirty_second.": "Gb5T.", "note-Gb5_whole": "Gb5W", "note-Gb5_whole.": "Gb5W.", "note-Gb6_eighth": "Gb6z", "note-Gb6_eighth.": "Gb6z.", "note-Gb6_half": "Gb6H", "note-Gb6_half.": "Gb6H.", "note-Gb6_quarter": "Gb6q", "note-Gb6_quarter.": "Gb6q.", "note-Gb6_sixteenth": "Gb6S", "note-Gb6_sixteenth.": "Gb6S.", "note-Gb6_sixty_fourth": "Gb6s", "note-Gb6_sixty_fourth.": "Gb6s.", "note-Gb6_thirty_second": "Gb6T", "note-Gb6_whole": "Gb6W", "note-Gb6_whole.": "Gb6W.", "note-Gb7_eighth": "Gb7z", "note-Gb7_half": "Gb7H", "note-Gb7_hundred_twenty_eighth": "Gb7h", "note-Gb7_quarter": "Gb7q", "note-Gb7_sixteenth": "Gb7S", "note-Gb7_sixty_fourth": "Gb7s", "note-Gb7_sixty_fourth.": "Gb7s.", "note-Gb7_thirty_second": "Gb7T", "note-Gbb2_eighth": "Gbb2z", "note-Gbb3_eighth": "Gbb3z", "note-Gbb3_half": "Gbb3H", "note-Gbb3_sixteenth": "Gbb3S", "note-Gbb4_half": "Gbb4H", "note-Gbb4_sixteenth": "Gbb4S", "note-Gbb5_eighth": "Gbb5z", "note-Gbb5_half": "Gbb5H", "note-Gbb5_sixteenth": "Gbb5S", "note-Gbb6_sixteenth": "Gbb6S", - "rest-breve": "rY", "rest-breve.": "rY.", "rest-eighth": "rz", "rest-eighth.": "rz.", "rest-eighth..": "rz..", "rest-eighth._fermata": "rz.^", "rest-eighth_fermata": "rz^", "rest-half": "rH", "rest-half.": "rH.", "rest-half._fermata": "rH.^", "rest-half_fermata": "rH^", "rest-hundred_twenty_eighth": "rh", "rest-long": "rlong", "rest-quadruple_whole": "rQ", "rest-quarter": "rq", "rest-quarter.": "rq.", "rest-quarter..": "rq..", "rest-quarter.._fermata": "rq..^", "rest-quarter._fermata": "rq.^", "rest-quarter_fermata": "rq^", "rest-sixteenth": "rS", "rest-sixteenth.": "rS.", "rest-sixteenth_fermata": "rS^", "rest-sixty_fourth": "rs", "rest-thirty_second": "rT", "rest-thirty_second.": "rT.", "rest-whole": "rW", "rest-whole.": "rW.", "rest-whole_fermata": "rW^", - "tie": "t", - "timeSignature-1/16": "s1/16", "timeSignature-1/2": "s1/2", "timeSignature-1/4": "s1/4", "timeSignature-1/8": "s1/8", "timeSignature-10/16": "s10/16", "timeSignature-10/4": "s10/4", "timeSignature-10/8": "s10/8", "timeSignature-11/16": "s11/16", "timeSignature-11/4": "s11/4", "timeSignature-11/8": "s11/8", "timeSignature-12/16": "s12/16", "timeSignature-12/32": "s12/32", "timeSignature-12/4": "s12/4", "timeSignature-12/8": "s12/8", "timeSignature-13/16": "s13/16", "timeSignature-13/8": "s13/8", "timeSignature-14/4": "s14/4", "timeSignature-14/8": "s14/8", "timeSignature-15/16": "s15/16", "timeSignature-15/4": "s15/4", "timeSignature-16/4": "s16/4", "timeSignature-17/16": "s17/16", "timeSignature-18/4": "s18/4", "timeSignature-19/16": "s19/16", "timeSignature-2/1": "s2/1", "timeSignature-2/2": "s2/2", "timeSignature-2/3": "s2/3", "timeSignature-2/32": "s2/32", "timeSignature-2/4": "s2/4", "timeSignature-2/48": "s2/48", "timeSignature-2/8": "s2/8", "timeSignature-20/8": "s20/8", "timeSignature-21/16": "s21/16", "timeSignature-22/8": "s22/8", "timeSignature-23/4": "s23/4", "timeSignature-23/8": "s23/8", "timeSignature-24/16": "s24/16", "timeSignature-27/16": "s27/16", "timeSignature-27/8": "s27/8", "timeSignature-28/8": "s28/8", "timeSignature-3/1": "s3/1", "timeSignature-3/16": "s3/16", "timeSignature-3/2": "s3/2", "timeSignature-3/4": "s3/4", "timeSignature-3/6": "s3/6", "timeSignature-3/8": "s3/8", "timeSignature-32/32": "s32/32", "timeSignature-33/32": "s33/32", "timeSignature-4/1": "s4/1", "timeSignature-4/16": "s4/16", "timeSignature-4/2": "s4/2", "timeSignature-4/4": "s4/4", "timeSignature-4/8": "s4/8", "timeSignature-5/16": "s5/16", "timeSignature-5/4": "s5/4", "timeSignature-5/8": "s5/8", "timeSignature-6/16": "s6/16", "timeSignature-6/2": "s6/2", "timeSignature-6/4": "s6/4", "timeSignature-6/8": "s6/8", "timeSignature-7/16": "s7/16", "timeSignature-7/2": "s7/2", "timeSignature-7/4": "s7/4", "timeSignature-7/8": "s7/8", "timeSignature-8/12": "s8/12", "timeSignature-8/16": "s8/16", "timeSignature-8/2": "s8/2", "timeSignature-8/4": "s8/4", "timeSignature-8/8": "s8/8", "timeSignature-9/16": "s9/16", "timeSignature-9/32": "s9/32", "timeSignature-9/4": "s9/4", "timeSignature-9/8": "s9/8", "timeSignature-C": "[", "timeSignature-C/": "[/" -} \ No newline at end of file From cc47eef5a0bb5594512aa41c0176e6dacd4c4c99 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Fri, 26 Jan 2024 17:19:16 +0100 Subject: [PATCH 49/76] Improve translation of symbols in `output_translator.py`. Return original symbol if not found in dictionary. --- pero_ocr/music/output_translator.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pero_ocr/music/output_translator.py b/pero_ocr/music/output_translator.py index 8e6e7a1..21d48ee 100644 --- a/pero_ocr/music/output_translator.py +++ b/pero_ocr/music/output_translator.py @@ -41,13 +41,15 @@ def translate_line(self, line, reverse: bool = False): def translate_symbol(self, symbol: str, reverse: bool = False): dictionary = self.dictionary_reversed if reverse else self.dictionary - try: - return dictionary[symbol] - except KeyError: - if symbol not in self.n_existing_labels: - self.n_existing_labels.add(symbol) - logger.info(f'Not existing label: ({symbol})') - return '' + translation = dictionary.get(symbol, None) + if translation is not None: + return translation + + if symbol not in self.n_existing_labels: + logger.debug(f'Not existing label: ({symbol})') + self.n_existing_labels.add(symbol) + + return symbol @staticmethod def load_dictionary(dictionary: dict = None, filename: str = None) -> dict: From c7d90a1176b795787cb7639d0e4ffc6394c1e8d3 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Wed, 31 Jan 2024 11:50:37 +0100 Subject: [PATCH 50/76] Add `atomic` option to `OutputTranslator` + output substitution toggles to `PageOCR` config section. - `SUBSTITUTE_OUTPUT` ('yes' or 'no'): enables (or disables) output substitution (enabled by default) - `SUBSTITUTE_OUTPUT_ATOMIC` ('yes' or 'no'): if 'yes' and any symbol cannot be translated, return original line. if 'no', line may be partially translated. --- pero_ocr/document_ocr/page_parser.py | 12 ++++++---- pero_ocr/music/output_translator.py | 22 ++++++++++++++----- pero_ocr/ocr_engine/line_ocr_engine.py | 5 +++-- pero_ocr/ocr_engine/pytorch_ocr_engine.py | 5 +++-- pero_ocr/ocr_engine/transformer_ocr_engine.py | 6 +++-- 5 files changed, 34 insertions(+), 16 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 593ec4b..3f3a78c 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -515,11 +515,15 @@ def __init__(self, config, device, config_path=''): self.device = device if not use_cpu else torch.device("cpu") self.categories = config_get_list(config, key='CATEGORIES', fallback=['text']) + self.substitute_output = config.getboolean('SUBSTITUTE_OUTPUT', fallback=True) + self.substitute_output_atomic = config.getboolean('SUBSTITUTE_OUTPUT_ATOMIC', fallback=True) if 'METHOD' in config and config['METHOD'] == "pytorch_ocr-transformer": - self.ocr_engine = TransformerEngineLineOCR(json_file, self.device) + self.ocr_engine = TransformerEngineLineOCR(json_file, self.device, + substitute_output_atomic=self.substitute_output_atomic) else: - self.ocr_engine = PytorchEngineLineOCR(json_file, self.device) + self.ocr_engine = PytorchEngineLineOCR(json_file, self.device, + substitute_output_atomic=self.substitute_output_atomic) def process_page(self, img, page_layout: PageLayout): lines_to_process = [] @@ -549,7 +553,7 @@ def process_page(self, img, page_layout: PageLayout): return page_layout def substitute_transcription(self, transcription): - if self.ocr_engine.output_substitution is not None: + if self.substitute_output and self.ocr_engine.output_substitution is not None: return self.ocr_engine.output_substitution(transcription) else: return transcription @@ -564,7 +568,7 @@ def get_line_confidence(line): confidences = get_line_confidence(line, log_probs=log_probs) return np.quantile(confidences, .50) except ValueError as e: - logger.warning(f'Error: PageOCR is unable to get confidence of line {line.id} due to exception: {e}.') + logger.warning(f'PageOCR is unable to get confidence of line {line.id} due to exception: {e}.') return default_confidence return default_confidence diff --git a/pero_ocr/music/output_translator.py b/pero_ocr/music/output_translator.py index 21d48ee..e45987d 100644 --- a/pero_ocr/music/output_translator.py +++ b/pero_ocr/music/output_translator.py @@ -12,10 +12,11 @@ class OutputTranslator: """Class for translating output from shorter form to longer form using simple dictionary. Used for example in Optical Music Recognition to translate shorter SSemantic encoding to Semantic encoding.""" - def __init__(self, dictionary: dict = None, filename: str = None): + def __init__(self, dictionary: dict = None, filename: str = None, atomic: bool = False): self.dictionary = self.load_dictionary(dictionary, filename) self.dictionary_reversed = {v: k for k, v in self.dictionary.items()} self.n_existing_labels = set() + self.atomic = atomic def __call__(self, inputs: Union[str, list], reverse: bool = False) -> Union[str, list]: if isinstance(inputs, list): @@ -32,13 +33,22 @@ def translate_lines(self, lines: list, reverse: bool = False) -> list: return [self.translate_line(line, reverse) for line in lines] def translate_line(self, line, reverse: bool = False): - line = line.strip('"').strip() - symbols = re.split(r'\s+', line) - converted_symbols = [self.translate_symbol(symbol, reverse) for symbol in symbols] + line_stripped = line.strip('"').strip() + symbols = re.split(r'\s+', line_stripped) + + converted_symbols = [] + for symbol in symbols: + translation = self.translate_symbol(symbol, reverse) + if translation is None: + if self.atomic: + return line + converted_symbols.append(symbol) + else: + converted_symbols.append(translation) return ' '.join(converted_symbols) - def translate_symbol(self, symbol: str, reverse: bool = False): + def translate_symbol(self, symbol: str, reverse: bool = False) -> str | None: dictionary = self.dictionary_reversed if reverse else self.dictionary translation = dictionary.get(symbol, None) @@ -49,7 +59,7 @@ def translate_symbol(self, symbol: str, reverse: bool = False): logger.debug(f'Not existing label: ({symbol})') self.n_existing_labels.add(symbol) - return symbol + return None @staticmethod def load_dictionary(dictionary: dict = None, filename: str = None) -> dict: diff --git a/pero_ocr/ocr_engine/line_ocr_engine.py b/pero_ocr/ocr_engine/line_ocr_engine.py index 52f1d9e..b3f6e02 100644 --- a/pero_ocr/ocr_engine/line_ocr_engine.py +++ b/pero_ocr/ocr_engine/line_ocr_engine.py @@ -14,7 +14,7 @@ class BaseEngineLineOCR(object): - def __init__(self, json_def, device, batch_size=8, model_type="ctc"): + def __init__(self, json_def, device, batch_size=8, model_type="ctc", substitute_output_atomic: bool = True): with open(json_def, 'r', encoding='utf8') as f: self.config = json.load(f) @@ -30,7 +30,8 @@ def __init__(self, json_def, device, batch_size=8, model_type="ctc"): self.output_substitution = None if 'output_substitution_table' in self.config: - self.output_substitution = OutputTranslator(dictionary=self.config['output_substitution_table']) + self.output_substitution = OutputTranslator(dictionary=self.config['output_substitution_table'], + atomic=substitute_output_atomic) self.net_name = self.config['net_name'] if "embed_num" in self.config: diff --git a/pero_ocr/ocr_engine/pytorch_ocr_engine.py b/pero_ocr/ocr_engine/pytorch_ocr_engine.py index bffaea9..fc37bf1 100644 --- a/pero_ocr/ocr_engine/pytorch_ocr_engine.py +++ b/pero_ocr/ocr_engine/pytorch_ocr_engine.py @@ -35,8 +35,9 @@ def greedy_decode_ctc(scores_probs, chars): class PytorchEngineLineOCR(BaseEngineLineOCR): - def __init__(self, json_def, device, batch_size=8): - super(PytorchEngineLineOCR, self).__init__(json_def, device, batch_size=batch_size) + def __init__(self, json_def, device, batch_size=8, substitute_output_atomic: bool = True): + super(PytorchEngineLineOCR, self).__init__(json_def, device, batch_size=batch_size, + substitute_output_atomic=substitute_output_atomic) self.net_subsampling = 4 self.characters = list(self.characters) + [u'\u200B'] diff --git a/pero_ocr/ocr_engine/transformer_ocr_engine.py b/pero_ocr/ocr_engine/transformer_ocr_engine.py index 4bd0295..6ea4341 100644 --- a/pero_ocr/ocr_engine/transformer_ocr_engine.py +++ b/pero_ocr/ocr_engine/transformer_ocr_engine.py @@ -8,8 +8,10 @@ class TransformerEngineLineOCR(BaseEngineLineOCR): - def __init__(self, json_def, device, batch_size=4): - super(TransformerEngineLineOCR, self).__init__(json_def, device, batch_size=batch_size, model_type="transformer") + def __init__(self, json_def, device, batch_size=4, substitute_output_atomic: bool = True): + super(TransformerEngineLineOCR, self).__init__(json_def, device, batch_size=batch_size, + model_type="transformer", + substitute_output_atomic=substitute_output_atomic) self.characters = list(self.characters) + [u'\u200B', ''] From 2f847120c0df01d2c927297ea913a57858c3f38f Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 30 May 2024 10:21:32 +0200 Subject: [PATCH 51/76] Fix `provides_ctc_logits` to look to all `ocrs` instead of `ocr` --- pero_ocr/document_ocr/page_parser.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 0133c91..be74206 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -606,15 +606,13 @@ def __init__(self, config, device=None, config_path='', ): self.filter_confident_lines_threshold = config['PAGE_PARSER'].getfloat('FILTER_CONFIDENT_LINES_THRESHOLD', fallback=-1) - self.layout_parser = None - self.line_cropper = None - self.ocr = None - self.decoder = None - self.device = device if device is not None else get_default_device() + self.layout_parsers = {} self.line_croppers = {} self.ocrs = {} + self.decoder = None + if self.run_layout_parser: self.layout_parsers = self.init_config_sections(config, config_path, 'LAYOUT_PARSER', layout_parser_factory) if self.run_line_cropper: @@ -639,10 +637,10 @@ def compute_line_confidence(line, threshold=None): @property def provides_ctc_logits(self): - if not self.ocr: + if not self.ocrs: return False - return self.ocr.provides_ctc_logits + return all(ocr.provides_ctc_logits for ocr in self.ocrs.values()) def update_confidences(self, page_layout): for line in page_layout.lines_iterator(): From eddf0e37e50da8ef4ef68a6fff810b55c622b0be Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 30 May 2024 18:30:44 +0200 Subject: [PATCH 52/76] Change `SUBSTITUTE_OUTPUT_ATOMIC` to work on a page level, not individual lines. --- pero_ocr/document_ocr/page_parser.py | 26 ++++++++++++++++++++------ pero_ocr/music/output_translator.py | 8 +++++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index be74206..dd12b9e 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -6,6 +6,7 @@ import time import re from typing import Union, Tuple +from copy import deepcopy import torch.cuda @@ -545,18 +546,31 @@ def process_page(self, img, page_layout: PageLayout): if (line.transcription_confidence is None or line.transcription_confidence < new_line.transcription_confidence): - line.transcription = self.substitute_transcription(line_transcription) + line.transcription = line_transcription line.logits = line_logits line.characters = self.ocr_engine.characters line.logit_coords = line_logit_coords line.transcription_confidence = new_line.transcription_confidence - return page_layout - def substitute_transcription(self, transcription): if self.substitute_output and self.ocr_engine.output_substitution is not None: - return self.ocr_engine.output_substitution(transcription) - else: - return transcription + self.substitute_transcriptions(lines_to_process) + + return page_layout + + def substitute_transcriptions(self, lines_to_process: list[TextLine]): + transcriptions_substituted = [] + + for line in lines_to_process: + transcriptions_substituted.append(self.ocr_engine.output_substitution(line.transcription)) + + if transcriptions_substituted[-1] is None: + if self.substitute_output_atomic: + return # scratch everything if the last line couldn't be substituted atomically + else: + transcriptions_substituted[-1] = line.transcription # keep the original transcription + + for line, transcription_substituted in zip(lines_to_process, transcriptions_substituted): + line.transcription = transcription_substituted @staticmethod def get_line_confidence(line): diff --git a/pero_ocr/music/output_translator.py b/pero_ocr/music/output_translator.py index e45987d..85b1399 100644 --- a/pero_ocr/music/output_translator.py +++ b/pero_ocr/music/output_translator.py @@ -16,9 +16,11 @@ def __init__(self, dictionary: dict = None, filename: str = None, atomic: bool = self.dictionary = self.load_dictionary(dictionary, filename) self.dictionary_reversed = {v: k for k, v in self.dictionary.items()} self.n_existing_labels = set() + + # ensures atomicity on line level (if one symbol is not found, return None and let caller handle it) self.atomic = atomic - def __call__(self, inputs: Union[str, list], reverse: bool = False) -> Union[str, list]: + def __call__(self, inputs: Union[str, list], reverse: bool = False) -> Union[str, list, None]: if isinstance(inputs, list): if len(inputs[0]) > 1: # list of strings (lines) return self.translate_lines(inputs, reverse) @@ -33,7 +35,7 @@ def translate_lines(self, lines: list, reverse: bool = False) -> list: return [self.translate_line(line, reverse) for line in lines] def translate_line(self, line, reverse: bool = False): - line_stripped = line.strip('"').strip() + line_stripped = line.replace('"', ' ').strip() symbols = re.split(r'\s+', line_stripped) converted_symbols = [] @@ -41,7 +43,7 @@ def translate_line(self, line, reverse: bool = False): translation = self.translate_symbol(symbol, reverse) if translation is None: if self.atomic: - return line + return None # return None and let caller handle it (e.g. by storing the original line or breaking) converted_symbols.append(symbol) else: converted_symbols.append(translation) From 70a7e35f11d953f028d9ba767d330d3baa065eb3 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 30 May 2024 18:56:32 +0200 Subject: [PATCH 53/76] Add config parameter `UPDATE_TRANSCRIPTION_BY_CONFIDENCE` Parameter sets if PageOCR should update to new line: - every time (false) - only if better confidence (true) Applies in case of rerunning OCR on previously transcribed line) --- pero_ocr/document_ocr/page_parser.py | 34 +++++++++++++++++----------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index dd12b9e..ec912af 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -510,6 +510,8 @@ def crop_lines(self, img, lines: list): class PageOCR: + default_confidence = 0.0 + def __init__(self, config, device, config_path=''): json_file = compose_path(config['OCR_JSON'], config_path) use_cpu = config.getboolean('USE_CPU') @@ -518,6 +520,8 @@ def __init__(self, config, device, config_path=''): self.categories = config_get_list(config, key='CATEGORIES', fallback=['text']) self.substitute_output = config.getboolean('SUBSTITUTE_OUTPUT', fallback=True) self.substitute_output_atomic = config.getboolean('SUBSTITUTE_OUTPUT_ATOMIC', fallback=True) + self.update_transcription_by_confidence = config.getboolean( + 'UPDATE_TRANSCRIPTION_BY_CONFIDENCE', fallback=False) if 'METHOD' in config and config['METHOD'] == "pytorch_ocr-transformer": self.ocr_engine = TransformerEngineLineOCR(json_file, self.device, @@ -544,13 +548,12 @@ def process_page(self, img, page_layout: PageLayout): logit_coords=line_logit_coords) new_line.transcription_confidence = self.get_line_confidence(new_line) - if (line.transcription_confidence is None or - line.transcription_confidence < new_line.transcription_confidence): - line.transcription = line_transcription - line.logits = line_logits - line.characters = self.ocr_engine.characters - line.logit_coords = line_logit_coords - line.transcription_confidence = new_line.transcription_confidence + if not self.update_transcription_by_confidence: + self.update_line(line, new_line) + else: + if (line.transcription_confidence in [None, self.default_confidence] or + line.transcription_confidence < new_line.transcription_confidence): + self.update_line(line, new_line) if self.substitute_output and self.ocr_engine.output_substitution is not None: self.substitute_transcriptions(lines_to_process) @@ -572,10 +575,7 @@ def substitute_transcriptions(self, lines_to_process: list[TextLine]): for line, transcription_substituted in zip(lines_to_process, transcriptions_substituted): line.transcription = transcription_substituted - @staticmethod - def get_line_confidence(line): - default_confidence = 0.0 - + def get_line_confidence(self, line): if line.transcription: try: log_probs = line.get_full_logprobs()[line.logit_coords[0]:line.logit_coords[1]] @@ -583,13 +583,21 @@ def get_line_confidence(line): return np.quantile(confidences, .50) except ValueError as e: logger.warning(f'PageOCR is unable to get confidence of line {line.id} due to exception: {e}.') - return default_confidence - return default_confidence + return self.default_confidence + return self.default_confidence @property def provides_ctc_logits(self): return isinstance(self.ocr_engine, PytorchEngineLineOCR) or isinstance(self.ocr_engine, TransformerEngineLineOCR) + @staticmethod + def update_line(line, new_line): + line.transcription = new_line.transcription + line.logits = new_line.logits + line.characters = new_line.characters + line.logit_coords = new_line.logit_coords + line.transcription_confidence = new_line.transcription_confidence + def get_prob(best_ids, best_probs): last_id = -1 From 9dcd33f2c56156e95c5e2c6bdda7f6b500d7b7dc Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Mon, 17 Jun 2024 14:35:10 +0200 Subject: [PATCH 54/76] Add ALTO baseline (export + import) in two options (float or points) - Versions older than 4.2 defines baseline as a simple float. (that's where the original baseline comes from) - version 4.2 and never defines baseline as a PointsType string with recommend format: "x1,y1 x2,y2 ..." --- pero_ocr/core/layout.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 53b7c22..d623580 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -127,7 +127,7 @@ def from_pagexml(cls, line_element: ET.SubElement, schema, fallback_index: int): baseline = line_element.find(schema + 'Baseline') if baseline is not None: - new_textline.baseline = get_coords_form_pagexml(baseline, schema) + new_textline.baseline = get_coords_from_pagexml(baseline, schema) else: logger.warning(f'Warning: Baseline is missing in TextLine. ' f'Skipping this line during import. Line ID: {new_textline.id}') @@ -135,7 +135,7 @@ def from_pagexml(cls, line_element: ET.SubElement, schema, fallback_index: int): textline = line_element.find(schema + 'Coords') if textline is not None: - new_textline.polygon = get_coords_form_pagexml(textline, schema) + new_textline.polygon = get_coords_from_pagexml(textline, schema) if not new_textline.heights: guess_line_heights_from_polygon(new_textline, use_center=False, n=len(new_textline.baseline)) @@ -181,9 +181,8 @@ def to_altoxml(self, text_block, arabic_helper, min_line_confidence): return text_line = ET.SubElement(text_block, "TextLine") - text_line_baseline = int(np.average(np.array(self.baseline)[:, 1])) text_line.set("ID", f'line_{self.id}') - text_line.set("BASELINE", str(text_line_baseline)) + text_line.set("BASELINE", self.to_altoxml_baseline()) text_line_height, text_line_width, text_line_vpos, text_line_hpos = get_hwvh(self.polygon) @@ -316,13 +315,25 @@ def to_altoxml_text(self, text_line, arabic_helper, space.set("HPOS", str(int(np.max(all_x)))) letter_counter += len(splitted_transcription[w]) + 1 + def to_altoxml_baseline(self, alto_version=2.0) -> str: + if alto_version < 4.2: + # ALTO 4.1 and older accept baseline only as a single point + text_line_baseline = int(np.round(np.average(np.array(self.baseline)[:, 1]))) + return str(text_line_baseline) + + # ALTO 4.2 and newer accept baseline as a string with list of points. Recommended "x1,y1 x2,y2 ..." format. + baseline_points = [f"{int(np.round(coord[0]))},{int(np.round(coord[1]))}" + for coord in self.baseline] + baseline_points = " ".join(baseline_points) + return baseline_points + @classmethod def from_altoxml(cls, line: ET.SubElement, schema): hpos = int(line.attrib['HPOS']) vpos = int(line.attrib['VPOS']) width = int(line.attrib['WIDTH']) height = int(line.attrib['HEIGHT']) - baseline = int(line.attrib['BASELINE']) + baseline = cls.from_altoxml_baseline(line.attrib['BASELINE']) new_textline = cls(id=line.attrib['ID'], baseline=np.asarray([[hpos, baseline], [hpos + width, baseline]]), @@ -345,6 +356,19 @@ def from_altoxml(cls, line: ET.SubElement, schema): new_textline.transcription = word return new_textline + @staticmethod + def from_altoxml_baseline(baseline_str): + baseline = baseline_str.strip().split(' ') + + if len(baseline) == 1: # baseline is only one number (probably ALTO version older than 4.2) + try: + return float(baseline[0]) + except ValueError: + pass + + coords = [t.split(",") for t in baseline] + return np.asarray([[int(round(float(x))), int(round(float(y)))] for x, y in coords]) + class RegionLayout(object): def __init__(self, id: str, @@ -416,7 +440,7 @@ def to_pagexml(self, page_element: ET.SubElement, validate_id: bool = False): @classmethod def from_pagexml(cls, region_element: ET.SubElement, schema): coords_element = region_element.find(schema + 'Coords') - region_coords = get_coords_form_pagexml(coords_element, schema) + region_coords = get_coords_from_pagexml(coords_element, schema) region_type = None if "type" in region_element.attrib: @@ -490,7 +514,7 @@ def from_altoxml(cls, text_block: ET.SubElement, schema): return region_layout -def get_coords_form_pagexml(coords_element, schema): +def get_coords_from_pagexml(coords_element, schema): if 'points' in coords_element.attrib: coords = points_string_to_array(coords_element.attrib['points']) else: From 1a46c0091b833d2ab92b58cca6fb6cee6585b832 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Wed, 19 Jun 2024 09:42:19 +0200 Subject: [PATCH 55/76] Add ALTO versions (options how to export baseline) + both baseline import options. - Versions older than 4.2 defines baseline as a simple float. (baseline is exported as mean of all Y baseline points) - version 4.2 and never defines baseline as a PointsType string with recommend format: "x1,y1 x2,y2 ..." --- pero_ocr/core/layout.py | 96 ++++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 35 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index d623580..3a8e310 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -29,6 +29,9 @@ class PAGEVersion(Enum): PAGE_2019_07_15 = 1 PAGE_2013_07_15 = 2 +class ALTOVersion(Enum): + ALTO_v2_x = 1 + ALTO_v4_4 = 2 def log_softmax(x): a = np.logaddexp.reduce(x, axis=1)[:, np.newaxis] @@ -176,13 +179,13 @@ def from_pagexml_parse_custom(self, custom_str): heights = heights_array self.heights = heights.tolist() - def to_altoxml(self, text_block, arabic_helper, min_line_confidence): + def to_altoxml(self, text_block, arabic_helper, min_line_confidence, version: ALTOVersion): if self.transcription_confidence is not None and self.transcription_confidence < min_line_confidence: return text_line = ET.SubElement(text_block, "TextLine") text_line.set("ID", f'line_{self.id}') - text_line.set("BASELINE", self.to_altoxml_baseline()) + text_line.set("BASELINE", self.to_altoxml_baseline(version)) text_line_height, text_line_width, text_line_vpos, text_line_hpos = get_hwvh(self.polygon) @@ -315,17 +318,18 @@ def to_altoxml_text(self, text_line, arabic_helper, space.set("HPOS", str(int(np.max(all_x)))) letter_counter += len(splitted_transcription[w]) + 1 - def to_altoxml_baseline(self, alto_version=2.0) -> str: - if alto_version < 4.2: + def to_altoxml_baseline(self, version: ALTOVersion) -> str: + if version == ALTOVersion.ALTO_v2_x: # ALTO 4.1 and older accept baseline only as a single point - text_line_baseline = int(np.round(np.average(np.array(self.baseline)[:, 1]))) - return str(text_line_baseline) - - # ALTO 4.2 and newer accept baseline as a string with list of points. Recommended "x1,y1 x2,y2 ..." format. - baseline_points = [f"{int(np.round(coord[0]))},{int(np.round(coord[1]))}" - for coord in self.baseline] - baseline_points = " ".join(baseline_points) - return baseline_points + baseline = int(np.round(np.average(np.array(self.baseline)[:, 1]))) + return str(baseline) + elif version == ALTOVersion.ALTO_v4_4: + # ALTO 4.2 and newer accept baseline as a string with list of points. Recommended "x1,y1 x2,y2 ..." format. + baseline_points = [f"{x},{y}" for x, y in np.round(self.baseline).astype('int')] + baseline_points = " ".join(baseline_points) + return baseline_points + else: + return "" @classmethod def from_altoxml(cls, line: ET.SubElement, schema): @@ -333,17 +337,10 @@ def from_altoxml(cls, line: ET.SubElement, schema): vpos = int(line.attrib['VPOS']) width = int(line.attrib['WIDTH']) height = int(line.attrib['HEIGHT']) - baseline = cls.from_altoxml_baseline(line.attrib['BASELINE']) + baseline_str = line.attrib['BASELINE'] + baseline, heights, polygon = cls.from_altoxml_polygon(baseline_str, hpos, vpos, width, height) - new_textline = cls(id=line.attrib['ID'], - baseline=np.asarray([[hpos, baseline], [hpos + width, baseline]]), - heights=np.asarray([height + vpos - baseline, baseline - vpos])) - - polygon = [[hpos, vpos], - [hpos + width, vpos], - [hpos + width, vpos + height], - [hpos, vpos + height]] - new_textline.polygon = np.asarray(polygon) + new_textline = cls(id=line.attrib['ID'], baseline=baseline, heights=heights, polygon=polygon) word = '' start = True @@ -357,17 +354,40 @@ def from_altoxml(cls, line: ET.SubElement, schema): return new_textline @staticmethod - def from_altoxml_baseline(baseline_str): + def from_altoxml_polygon(baseline_str, hpos, vpos, width, height) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: baseline = baseline_str.strip().split(' ') - if len(baseline) == 1: # baseline is only one number (probably ALTO version older than 4.2) + if len(baseline) == 1: + # baseline is only one number (probably ALTOversion = 2.x) try: - return float(baseline[0]) + baseline = float(baseline[0]) except ValueError: - pass + baseline = vpos + height # fallback: baseline is at the bottom of the bounding box, heights[1] = 0 + + baseline_arr = np.asarray([[hpos, baseline], [hpos + width, baseline]]) + heights = np.asarray([baseline - vpos, vpos + height - baseline]) + polygon = np.asarray([[hpos, vpos], + [hpos + width, vpos], + [hpos + width, vpos + height], + [hpos, vpos + height]]) + return baseline_arr, heights, polygon + else: + # baseline is list of points (probably ALTOversion = 4.4) + baseline_coords = [t.split(",") for t in baseline] + baseline = np.asarray([[int(round(float(x))), int(round(float(y)))] for x, y in baseline_coords]) + + # count heights from the FIRST element of baseline + heights = np.asarray([baseline[0, 1] - vpos, vpos + height - baseline[0, 1]]) + print(f'height[0]: {heights[0]} = {baseline[0, 1]} - {vpos}') + print(f'height[1]: {heights[1]} = {vpos + height} - {baseline[0, 1]}') - coords = [t.split(",") for t in baseline] - return np.asarray([[int(round(float(x))), int(round(float(y)))] for x, y in coords]) + coords_top = [[x, y - heights[0]] for x, y in baseline] + coords_bottom = [[x, y + heights[1]] for x, y in baseline] + # reverse coords_bottom to create polygon in clockwise order + coords_bottom.reverse() + polygon = np.concatenate([coords_top, coords_bottom, coords_top[:1]]) + + return baseline, heights, polygon class RegionLayout(object): @@ -471,7 +491,7 @@ def from_pagexml(cls, region_element: ET.SubElement, schema): return layout_region def to_altoxml(self, print_space, arabic_helper, min_line_confidence, - print_space_coords: Tuple[int, int, int, int]) -> Tuple[int, int, int, int]: + print_space_coords: Tuple[int, int, int, int], version: ALTOVersion) -> Tuple[int, int, int, int]: print_space_height, print_space_width, print_space_vpos, print_space_hpos = print_space_coords text_block = ET.SubElement(print_space, "TextBlock") @@ -493,7 +513,7 @@ def to_altoxml(self, print_space, arabic_helper, min_line_confidence, for line in self.lines: if not line.transcription or line.transcription.strip() == "": continue - line.to_altoxml(text_block, arabic_helper, min_line_confidence) + line.to_altoxml(text_block, arabic_helper, min_line_confidence, version) return print_space_height, print_space_width, print_space_vpos, print_space_hpos @classmethod @@ -702,12 +722,17 @@ def to_pagexml(self, file_name: str, creator: str = 'Pero OCR', with open(file_name, 'w', encoding='utf-8') as out_f: out_f.write(xml_string) - def to_altoxml_string(self, ocr_processing_element: ET.SubElement = None, page_uuid: str = None, min_line_confidence: float = 0): + def to_altoxml_string(self, ocr_processing_element: ET.SubElement = None, page_uuid: str = None, + min_line_confidence: float = 0, version: ALTOVersion = ALTOVersion.ALTO_v4_4): arabic_helper = ArabicHelper() NSMAP = {"xlink": 'http://www.w3.org/1999/xlink', "xsi": 'http://www.w3.org/2001/XMLSchema-instance'} root = ET.Element("alto", nsmap=NSMAP) - root.set("xmlns", "http://www.loc.gov/standards/alto/ns-v2#") + + if version == ALTOVersion.ALTO_v4_4: + root.set("xmlns", "http://www.loc.gov/standards/alto/ns-v4#") + elif version == ALTOVersion.ALTO_v2_1: + root.set("xmlns", "http://www.loc.gov/standards/alto/ns-v2#") description = ET.SubElement(root, "Description") measurement_unit = ET.SubElement(description, "MeasurementUnit") @@ -743,7 +768,7 @@ def to_altoxml_string(self, ocr_processing_element: ET.SubElement = None, page_u print_space_coords = (print_space_height, print_space_width, print_space_vpos, print_space_hpos) for block in self.regions: - print_space_coords = block.to_altoxml(print_space, arabic_helper, min_line_confidence, print_space_coords) + print_space_coords = block.to_altoxml(print_space, arabic_helper, min_line_confidence, print_space_coords, version) print_space_height, print_space_width, print_space_vpos, print_space_hpos = print_space_coords @@ -774,8 +799,9 @@ def to_altoxml_string(self, ocr_processing_element: ET.SubElement = None, page_u return ET.tostring(root, pretty_print=True, encoding="utf-8", xml_declaration=True).decode("utf-8") - def to_altoxml(self, file_name: str, ocr_processing_element: ET.SubElement = None, page_uuid: str = None): - alto_string = self.to_altoxml_string(ocr_processing_element=ocr_processing_element, page_uuid=page_uuid) + def to_altoxml(self, file_name: str, ocr_processing_element: ET.SubElement = None, page_uuid: str = None, + version: ALTOVersion = ALTOVersion.ALTO_v4_4): + alto_string = self.to_altoxml_string(ocr_processing_element=ocr_processing_element, page_uuid=page_uuid, version=version) with open(file_name, 'w', encoding='utf-8') as out_f: out_f.write(alto_string) From 2141002ca9b86ce865271d01a0e2e89498fa3a46 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Wed, 19 Jun 2024 13:26:00 +0200 Subject: [PATCH 56/76] Save polygon points only as positive numbers. (XSD validation issue) --- pero_ocr/core/layout.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 3a8e310..e1c0e9b 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -94,17 +94,11 @@ def to_pagexml(self, region_element: ET.SubElement, fallback_id: int, validate_i coords = ET.SubElement(text_line, "Coords") if self.polygon is not None: - points = ["{},{}".format(int(np.round(coord[0])), int(np.round(coord[1]))) for coord in - self.polygon] - points = " ".join(points) - coords.set("points", points) + coords.set("points", coords_to_pagexml_points(self.polygon)) if self.baseline is not None: baseline_element = ET.SubElement(text_line, "Baseline") - points = ["{},{}".format(int(np.round(coord[0])), int(np.round(coord[1]))) for coord in - self.baseline] - points = " ".join(points) - baseline_element.set("points", points) + baseline_element.set("points", coords_to_pagexml_points(self.baseline)) if self.transcription is not None: text_element = ET.SubElement(text_line, "TextEquiv") @@ -444,9 +438,8 @@ def to_pagexml(self, page_element: ET.SubElement, validate_id: bool = False): custom = json.dumps(custom) region_element.set("custom", custom) - points = ["{},{}".format(int(np.round(coord[0])), int(np.round(coord[1]))) for coord in self.polygon] - points = " ".join(points) - coords.set("points", points) + coords.set("points", coords_to_pagexml_points(self.polygon)) + if self.transcription is not None: text_element = ET.SubElement(region_element, "TextEquiv") text_element = ET.SubElement(text_element, "Unicode") @@ -546,6 +539,13 @@ def get_coords_from_pagexml(coords_element, schema): return coords +def coords_to_pagexml_points(polygon: np.ndarray) -> str: + polygon = np.round(polygon).astype(np.dtype('int')) + points = [f"{x},{y}" for x, y in np.maximum(polygon, 0)] + points = " ".join(points) + return points + + def guess_line_heights_from_polygon(text_line: TextLine, use_center: bool = False, n: int = 10, interpolate=False): ''' Guess line heights for line if missing (e.g. import from Transkribus). From 34c65840007fdf039bb4d95dbbb67fc0508eb4f2 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Wed, 19 Jun 2024 13:30:55 +0200 Subject: [PATCH 57/76] Remove prints. --- pero_ocr/core/layout.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index e1c0e9b..18afbcc 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -372,8 +372,6 @@ def from_altoxml_polygon(baseline_str, hpos, vpos, width, height) -> Tuple[np.nd # count heights from the FIRST element of baseline heights = np.asarray([baseline[0, 1] - vpos, vpos + height - baseline[0, 1]]) - print(f'height[0]: {heights[0]} = {baseline[0, 1]} - {vpos}') - print(f'height[1]: {heights[1]} = {vpos + height} - {baseline[0, 1]}') coords_top = [[x, y - heights[0]] for x, y in baseline] coords_bottom = [[x, y + heights[1]] for x, y in baseline] From 82b3e70cee460780ac09cc43593aa890708ea01d Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Wed, 19 Jun 2024 14:00:31 +0200 Subject: [PATCH 58/76] Allow run when at least one ORC engine `provide_ctc_logits`. --- pero_ocr/document_ocr/page_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index ec912af..4521662 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -662,7 +662,7 @@ def provides_ctc_logits(self): if not self.ocrs: return False - return all(ocr.provides_ctc_logits for ocr in self.ocrs.values()) + return any(ocr.provides_ctc_logits for ocr in self.ocrs.values()) def update_confidences(self, page_layout): for line in page_layout.lines_iterator(): From 2aed4bc042b8a77045822afc23b0629ac47488f3 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Wed, 19 Jun 2024 14:36:41 +0200 Subject: [PATCH 59/76] Fix README.md example + delete false info about setup.py. --- README.md | 13 +------------ pero_ocr/document_ocr/page_parser.py | 4 +++- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 59339fb..eab53ad 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,6 @@ For the current shell session, this can be achieved by setting ``PYTHONPATH`` up export PYTHONPATH=/path/to/the/repo:$PYTHONPATH ``` -As a more permanent solution, a very simplistic `setup.py` is prepared: -``` -python setup.py develop -``` -Beware that the `setup.py` does not promise to bring all the required stuff, e.g. setting CUDA up is up to you. - -Pero can be later removed from your Python distribution by running: -``` -python setup.py develop --uninstall -``` - ## Available models General layout analysis (printed and handwritten) with european printed OCR specialized to czech newspapers can be [downloaded here](https://nextcloud.fit.vutbr.cz/s/NtAbHTNkZFpapdJ). The OCR engine is suitable for most european printed documents. It is specialized for low-quality czech newspapers digitized from microfilms, but it provides very good results for almast all types of printed documents in most languages. If you are interested in processing printed fraktur fonts, handwritten documents or medieval manuscripts, feel free to contact the authors. The newest OCR engines are available at [pero-ocr.fit.vutbr.cz](https://pero-ocr.fit.vutbr.cz). OCR engines are available also through API runing at [pero-ocr.fit.vutbr.cz/api](https://pero-ocr.fit.vutbr.cz/api), [github repository](https://github.com/DCGM/pero-ocr-api). @@ -63,7 +52,7 @@ import os import configparser import cv2 import numpy as np -from pero_ocr.document_ocr.layout import PageLayout +from pero_ocr.core.layout import PageLayout from pero_ocr.document_ocr.page_parser import PageParser # Read config file. diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 4521662..ddcebbb 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -6,7 +6,6 @@ import time import re from typing import Union, Tuple -from copy import deepcopy import torch.cuda @@ -621,6 +620,9 @@ def get_default_device(): class PageParser(object): def __init__(self, config, device=None, config_path='', ): + if not config.sections(): + raise ValueError('Config file is empty or does not exist.') + self.run_layout_parser = config['PAGE_PARSER'].getboolean('RUN_LAYOUT_PARSER', fallback=False) self.run_line_cropper = config['PAGE_PARSER'].getboolean('RUN_LINE_CROPPER', fallback=False) self.run_ocr = config['PAGE_PARSER'].getboolean('RUN_OCR', fallback=False) From 4efdbabfa94b5cf4dc3e13427faa7f3073047b26 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 20 Jun 2024 09:59:59 +0200 Subject: [PATCH 60/76] Update README.md - spelling correction. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eab53ad..b92747d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ export PYTHONPATH=/path/to/the/repo:$PYTHONPATH General layout analysis (printed and handwritten) with european printed OCR specialized to czech newspapers can be [downloaded here](https://nextcloud.fit.vutbr.cz/s/NtAbHTNkZFpapdJ). The OCR engine is suitable for most european printed documents. It is specialized for low-quality czech newspapers digitized from microfilms, but it provides very good results for almast all types of printed documents in most languages. If you are interested in processing printed fraktur fonts, handwritten documents or medieval manuscripts, feel free to contact the authors. The newest OCR engines are available at [pero-ocr.fit.vutbr.cz](https://pero-ocr.fit.vutbr.cz). OCR engines are available also through API runing at [pero-ocr.fit.vutbr.cz/api](https://pero-ocr.fit.vutbr.cz/api), [github repository](https://github.com/DCGM/pero-ocr-api). ## Command line application -A command line application is ./user_scripts/parse_folder.py. It is able to process images in a directory using an OCR engine. It can render detected lines in an image and provide document content in Page XML and ALTO XML formats. Additionally, it is able to crop all text lines as rectangular regions of normalized size and save them into separate image files. +A command line application is `./user_scripts/parse_folder.py.` It is able to process images in a directory using an OCR engine. It can render detected lines in an image and provide document content in Page XML and ALTO XML formats. Additionally, it is able to crop all text lines as rectangular regions of normalized size and save them into separate image files. ## Running command line application in container A docker container can be built from the sourcecode to run scripts and programs based on the pero-ocr. Example of running the `parse_folder.py` script to generate page-xml files for images in input directory: @@ -106,7 +106,7 @@ Currently, only unittests are provided with the code. Some of the code. So simpl ``` #### Simple regression testing -Regression testing can be done by `test/processing_test.sh`. Script calls containerized `parser_folder.py` to process input images and page-xml files and calls user suplied comparison script to compare outputs to example outputs suplied by user. `PERO-OCR` container have to be built in advance to run the test, see 'Running command line application in container' chapter. Script can be called like this: +Regression testing can be done by `test/processing_test.sh`. Script calls containerized `parse_folder.py` to process input images and page-xml files and calls user suplied comparison script to compare outputs to example outputs suplied by user. `PERO-OCR` container have to be built in advance to run the test, see 'Running command line application in container' chapter. Script can be called like this: ```shell sh test/processing_test.sh \ --input-images path/to/input/image/directory \ From 134f51a8214c025445d64586e91342990cd422a6 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 20 Jun 2024 13:28:54 +0200 Subject: [PATCH 61/76] Add typing Optional to allow lower versions of Python (tested on Python3.9) --- pero_ocr/core/layout.py | 2 +- pero_ocr/layout_engines/layout_helpers.py | 5 ++--- pero_ocr/music/music_structures.py | 9 +++++---- pero_ocr/music/music_symbols.py | 3 ++- pero_ocr/music/output_translator.py | 3 ++- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 18afbcc..c4d2639 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -396,7 +396,7 @@ def __init__(self, id: str, self.transcription = None self.detection_confidence = detection_confidence - def get_lines_of_category(self, categories: str | list): + def get_lines_of_category(self, categories: Union[str, list]): if isinstance(categories, str): categories = [categories] diff --git a/pero_ocr/layout_engines/layout_helpers.py b/pero_ocr/layout_engines/layout_helpers.py index 49c62ba..4935791 100644 --- a/pero_ocr/layout_engines/layout_helpers.py +++ b/pero_ocr/layout_engines/layout_helpers.py @@ -1,9 +1,8 @@ import math import random import warnings -import logging from copy import deepcopy -from typing import Tuple +from typing import Tuple, Optional import numpy as np import cv2 @@ -495,7 +494,7 @@ def insert_line_to_page_layout(page_layout: PageLayout, region: RegionLayout, li return page_layout -def find_region_by_id(page_layout: PageLayout, region_id: str) -> RegionLayout | None: +def find_region_by_id(page_layout: PageLayout, region_id: str) -> Optional[RegionLayout]: for region in page_layout.regions: if region.id == region_id: return region diff --git a/pero_ocr/music/music_structures.py b/pero_ocr/music/music_structures.py index 8261cab..cc1bd9f 100644 --- a/pero_ocr/music/music_structures.py +++ b/pero_ocr/music/music_structures.py @@ -10,6 +10,7 @@ import re from enum import Enum import logging +from typing import Optional import music21 as music from pero_ocr.music.music_symbols import Symbol, SymbolType, AlteredPitches, LENGTH_TO_SYMBOL @@ -75,14 +76,14 @@ def get_key(self, previous_measure_key: music.key.Key) -> music.key.Key: self.set_key(previous_measure_key) return self.keysignature - def get_start_clef(self) -> music.clef.Clef | None: + def get_start_clef(self) -> Optional[music.clef.Clef]: if self.start_clef is not None: return self.start_clef else: return self.symbol_groups[0].get_clef() def get_last_clef(self, previous_measure_last_clef: music.clef.Clef = music.clef.TrebleClef - ) -> music.clef.Clef | None: + ) -> Optional[music.clef.Clef]: if self.last_clef is not None: return self.last_clef @@ -325,7 +326,7 @@ def get_type(self): else: return SymbolGroupType.TUPLE - def get_key(self) -> music.key.Key | None: + def get_key(self) -> Optional[music.key.Key]: """Go through all labels and find key signature or return None. Returns: @@ -423,7 +424,7 @@ def get_groups_to_add(self): return groups_to_add - def get_clef(self) -> music.clef.Clef | None: + def get_clef(self) -> Optional[music.clef.Clef]: if self.type == SymbolGroupType.SYMBOL and self.symbols[0].type == SymbolType.CLEF: return self.symbols[0].repr return None diff --git a/pero_ocr/music/music_symbols.py b/pero_ocr/music/music_symbols.py index c9cb40c..d922a53 100644 --- a/pero_ocr/music/music_symbols.py +++ b/pero_ocr/music/music_symbols.py @@ -13,6 +13,7 @@ import logging from enum import Enum import re +from typing import Optional import music21 as music @@ -257,7 +258,7 @@ def __init__(self, duration: music.duration.Duration, height: str, self.gracenote = gracenote self.note_ready = False - def get_real_height(self, altered_pitches: AlteredPitches) -> music.note.Note | None: + def get_real_height(self, altered_pitches: AlteredPitches) -> Optional[music.note.Note]: """Returns the real height of the note. Args: diff --git a/pero_ocr/music/output_translator.py b/pero_ocr/music/output_translator.py index 85b1399..f96a37f 100644 --- a/pero_ocr/music/output_translator.py +++ b/pero_ocr/music/output_translator.py @@ -4,6 +4,7 @@ import logging import json import os +from typing import Optional logger = logging.getLogger(__name__) @@ -50,7 +51,7 @@ def translate_line(self, line, reverse: bool = False): return ' '.join(converted_symbols) - def translate_symbol(self, symbol: str, reverse: bool = False) -> str | None: + def translate_symbol(self, symbol: str, reverse: bool = False) -> Optional[str]: dictionary = self.dictionary_reversed if reverse else self.dictionary translation = dictionary.get(symbol, None) From 25f555c93905aa7db7806820d86b051b3ebec1c7 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 20 Jun 2024 15:15:27 +0200 Subject: [PATCH 62/76] Add libraries needed to install in docker installation. --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index f025360..aac2655 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,8 @@ dependencies = [ "pyamg", "imgaug", "arabic_reshaper", + "ultralytics", + "music21", ] From 26b0c1a279b0f42365e4af84d931db82c951abdd Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 20 Jun 2024 15:16:21 +0200 Subject: [PATCH 63/76] Update texts for better UX. --- README.md | 3 ++- test/processing_test.sh | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) mode change 100644 => 100755 test/processing_test.sh diff --git a/README.md b/README.md index b92747d..9cdd5da 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ General layout analysis (printed and handwritten) with european printed OCR spec A command line application is `./user_scripts/parse_folder.py.` It is able to process images in a directory using an OCR engine. It can render detected lines in an image and provide document content in Page XML and ALTO XML formats. Additionally, it is able to crop all text lines as rectangular regions of normalized size and save them into separate image files. ## Running command line application in container + A docker container can be built from the sourcecode to run scripts and programs based on the pero-ocr. Example of running the `parse_folder.py` script to generate page-xml files for images in input directory: ```shell docker run --rm --tty --interactive \ @@ -106,7 +107,7 @@ Currently, only unittests are provided with the code. Some of the code. So simpl ``` #### Simple regression testing -Regression testing can be done by `test/processing_test.sh`. Script calls containerized `parse_folder.py` to process input images and page-xml files and calls user suplied comparison script to compare outputs to example outputs suplied by user. `PERO-OCR` container have to be built in advance to run the test, see 'Running command line application in container' chapter. Script can be called like this: +Regression testing can be done by `test/processing_test.sh`. Script calls containerized `parse_folder.py` to process input images and page-xml files and calls user suplied comparison script to compare outputs to example outputs suplied by user. `PERO-OCR` container have to be built in advance to run the test, see [Running command line application in container](#running-command-line-application-in-container) for more information. Script can be run like this: ```shell sh test/processing_test.sh \ --input-images path/to/input/image/directory \ diff --git a/test/processing_test.sh b/test/processing_test.sh old mode 100644 new mode 100755 index f9712e8..e04df78 --- a/test/processing_test.sh +++ b/test/processing_test.sh @@ -16,12 +16,12 @@ print_help() { echo "$ sh processing_test.sh -i in_dir -o out_dir -c engine_dir/config.ini" echo "Options:" echo " -i|--input-images Input directory with test images." - echo " -x|--input-xmls Input directory with xml files." echo " -o|--output-dir Output directory for results." echo " -c|--configuration Configuration file for ocr." - echo " -e|--example Example outputs for comparison." - echo " -u|--test-utility Path to test utility." - echo " -t|--test-output Test utility output folder." + echo " -x|--input-xmls Input directory with xml files. (optional)" + echo " -e|--example Example outputs for comparison. (optional)" + echo " -u|--test-utility Path to test utility. (optional)" + echo " -t|--test-output Test utility output folder. (optional)" echo " -g|--gpu-ids Ids of GPU to use for ocr processing. (default=all)" echo " -h|--help Shows this help message." } From 3ce8bbc2ed9858b55922965fef99a3901c778b6f Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Tue, 25 Jun 2024 17:29:01 +0200 Subject: [PATCH 64/76] Make default version of ALTO to the older one. --- pero_ocr/core/layout.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index c4d2639..9e4c50f 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -721,7 +721,7 @@ def to_pagexml(self, file_name: str, creator: str = 'Pero OCR', out_f.write(xml_string) def to_altoxml_string(self, ocr_processing_element: ET.SubElement = None, page_uuid: str = None, - min_line_confidence: float = 0, version: ALTOVersion = ALTOVersion.ALTO_v4_4): + min_line_confidence: float = 0, version: ALTOVersion = ALTOVersion.ALTO_v2_x): arabic_helper = ArabicHelper() NSMAP = {"xlink": 'http://www.w3.org/1999/xlink', "xsi": 'http://www.w3.org/2001/XMLSchema-instance'} @@ -729,7 +729,7 @@ def to_altoxml_string(self, ocr_processing_element: ET.SubElement = None, page_u if version == ALTOVersion.ALTO_v4_4: root.set("xmlns", "http://www.loc.gov/standards/alto/ns-v4#") - elif version == ALTOVersion.ALTO_v2_1: + elif version == ALTOVersion.ALTO_v2_x: root.set("xmlns", "http://www.loc.gov/standards/alto/ns-v2#") description = ET.SubElement(root, "Description") @@ -798,7 +798,7 @@ def to_altoxml_string(self, ocr_processing_element: ET.SubElement = None, page_u return ET.tostring(root, pretty_print=True, encoding="utf-8", xml_declaration=True).decode("utf-8") def to_altoxml(self, file_name: str, ocr_processing_element: ET.SubElement = None, page_uuid: str = None, - version: ALTOVersion = ALTOVersion.ALTO_v4_4): + version: ALTOVersion = ALTOVersion.ALTO_v2_x): alto_string = self.to_altoxml_string(ocr_processing_element=ocr_processing_element, page_uuid=page_uuid, version=version) with open(file_name, 'w', encoding='utf-8') as out_f: out_f.write(alto_string) From 3ec79023d9555b6afdfe15067fbd1b363e78d6c7 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Tue, 25 Jun 2024 17:57:07 +0200 Subject: [PATCH 65/76] Add typing List and Tuple to allow lower versions of Python (tested on Python3.9) --- pero_ocr/core/layout.py | 2 +- pero_ocr/document_ocr/page_parser.py | 4 +- pero_ocr/layout_engines/cnn_layout_engine.py | 4 +- pero_ocr/music/music_exporter.py | 25 ++++++------ pero_ocr/music/music_structures.py | 40 ++++++++++---------- 5 files changed, 38 insertions(+), 37 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 9e4c50f..675e898 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -408,7 +408,7 @@ def replace_id(self, new_id): line.id = line.id.replace(self.id, new_id) self.id = new_id - def get_polygon_bounding_box(self) -> tuple[int, int, int, int]: + def get_polygon_bounding_box(self) -> Tuple[int, int, int, int]: """Get bounding box of region polygon which includes all polygon points. :return: tuple[int, int, int, int]: (x_min, y_min, x_max, y_max) """ diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index ddcebbb..d8d81d3 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -5,7 +5,7 @@ import math import time import re -from typing import Union, Tuple +from typing import Union, Tuple, List import torch.cuda @@ -559,7 +559,7 @@ def process_page(self, img, page_layout: PageLayout): return page_layout - def substitute_transcriptions(self, lines_to_process: list[TextLine]): + def substitute_transcriptions(self, lines_to_process: List[TextLine]): transcriptions_substituted = [] for line in lines_to_process: diff --git a/pero_ocr/layout_engines/cnn_layout_engine.py b/pero_ocr/layout_engines/cnn_layout_engine.py index c0efc88..5adc935 100644 --- a/pero_ocr/layout_engines/cnn_layout_engine.py +++ b/pero_ocr/layout_engines/cnn_layout_engine.py @@ -1,7 +1,7 @@ import numpy as np from copy import deepcopy import time -from typing import Union +from typing import Union, Tuple import cv2 from scipy import ndimage @@ -377,7 +377,7 @@ def make_clusters(self, b_list, h_list, t_list, layout_separator_map, ds): class LayoutEngineYolo(object): def __init__(self, model_path, device, - image_size: Union[int, tuple[int, int], None] = None, + image_size: Union[int, Tuple[int, int], None] = None, detection_threshold=0.2): self.yolo_net = YOLO(model_path).to(device) self.detection_threshold = detection_threshold diff --git a/pero_ocr/music/music_exporter.py b/pero_ocr/music/music_exporter.py index b68daaf..fcaeefc 100644 --- a/pero_ocr/music/music_exporter.py +++ b/pero_ocr/music/music_exporter.py @@ -3,6 +3,7 @@ import os import re import logging +from typing import List import music21 as music @@ -18,9 +19,9 @@ class MusicPageExporter: For CLI usage see user_scripts/music_exporter.py """ - def __init__(self, input_xml_path: str = '', input_transcription_files: list[str] = None, + def __init__(self, input_xml_path: str = '', input_transcription_files: List[str] = None, translator_path: str = None, output_folder: str = 'output_page', export_midi: bool = False, - export_musicxml: bool = False, categories: list = None, verbose: bool = False): + export_musicxml: bool = False, categories: List = None, verbose: bool = False): self.translator_path = translator_path if verbose: logging.basicConfig(level=logging.DEBUG, format='[%(levelname)-s] \t- %(message)s') @@ -103,7 +104,7 @@ def export_to_midi(self, score, parts, file_id: str = None): base = self.get_output_file_base(file_id) part.export_to_midi(base) - def regions_to_parts(self, regions: list[RegionLayout]) -> list[Part]: + def regions_to_parts(self, regions: List[RegionLayout]) -> List[Part]: """Takes a list of regions and splits them to parts.""" max_parts = max( [len(region.get_lines_of_category(self.categories)) for region in regions], @@ -123,7 +124,7 @@ def regions_to_parts(self, regions: list[RegionLayout]) -> list[Part]: class MusicLinesExporter: """Takes text files with transcriptions as individual lines and exports musicxml file for each one""" - def __init__(self, input_files: list[str] = None, output_folder: str = 'output_musicxml', + def __init__(self, input_files: List[str] = None, output_folder: str = 'output_musicxml', translator: Translator = None, verbose: bool = False): self.translator = translator self.output_folder = output_folder @@ -180,7 +181,7 @@ def prepare_output_folder(output_folder: str): os.makedirs(output_folder) @staticmethod - def get_input_files(input_files: list[str] = None): + def get_input_files(input_files: List[str] = None): existing_files = [] if not input_files: @@ -193,7 +194,7 @@ def get_input_files(input_files: list[str] = None): return existing_files @staticmethod - def read_file_lines(input_file: str) -> list[str]: + def read_file_lines(input_file: str) -> List[str]: with open(input_file, 'r', encoding='utf-8') as f: lines = f.read().splitlines() @@ -210,9 +211,9 @@ def __init__(self, translator: Translator = None): self.translator = translator self.repr_music21 = music.stream.Part([music.instrument.Piano()]) - self.labels: list[str] = [] - self.textlines: list[TextLineWrapper] = [] - self.measures: list[Measure] = [] # List of measures in internal representation, NOT music21 + self.labels: List[str] = [] + self.textlines: List[TextLineWrapper] = [] + self.measures: List[Measure] = [] # List of measures in internal representation, NOT music21 def add_textline(self, line: TextLine) -> None: labels = line.transcription @@ -247,7 +248,7 @@ def export_to_midi(self, file_base: str): class TextLineWrapper: """Class to wrap one TextLine for easier export etc.""" - def __init__(self, text_line: TextLine, measures: list[music.stream.Measure]): + def __init__(self, text_line: TextLine, measures: List[music.stream.Measure]): self.text_line = text_line self.repr_music21 = music.stream.Part([music.instrument.Piano()] + measures) @@ -259,7 +260,7 @@ def export_midi(self, file_base: str = 'out'): parsed_xml.write('mid', filename) -def parse_semantic_to_measures(labels: str) -> list[Measure]: +def parse_semantic_to_measures(labels: str) -> List[Measure]: """Convert line of semantic labels to list of measures. Args: @@ -290,7 +291,7 @@ def parse_semantic_to_measures(labels: str) -> list[Measure]: return measures -def encode_measures(measures: list, measure_id_start_from: int = 1) -> list[Measure]: +def encode_measures(measures: List, measure_id_start_from: int = 1) -> List[Measure]: """Get list of measures and encode them to music21 encoded measures.""" logging.debug('-------------------------------- -------------- --------------------------------') logging.debug('-------------------------------- START ENCODING --------------------------------') diff --git a/pero_ocr/music/music_structures.py b/pero_ocr/music/music_structures.py index cc1bd9f..2b1fb4c 100644 --- a/pero_ocr/music/music_structures.py +++ b/pero_ocr/music/music_structures.py @@ -10,7 +10,7 @@ import re from enum import Enum import logging -from typing import Optional +from typing import Optional, List import music21 as music from pero_ocr.music.music_symbols import Symbol, SymbolType, AlteredPitches, LENGTH_TO_SYMBOL @@ -174,15 +174,15 @@ def encode_to_music21_polyphonic(self) -> music.stream.Measure: return music.stream.Measure(zero_length_encoded + voices_repr) @staticmethod - def find_shortest_voices(voices: list, ignore: list = None) -> list[int]: + def find_shortest_voices(voices: List, ignore: List = None) -> List[int]: """Go through all voices and find the one with the current shortest duration. Args: - voices (list): list of voices. - ignore (list): indexes of voices to ignore. + voices (List): List of voices. + ignore (List): indexes of voices to ignore. Returns: - list: indexes of voices with the current shortest duration. + List: indexes of voices with the current shortest duration. """ if ignore is None: ignore = [] @@ -201,8 +201,8 @@ def find_shortest_voices(voices: list, ignore: list = None) -> list[int]: return shortest_voice_ids @staticmethod - def find_zero_length_symbol_groups(symbol_groups: list[SymbolGroup]) -> list[SymbolGroup]: - """Returns a list of zero-length symbol groups AT THE BEGGING OF THE MEASURE.""" + def find_zero_length_symbol_groups(symbol_groups: List[SymbolGroup]) -> List[SymbolGroup]: + """Returns a List of zero-length symbol groups AT THE BEGGING OF THE MEASURE.""" zero_length_symbol_groups = [] for symbol_group in symbol_groups: if symbol_group.type == SymbolGroupType.TUPLE or symbol_group.length > 0: @@ -211,15 +211,15 @@ def find_zero_length_symbol_groups(symbol_groups: list[SymbolGroup]) -> list[Sym return zero_length_symbol_groups @staticmethod - def pad_voices_to_n_shortest(voices: list[Voice], n: int = 1) -> list[int]: + def pad_voices_to_n_shortest(voices: List[Voice], n: int = 1) -> List[int]: """Pads voices (starting from the shortest) so there is n shortest voices with same length. Args: - voices (list): list of voices. + voices (List): List of voices. n (int): number of desired shortest voices. Returns: - list: list of voice IDS with the current shortest duration. + List: List of voice IDS with the current shortest duration. """ shortest_voice_ids = Measure.find_shortest_voices(voices) @@ -236,11 +236,11 @@ def pad_voices_to_n_shortest(voices: list[Voice], n: int = 1) -> list[int]: return shortest_voice_ids @staticmethod - def get_mono_start_symbol_groups(symbol_groups: list[SymbolGroup]) -> list[SymbolGroup]: - """Get a list of monophonic symbol groups AT THE BEGINNING OF THE MEASURE. + def get_mono_start_symbol_groups(symbol_groups: List[SymbolGroup]) -> List[SymbolGroup]: + """Get a List of monophonic symbol groups AT THE BEGINNING OF THE MEASURE. Returns: - list: list of monophonic symbol groups AT THE BEGINNING OF THE MEASURE + List: List of monophonic symbol groups AT THE BEGINNING OF THE MEASURE """ mono_start_symbol_groups = [] for symbol_group in symbol_groups: @@ -250,15 +250,15 @@ def get_mono_start_symbol_groups(symbol_groups: list[SymbolGroup]) -> list[Symbo return mono_start_symbol_groups @staticmethod - def create_mono_start(voices: list[Voice], mono_start_symbol_groups: list[SymbolGroup]) -> list[Voice]: + def create_mono_start(voices: List[Voice], mono_start_symbol_groups: List[SymbolGroup]) -> List[Voice]: """Create monophonic start of measure in the first voice and add padding to the others. Args: - voices (list[Voices]): list of voices - mono_start_symbol_groups: list of monophonic symbol groups AT THE BEGINNING OF MEASURE. + voices (List[Voices]): List of voices + mono_start_symbol_groups: List of monophonic symbol groups AT THE BEGINNING OF MEASURE. Returns: - list[Voice]: list of voices + List[Voice]: List of voices """ padding_length = 0 for symbol_group in mono_start_symbol_groups: @@ -281,7 +281,7 @@ class SymbolGroupType(Enum): class SymbolGroup: """Represents one label group in a measure. Consisting of 1 to n labels/symbols.""" - tuple_data: list = None # Tuple data consists of a list of symbol groups where symbols have same lengths. + tuple_data: List = None # Tuple data consists of a List of symbol groups where symbols have same lengths. length: float = None # Length of the symbol group in quarter notes. def __init__(self, labels: str): @@ -373,7 +373,7 @@ def encode_to_music21_monophonic(self): def create_tuple_data(self): """Create tuple data for the label group. - Tuple data consists of a list of symbol groups where symbols have same lengths. + Tuple data consists of a List of symbol groups where symbols have same lengths. """ # logging.debug(f'Creating tuple data for label group: {self.labels}') list_of_groups = [[self.symbols[0]]] @@ -433,7 +433,7 @@ def get_clef(self) -> Optional[music.clef.Clef]: class Voice: """Internal representation of voice (list of symbol groups symbolizing one musical line).""" length: float = 0.0 # Accumulated length of symbol groups (in quarter notes). - symbol_groups: list = [] + symbol_groups: List = [] repr = None def __init__(self): From 520e3aeaa07e718fbbf551a84fc0628ae13b005e Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 27 Jun 2024 10:26:03 +0200 Subject: [PATCH 66/76] Fix page_xml "custom" field export to export category only if not None. --- pero_ocr/core/layout.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index 675e898..ae61a79 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -84,11 +84,13 @@ def to_pagexml(self, region_element: ET.SubElement, fallback_id: int, validate_i text_line.set("index", f'{self.index:d}') else: text_line.set("index", f'{fallback_id:d}') + + custom = {} if self.heights is not None: - custom = { - "heights": list(np.round(self.heights, decimals=1)), - "category": self.category - } + custom['heights'] = list(np.round(self.heights, decimals=1)) + if self.category is not None: + custom['category'] = self.category + if len(custom) > 0: text_line.set("custom", json.dumps(custom)) coords = ET.SubElement(text_line, "Coords") From 9b414a0f4f1cb43d9326f2fed30d7640f587420a Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 27 Jun 2024 12:01:27 +0200 Subject: [PATCH 67/76] Set category filter fallback to `[]` for backward compatibility. Old XMLs on input don't have category => line.category = None, OCR (and others) have to be set to `[]` by default to process ALL PAGES. --- pero_ocr/core/layout.py | 2 +- pero_ocr/document_ocr/page_parser.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index ae61a79..d42681d 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -707,7 +707,7 @@ def to_pagexml_string(self, creator: str = 'Pero OCR', validate_id: bool = False page.set("imageWidth", str(self.page_size[1])) page.set("imageHeight", str(self.page_size[0])) - if self.reading_order is not None: + if self.reading_order is not None and self.reading_order != {}: self.sort_regions_by_reading_order() self.reading_order_to_pagexml(page) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index d8d81d3..088688a 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -73,7 +73,7 @@ def page_decoder_factory(config, device, config_path=''): decoder = decoding_itf.decoder_factory(config['DECODER'], ocr_chars, device, allow_no_decoder=False, config_path=config_path) confidence_threshold = config['DECODER'].getfloat('CONFIDENCE_THRESHOLD', fallback=math.inf) carry_h_over = config['DECODER'].getboolean('CARRY_H_OVER') - categories = config_get_list(config['DECODER'], key='CATEGORIES', fallback=['text']) + categories = config_get_list(config['DECODER'], key='CATEGORIES', fallback=[]) return PageDecoder(decoder, line_confidence_threshold=confidence_threshold, carry_h_over=carry_h_over, categories=categories) @@ -215,7 +215,7 @@ def __init__(self, config, device, config_path=''): self.adjust_heights = config.getboolean('ADJUST_HEIGHTS') self.multi_orientation = config.getboolean('MULTI_ORIENTATION') self.adjust_baselines = config.getboolean('ADJUST_BASELINES') - self.categories = config_get_list(config, key='CATEGORIES', fallback=['text']) + self.categories = config_get_list(config, key='CATEGORIES', fallback=[]) use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") @@ -406,7 +406,7 @@ def __init__(self, config, device, config_path): self.filter_incomplete_pages = config.getboolean('FILTER_INCOMPLETE_PAGES') self.filter_pages_with_short_lines = config.getboolean('FILTER_PAGES_WITH_SHORT_LINES') self.length_threshold = config.getint('LENGTH_THRESHOLD') - self.categories = config_get_list(config, key='CATEGORIES', fallback=['text']) + self.categories = config_get_list(config, key='CATEGORIES', fallback=[]) use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") @@ -516,7 +516,7 @@ def __init__(self, config, device, config_path=''): use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") - self.categories = config_get_list(config, key='CATEGORIES', fallback=['text']) + self.categories = config_get_list(config, key='CATEGORIES', fallback=[]) self.substitute_output = config.getboolean('SUBSTITUTE_OUTPUT', fallback=True) self.substitute_output_atomic = config.getboolean('SUBSTITUTE_OUTPUT_ATOMIC', fallback=True) self.update_transcription_by_confidence = config.getboolean( From a110f0e94a5027ee681194502b8bb22be95aa41f Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 27 Jun 2024 15:35:22 +0200 Subject: [PATCH 68/76] Library versions fixes. 1) Remove `ultralytics` and `music21` from dependencies for the whole projest. the user will have to install them when needed. 2) Import `ultralytics` only when needed, so it doesn't create import error for specific numpy versions. Ultralytics has this dependency right now: "numpy>=1.23.5,<2.0.0". See current at [github.com/ultralytics/ultralytics/blob/main/pyproject.toml](https://github.com/ultralytics/ultralytics/blob/69cfc8aa228dbf1267975f82fcae9a24665f23b9/pyproject.toml#L67) --- pero_ocr/core/layout.py | 3 ++- pero_ocr/document_ocr/page_parser.py | 3 +++ pero_ocr/layout_engines/cnn_layout_engine.py | 6 ++++-- pyproject.toml | 2 -- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index d42681d..7bb6f56 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -87,7 +87,8 @@ def to_pagexml(self, region_element: ET.SubElement, fallback_id: int, validate_i custom = {} if self.heights is not None: - custom['heights'] = list(np.round(self.heights, decimals=1)) + heights_out = [np.float64(x) for x in self.heights] + custom['heights'] = list(np.round(heights_out, decimals=1)) if self.category is not None: custom['category'] = self.category if len(custom) > 0: diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 088688a..1fc0752 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -312,6 +312,9 @@ def process_page(self, img, page_layout: PageLayout): class LayoutExtractorYolo(object): def __init__(self, config, device, config_path=''): + import ultralytics # check if ultralytics library is installed + # (ultralytics need different numpy version than some specific version installed on pero-ocr machines) + use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") self.categories = config_get_list(config, key='CATEGORIES', fallback=[]) diff --git a/pero_ocr/layout_engines/cnn_layout_engine.py b/pero_ocr/layout_engines/cnn_layout_engine.py index 5adc935..9455fc4 100644 --- a/pero_ocr/layout_engines/cnn_layout_engine.py +++ b/pero_ocr/layout_engines/cnn_layout_engine.py @@ -9,7 +9,6 @@ from scipy.sparse.csgraph import connected_components import skimage.draw import shapely.geometry as sg -from ultralytics import YOLO import torch from pero_ocr.layout_engines import layout_helpers as helpers @@ -379,6 +378,9 @@ class LayoutEngineYolo(object): def __init__(self, model_path, device, image_size: Union[int, Tuple[int, int], None] = None, detection_threshold=0.2): + from ultralytics import YOLO # import here, only if needed + # (ultralytics need different numpy version than some specific version installed on pero-ocr machines) + self.yolo_net = YOLO(model_path).to(device) self.detection_threshold = detection_threshold self.image_size = image_size # height or (height, width) @@ -398,7 +400,7 @@ def detect(self, image): verbose=False) if results is None: - raise Exception('LayoutEngineYolo returned None.') + raise Exception('Yolo inference returned None.') return results[0] diff --git a/pyproject.toml b/pyproject.toml index aac2655..f025360 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,8 +41,6 @@ dependencies = [ "pyamg", "imgaug", "arabic_reshaper", - "ultralytics", - "music21", ] From f5a7a5154871d4575ffdb54e88925ffa721cff96 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Thu, 11 Jul 2024 16:46:21 +0200 Subject: [PATCH 69/76] Add libraries back to pyproject.toml, so new machines install it right away. --- pero_ocr/document_ocr/page_parser.py | 8 ++++++-- pyproject.toml | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index 1fc0752..c637569 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -312,8 +312,12 @@ def process_page(self, img, page_layout: PageLayout): class LayoutExtractorYolo(object): def __init__(self, config, device, config_path=''): - import ultralytics # check if ultralytics library is installed - # (ultralytics need different numpy version than some specific version installed on pero-ocr machines) + try: + import ultralytics # check if ultralytics library is installed + # (ultralytics need different numpy version than some specific version installed on pero-ocr machines) + except ImportError: + raise ImportError("To use LayoutExtractorYolo, you need to install ultralytics library. " + "You can do it by running 'pip install ultralytics'.") use_cpu = config.getboolean('USE_CPU') self.device = device if not use_cpu else torch.device("cpu") diff --git a/pyproject.toml b/pyproject.toml index f025360..c0dd373 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,8 @@ dependencies = [ "pyamg", "imgaug", "arabic_reshaper", + "ultralytics", + "music21" ] From cc9b0aec1f8e73d01b5f6ff9a2afc825a3d8af80 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Mon, 5 Aug 2024 13:39:26 +0200 Subject: [PATCH 70/76] Fix bugs according to Pull request comment. In `smart_sorter.py`: - if less then to engines filtered, return original page_layout and not only the split one. In `music structures.py`: - change type of `lengths` to numpy array, fix min_length to take from numbers and not names. - ensure `encoded_group` is not None before appending it to the voice. full comment: [pero-ocr/pull/56/#issuecomment-2245202776](https://github.com/DCGM/pero-ocr/pull/56/#issuecomment-2245202776) --- pero_ocr/layout_engines/smart_sorter.py | 1 + pero_ocr/music/music_structures.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pero_ocr/layout_engines/smart_sorter.py b/pero_ocr/layout_engines/smart_sorter.py index 8ffcf45..2b671ad 100644 --- a/pero_ocr/layout_engines/smart_sorter.py +++ b/pero_ocr/layout_engines/smart_sorter.py @@ -284,6 +284,7 @@ def process_page(self, image, page_layout: PageLayout): regions = [] if len(page_layout.regions) < 2: + page_layout = helpers.merge_page_layouts(page_layout_ignore, page_layout) return page_layout rotation = SmartRegionSorter.get_rotation(max(*page_layout.regions, key=lambda reg: len(reg.lines)).lines) diff --git a/pero_ocr/music/music_structures.py b/pero_ocr/music/music_structures.py index 2b1fb4c..3278fe8 100644 --- a/pero_ocr/music/music_structures.py +++ b/pero_ocr/music/music_structures.py @@ -12,6 +12,7 @@ import logging from typing import Optional, List +import numpy as np import music21 as music from pero_ocr.music.music_symbols import Symbol, SymbolType, AlteredPitches, LENGTH_TO_SYMBOL @@ -473,7 +474,9 @@ def encode_to_music21_monophonic(self) -> music.stream.Voice: self.repr = music.stream.Voice() for group in self.symbol_groups: - self.repr.append(group.encode_to_music21_monophonic()) + encoded_group = group.encode_to_music21_monophonic() + if encoded_group is not None: + self.repr.append(encoded_group) return self.repr def add_padding(self, padding_length: float) -> None: @@ -482,8 +485,8 @@ def add_padding(self, padding_length: float) -> None: Args: padding_length (float): desired length of the padding in quarter notes. """ - lengths = list(LENGTH_TO_SYMBOL.values()) - min_length = min(LENGTH_TO_SYMBOL.keys()) + lengths = np.array(list(LENGTH_TO_SYMBOL.keys())) + min_length = lengths.min() while padding_length > 0: if padding_length in LENGTH_TO_SYMBOL: From b60196f4976a77ec1c7254fea82d1c7353d62c31 Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Mon, 5 Aug 2024 16:53:00 +0200 Subject: [PATCH 71/76] Add regions to splitting by category. If `region.category` set, move whole region to positive or negative (ignore categories of lines inside the region) --- pero_ocr/layout_engines/layout_helpers.py | 32 ++++++++++++++--------- pero_ocr/music/music_exporter.py | 2 +- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/pero_ocr/layout_engines/layout_helpers.py b/pero_ocr/layout_engines/layout_helpers.py index 4935791..f3a274c 100644 --- a/pero_ocr/layout_engines/layout_helpers.py +++ b/pero_ocr/layout_engines/layout_helpers.py @@ -440,20 +440,26 @@ def split_page_layout_by_categories(page_layout: PageLayout, categories: list) - regions = page_layout.regions page_layout.regions = [] - page_layout_text = page_layout - page_layout_no_text = deepcopy(page_layout) + page_layout_positive = page_layout + page_layout_negative = deepcopy(page_layout) for region in regions: - for line in region.lines: - if line.category in categories: - page_layout_text = insert_line_to_page_layout(page_layout_text, region, line) + if region.category is not None or len(region.lines) == 0: + if region.category in categories: + page_layout_positive.regions.append(region) else: - page_layout_no_text = insert_line_to_page_layout(page_layout_no_text, region, line) + page_layout_negative.regions.append(region) + else: + for line in region.lines: + if line.category in categories: + page_layout_positive = insert_line_to_page_layout(page_layout_positive, region, line) + else: + page_layout_negative = insert_line_to_page_layout(page_layout_negative, region, line) - return page_layout_text, page_layout_no_text + return page_layout_positive, page_layout_negative -def merge_page_layouts(page_layout_text: PageLayout, page_layout_no_text: PageLayout) -> PageLayout: +def merge_page_layouts(page_layout_positive: PageLayout, page_layout_negative: PageLayout) -> PageLayout: """Merge two page_layouts into one by line. If same region ID, create new ID. Example: @@ -465,21 +471,21 @@ def merge_page_layouts(page_layout_text: PageLayout, page_layout_no_text: PageLa RegionLayout(id='r001', lines=[TextLine(id='r001-l001', category='text')]), RegionLayout(id='r002', lines=[TextLine(id='r002-l002', category='logo')])]) """ - used_region_ids = set(region.id for region in page_layout_text.regions) + used_region_ids = set(region.id for region in page_layout_positive.regions) id_offset = 0 - for region in page_layout_no_text.regions: + for region in page_layout_negative.regions: if region.id not in used_region_ids: used_region_ids.add(region.id) - page_layout_text.regions.append(region) + page_layout_positive.regions.append(region) else: while 'r{:03d}'.format(id_offset) in used_region_ids: id_offset += 1 region.replace_id('r{:03d}'.format(id_offset)) used_region_ids.add(region.id) - page_layout_text.regions.append(region) + page_layout_positive.regions.append(region) - return page_layout_text + return page_layout_positive def insert_line_to_page_layout(page_layout: PageLayout, region: RegionLayout, line: TextLine) -> PageLayout: diff --git a/pero_ocr/music/music_exporter.py b/pero_ocr/music/music_exporter.py index fcaeefc..e975c64 100644 --- a/pero_ocr/music/music_exporter.py +++ b/pero_ocr/music/music_exporter.py @@ -210,7 +210,7 @@ class Part: def __init__(self, translator: Translator = None): self.translator = translator - self.repr_music21 = music.stream.Part([music.instrument.Piano()]) + self.repr_music21 = music.stream.Part([music.instrument.Piano()]) # Default instrument is piano self.labels: List[str] = [] self.textlines: List[TextLineWrapper] = [] self.measures: List[Measure] = [] # List of measures in internal representation, NOT music21 From 4d2ddaa3d7ce0e5345d1ceb65883fe1f47053d1e Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Tue, 6 Aug 2024 10:49:36 +0200 Subject: [PATCH 72/76] Add better None check. --- pero_ocr/music/music_structures.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pero_ocr/music/music_structures.py b/pero_ocr/music/music_structures.py index 3278fe8..0146a20 100644 --- a/pero_ocr/music/music_structures.py +++ b/pero_ocr/music/music_structures.py @@ -124,7 +124,9 @@ def encode_to_music21(self) -> music.stream.Measure: self.repr = music.stream.Measure() if not self._is_polyphonic: for symbol_group in self.symbol_groups: - self.repr.append(symbol_group.encode_to_music21_monophonic()) + encoded_group = symbol_group.encode_to_music21_monophonic() + if encoded_group is not None: + self.repr.append(encoded_group) else: self.repr = self.encode_to_music21_polyphonic() @@ -171,7 +173,14 @@ def encode_to_music21_polyphonic(self) -> music.stream.Measure: logging.debug(f'voice ({voice_id}) len: {voice.length}') zero_length_encoded = [group.encode_to_music21_monophonic() for group in zero_length_symbol_groups] + zero_length_encoded = [group for group in zero_length_encoded if group is not None] voices_repr = [voice.encode_to_music21_monophonic() for voice in voices] + voices_repr = [voice for voice in voices_repr if voice is not None] + + if len(voices_repr) == 0: + logging.warning('No voices in the measure, returning empty measure.') + return music.stream.Measure(zero_length_encoded) + return music.stream.Measure(zero_length_encoded + voices_repr) @staticmethod @@ -350,7 +359,7 @@ def set_key(self, altered_pitches: AlteredPitches): for symbol in self.symbols: symbol.set_key(altered_pitches) - def encode_to_music21_monophonic(self): + def encode_to_music21_monophonic(self) -> Optional[music.stream.Stream]: """Encodes the label group to music21 format. Returns: @@ -460,7 +469,7 @@ def add_symbol_group(self, symbol_group: SymbolGroup) -> None: self.length += symbol_group.length self.repr = None - def encode_to_music21_monophonic(self) -> music.stream.Voice: + def encode_to_music21_monophonic(self) -> Optional[music.stream.Voice]: """Encodes the voice to music21 format. Returns: From 7c4251e4b99e8481bc239c62cfba5bd56ec2d1ec Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Tue, 27 Aug 2024 20:14:50 +0200 Subject: [PATCH 73/76] Disable exporting midi lines if no notes on the line. Export multirest as a simple default 'whole' rest. --- pero_ocr/music/music_exporter.py | 4 ++++ pero_ocr/music/music_symbols.py | 16 ++++------------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/pero_ocr/music/music_exporter.py b/pero_ocr/music/music_exporter.py index e975c64..0b4438c 100644 --- a/pero_ocr/music/music_exporter.py +++ b/pero_ocr/music/music_exporter.py @@ -253,6 +253,10 @@ def __init__(self, text_line: TextLine, measures: List[music.stream.Measure]): self.repr_music21 = music.stream.Part([music.instrument.Piano()] + measures) def export_midi(self, file_base: str = 'out'): + # do not export line, if it has no notes + if not any([note for note in self.repr_music21.flatten().notes]): + return + filename = f'{file_base}_{self.text_line.id}.mid' xml = music21_to_musicxml(self.repr_music21) diff --git a/pero_ocr/music/music_symbols.py b/pero_ocr/music/music_symbols.py index d922a53..84118d5 100644 --- a/pero_ocr/music/music_symbols.py +++ b/pero_ocr/music/music_symbols.py @@ -124,7 +124,7 @@ def keysignature_to_symbol(keysignature) -> music.key.Key: return music.key.Key(keysignature) @staticmethod - def multirest_to_symbol(multirest: str) -> MultiRest: + def multirest_to_symbol(multirest: str) -> music.note.Rest: """Converts one multi rest label to internal MultiRest format. Args: @@ -133,17 +133,9 @@ def multirest_to_symbol(multirest: str) -> MultiRest: Returns: music.note.Rest: one rest in music21 format """ - def return_default_multirest() -> MultiRest: - logging.info(f'Unknown multi rest label: {multirest}, returning default Multirest.') - return MultiRest() - - if not multirest: - return_default_multirest() - - try: - return MultiRest(int(multirest)) - except ValueError: - return return_default_multirest() + rest = music.note.Rest() + rest.duration = label_to_length('whole') # default duration, because multirest is not implemented in music21 + return rest @staticmethod def note_to_symbol(note, gracenote: bool = False) -> Note: From fa1a897eb0f0c4dfebc878d74dce88949b0c673e Mon Sep 17 00:00:00 2001 From: vlachvojta Date: Wed, 16 Oct 2024 15:04:05 +0200 Subject: [PATCH 74/76] Simplify splitting page layouts to allow backwards (only look at region category, None = 'text') --- pero_ocr/layout_engines/layout_helpers.py | 70 +++++++++-------------- 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/pero_ocr/layout_engines/layout_helpers.py b/pero_ocr/layout_engines/layout_helpers.py index f3a274c..27f2ed7 100644 --- a/pero_ocr/layout_engines/layout_helpers.py +++ b/pero_ocr/layout_engines/layout_helpers.py @@ -414,22 +414,27 @@ def adjust_baselines_to_intensity(baselines, img, tolerance=5): def split_page_layout(page_layout: PageLayout) -> Tuple[PageLayout, PageLayout]: - """Split page layout to text and non-text lines.""" + """Split page layout to text and non-text regions.""" return split_page_layout_by_categories(page_layout, ['text']) def split_page_layout_by_categories(page_layout: PageLayout, categories: list) -> Tuple[PageLayout, PageLayout]: - """Split page_layout into two: one with textlines of given categories, the other with textlines of other categories. + """Split page_layout into two by region category. Return one page_layout with regions of given categories and one with + regions of other categories. No region category is treated as 'text' for backwards compatibility. + If no categories, return original page_layout and empty page_layout. + ! TextLine categories are ignored here ! Example: split_page_layout_by_categories(page_layout, ['text']) IN: PageLayout(regions=[ - RegionLayout(id='r001', lines=[TextLine(id='r001-l001', category='text'), - TextLine(id='r001-l002', category='logo')])]) + RegionLayout(id='r001', category='text', lines=[TextLine(id='r001-l001', category='text'), + TextLine(id='r001-l002', category='logo')]), + RegionLayout(id='r002', category='image', lines=[TextLine(id='r002-l001', category='text')])]) OUT: PageLayout(regions=[ - RegionLayout(id='r001', lines=[TextLine(id='r001-l001', category='text')])]), - PageLayout(regions=[ - RegionLayout(id='r001', lines=[TextLine(id='r001-l002', category='logo')])]) + RegionLayout(id='r001', category='text', lines=[TextLine(id='r001-l001', category='text'), + TextLine(id='r001-l002', category='logo')])]) + PageLayout(regions=[ + RegionLayout(id='r002', category='image', lines=[TextLine(id='r002-l001', category='text')])]) """ if not categories: # if no categories, return original page_layout and empty page_layout @@ -444,23 +449,16 @@ def split_page_layout_by_categories(page_layout: PageLayout, categories: list) - page_layout_negative = deepcopy(page_layout) for region in regions: - if region.category is not None or len(region.lines) == 0: - if region.category in categories: - page_layout_positive.regions.append(region) - else: - page_layout_negative.regions.append(region) + region_category = region.category if region.category is not None else 'text' + if region_category in categories: + page_layout_positive.regions.append(region) else: - for line in region.lines: - if line.category in categories: - page_layout_positive = insert_line_to_page_layout(page_layout_positive, region, line) - else: - page_layout_negative = insert_line_to_page_layout(page_layout_negative, region, line) - + page_layout_negative.regions.append(region) return page_layout_positive, page_layout_negative def merge_page_layouts(page_layout_positive: PageLayout, page_layout_negative: PageLayout) -> PageLayout: - """Merge two page_layouts into one by line. If same region ID, create new ID. + """Merge two page_layouts into one by regions. If same region ID, create new ID (rename line IDs also). Example: IN: PageLayout(regions=[ @@ -469,39 +467,25 @@ def merge_page_layouts(page_layout_positive: PageLayout, page_layout_negative: P RegionLayout(id='r001', lines=[TextLine(id='r001-l002', category='logo')])]) OUT: PageLayout(regions=[ RegionLayout(id='r001', lines=[TextLine(id='r001-l001', category='text')]), - RegionLayout(id='r002', lines=[TextLine(id='r002-l002', category='logo')])]) + RegionLayout(id='r001-1', lines=[TextLine(id='r001-1-l002', category='logo')])]) """ used_region_ids = set(region.id for region in page_layout_positive.regions) - id_offset = 0 for region in page_layout_negative.regions: if region.id not in used_region_ids: used_region_ids.add(region.id) page_layout_positive.regions.append(region) else: - while 'r{:03d}'.format(id_offset) in used_region_ids: + new_region_id = region.id + id_offset = 1 + + # find new unique region ID by adding offset + while new_region_id in used_region_ids: + new_region_id = region.id + '-' + str(id_offset) id_offset += 1 - region.replace_id('r{:03d}'.format(id_offset)) - used_region_ids.add(region.id) + + region.replace_id(new_region_id) + used_region_ids.add(new_region_id) page_layout_positive.regions.append(region) return page_layout_positive - - -def insert_line_to_page_layout(page_layout: PageLayout, region: RegionLayout, line: TextLine) -> PageLayout: - """Insert line to page layout given region of origin. Find if region already exists by ID.""" - existing_region = find_region_by_id(page_layout, region.id) - - if existing_region is not None: - existing_region.lines.append(line) - else: - region.lines = [line] - page_layout.regions.append(region) - return page_layout - - -def find_region_by_id(page_layout: PageLayout, region_id: str) -> Optional[RegionLayout]: - for region in page_layout.regions: - if region.id == region_id: - return region - return None From f5f2f4239642f8d12c1a8bb9690502aede11e660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ki=C5=A1=C5=A1?= Date: Fri, 25 Oct 2024 17:10:28 +0200 Subject: [PATCH 75/76] Add IndexError to catch expression when calculating transcription confidence -- in case when there are no logits (i.e. logits.shape[0] == 0) the confidence cannot be calculated. --- pero_ocr/document_ocr/page_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pero_ocr/document_ocr/page_parser.py b/pero_ocr/document_ocr/page_parser.py index c637569..b4327ba 100644 --- a/pero_ocr/document_ocr/page_parser.py +++ b/pero_ocr/document_ocr/page_parser.py @@ -587,7 +587,7 @@ def get_line_confidence(self, line): log_probs = line.get_full_logprobs()[line.logit_coords[0]:line.logit_coords[1]] confidences = get_line_confidence(line, log_probs=log_probs) return np.quantile(confidences, .50) - except ValueError as e: + except (ValueError, IndexError) as e: logger.warning(f'PageOCR is unable to get confidence of line {line.id} due to exception: {e}.') return self.default_confidence return self.default_confidence From 747e491ebdb3a68e571ec252eb6f0cb03c2ab1d4 Mon Sep 17 00:00:00 2001 From: Michal Hradis Date: Tue, 5 Nov 2024 11:15:54 +0100 Subject: [PATCH 76/76] Update layout.py --- pero_ocr/core/layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pero_ocr/core/layout.py b/pero_ocr/core/layout.py index f550b1b..9817f14 100644 --- a/pero_ocr/core/layout.py +++ b/pero_ocr/core/layout.py @@ -242,7 +242,7 @@ def to_altoxml_text(self, text_line, arabic_helper, except (ValueError, IndexError, TypeError) as e: logger.warning(f'Error: Alto export, unable to align line {self.id} due to exception: {e}.') - if logits is not None: + if logits is not None and logits.shape[0] > 0: max_val = np.max(logits, axis=1) logits = logits - max_val[:, np.newaxis] probs = np.exp(logits) @@ -1147,4 +1147,4 @@ def create_ocr_processing_element(id: str = "IdOcr", def normalize_text(text: str) -> str: """Normalize text to ASCII characters. (e.g. Obrázek -> Obrazek)""" - return unicodedata.normalize('NFD', text).encode('ascii', 'ignore').decode('ascii') \ No newline at end of file + return unicodedata.normalize('NFD', text).encode('ascii', 'ignore').decode('ascii')