├── .gitignore ├── .travis.yml ├── LICENSE ├── ModelGenerator-pytorch ├── .gitignore ├── TrainModel.py ├── VisualizeFilters.py ├── datasets │ ├── AdditionalDataset.py │ ├── DatasetSplitter.py │ ├── ImageResizer.py │ ├── MuscimaDataset.py │ ├── OnlineDataset.py │ ├── PascalVocDataset.py │ ├── ScoreClassificationDataset.py │ └── __init__.py ├── models │ ├── MobileNetV2.py │ ├── SimpleNetwork.py │ └── __init__.py ├── pytest.ini ├── requirements.txt └── tests │ ├── OnlineDatasetTest.py │ ├── ScoreClassificationDatasetTest.py │ └── __init__.py ├── ModelGenerator-tensorflow ├── .gitignore ├── EvaluationPreprocessor.py ├── ExportModelToTensorflow.py ├── TestModel.py ├── TrainModel.py ├── TrainingHistoryPlotter.py ├── datasets │ ├── AdditionalDataset.py │ ├── Dataset.py │ ├── DatasetSplitter.py │ ├── MuscimaDataset.py │ ├── PascalVocDataset.py │ └── __init__.py ├── models │ ├── ConfigurationFactory.py │ ├── MobileNetV2Configuration.py │ ├── SimpleConfiguration.py │ ├── TrainingConfiguration.py │ ├── VggConfiguration.py │ ├── XceptionConfiguration.py │ └── __init__.py ├── requirements.txt └── tests │ ├── __init__.py │ └── tensorflow_test.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/workspace.xml 8 | .idea/tasks.xml 9 | 10 | # Sensitive or high-churn files: 11 | .idea/dataSources/ 12 | .idea/dataSources.ids 13 | .idea/dataSources.xml 14 | .idea/dataSources.local.xml 15 | .idea/sqlDataSources.xml 16 | .idea/dynamic.xml 17 | .idea/uiDesigner.xml 18 | 19 | # Gradle: 20 | .idea/gradle.xml 21 | .idea/libraries 22 | 23 | # Mongo Explorer plugin: 24 | .idea/mongoSettings.xml 25 | 26 | ## File-based project format: 27 | *.iws 28 | 29 | ## Plugin-specific files: 30 | 31 | # IntelliJ 32 | /out/ 33 | 34 | # mpeltonen/sbt-idea plugin 35 | .idea_modules/ 36 | 37 | # JIRA plugin 38 | atlassian-ide-plugin.xml 39 | 40 | # Crashlytics plugin (for Android Studio and IntelliJ) 41 | com_crashlytics_export_strings.xml 42 | crashlytics.properties 43 | crashlytics-build.properties 44 | fabric.properties 45 | ### Windows template 46 | # Windows image file caches 47 | Thumbs.db 48 | ehthumbs.db 49 | 50 | # Folder config file 51 | Desktop.ini 52 | 53 | # Recycle Bin used on file shares 54 | $RECYCLE.BIN/ 55 | 56 | # Windows Installer files 57 | *.cab 58 | *.msi 59 | *.msm 60 | *.msp 61 | 62 | # Windows shortcuts 63 | *.lnk 64 | ### Android template 65 | # Built application files 66 | *.apk 67 | *.ap_ 68 | 69 | # Files for the ART/Dalvik VM 70 | *.dex 71 | 72 | # Java class files 73 | *.class 74 | 75 | # Generated files 76 | bin/ 77 | gen/ 78 | out/ 79 | 80 | # Gradle files 81 | .gradle/ 82 | build/ 83 | 84 | # Local configuration file (sdk path, etc) 85 | local.properties 86 | 87 | # Proguard folder generated by Eclipse 88 | proguard/ 89 | 90 | # Log Files 91 | *.log 92 | 93 | # Android Studio Navigation editor temp files 94 | .navigation/ 95 | 96 | # Android Studio captures folder 97 | captures/ 98 | 99 | # Intellij 100 | *.iml 101 | 102 | # Keystore files 103 | *.jks 104 | 105 | # External native build folder generated in Android Studio 2.2 and later 106 | .externalNativeBuild 107 | ### Python template 108 | # Byte-compiled / optimized / DLL files 109 | __pycache__/ 110 | *.py[cod] 111 | *$py.class 112 | 113 | # C extensions 114 | *.so 115 | 116 | # Distribution / packaging 117 | .Python 118 | env/ 119 | develop-eggs/ 120 | dist/ 121 | downloads/ 122 | eggs/ 123 | .eggs/ 124 | lib/ 125 | lib64/ 126 | parts/ 127 | sdist/ 128 | var/ 129 | *.egg-info/ 130 | .installed.cfg 131 | *.egg 132 | 133 | # PyInstaller 134 | # Usually these files are written by a python script from a template 135 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 136 | *.manifest 137 | *.spec 138 | 139 | # Installer logs 140 | pip-log.txt 141 | pip-delete-this-directory.txt 142 | 143 | # Unit test / coverage reports 144 | htmlcov/ 145 | .tox/ 146 | .coverage 147 | .coverage.* 148 | .cache 149 | nosetests.xml 150 | coverage.xml 151 | *,cover 152 | .hypothesis/ 153 | 154 | # Translations 155 | *.mo 156 | *.pot 157 | 158 | # Django stuff: 159 | local_settings.py 160 | 161 | # Flask stuff: 162 | instance/ 163 | .webassets-cache 164 | 165 | # Scrapy stuff: 166 | .scrapy 167 | 168 | # Sphinx documentation 169 | docs/_build/ 170 | 171 | # PyBuilder 172 | target/ 173 | 174 | # Jupyter Notebook 175 | .ipynb_checkpoints 176 | 177 | # pyenv 178 | .python-version 179 | 180 | # celery beat schedule file 181 | celerybeat-schedule 182 | 183 | # dotenv 184 | .env 185 | 186 | # virtualenv 187 | .venv/ 188 | venv/ 189 | ENV/ 190 | 191 | # Spyder project settings 192 | .spyderproject 193 | 194 | # Rope project settings 195 | .ropeproject 196 | ### Linux template 197 | *~ 198 | 199 | # temporary files which can be created if a process still has a handle open of a deleted file 200 | .fuse_hidden* 201 | 202 | # KDE directory preferences 203 | .directory 204 | 205 | # Linux trash folder which might appear on any partition or disk 206 | .Trash-* 207 | 208 | # .nfs files are created when an open file is removed but is still being accessed 209 | .nfs* 210 | 211 | *.tar 212 | *.zip 213 | /ModelGenerator-pytorch/checkpoints/ 214 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: python 4 | 5 | python: 6 | - "3.6" 7 | 8 | install: 9 | # code below is taken from http://conda.pydata.org/docs/travis.html 10 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh 11 | - bash miniconda.sh -b -p $HOME/miniconda 12 | - export PATH="$HOME/miniconda/bin:$PATH" 13 | - hash -r 14 | - conda config --set always_yes yes --set changeps1 no 15 | - conda update -q conda 16 | # Useful for debugging any issues with conda 17 | - conda info -a 18 | 19 | - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION numpy scipy matplotlib pandas pytest h5py Pillow 20 | - source activate test-environment 21 | 22 | # Upgrading libgcc to prevent an error when running the tests (see https://github.com/tensorflow/tensorflow/issues/5017#issuecomment-295566267) 23 | - conda install libgcc 24 | - conda update -f libstdcxx-ng 25 | - conda install -c anaconda tensorflow 26 | 27 | # Install additional requirements 28 | - pip install https://download.pytorch.org/whl/cpu/torch-1.0.0-cp36-cp36m-linux_x86_64.whl 29 | - pip install -r ModelGenerator-tensorflow/requirements.txt 30 | - pip install -r ModelGenerator-pytorch/requirements.txt 31 | 32 | script: 33 | - pytest --cov=./ 34 | - cd ModelGenerator-pytorch 35 | - pytest --cov=./ -s 36 | 37 | after_success: 38 | - codecov -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alexander Pacha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ModelGenerator-pytorch/.gitignore: -------------------------------------------------------------------------------- 1 | /data/ 2 | /.idea/ 3 | -------------------------------------------------------------------------------- /ModelGenerator-pytorch/TrainModel.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import datetime 3 | import os 4 | import shutil 5 | from typing import Tuple 6 | 7 | import torch 8 | from time import time 9 | 10 | from ignite.engine import create_supervised_trainer, create_supervised_evaluator, Events, Engine 11 | from ignite.handlers import ModelCheckpoint, Timer, EarlyStopping 12 | from ignite.metrics import Accuracy, Loss 13 | from torch.nn import CrossEntropyLoss, Module 14 | from torch.optim import SGD, Adadelta 15 | from torch.optim.lr_scheduler import ReduceLROnPlateau 16 | from torch.utils.data import DataLoader 17 | from torchsummary import summary 18 | from torchvision.datasets import ImageFolder 19 | from torchvision.models import VGG, vgg11_bn, SqueezeNet 20 | from torchvision.transforms import transforms 21 | from tqdm import tqdm 22 | 23 | from datasets.AdditionalDataset import AdditionalDataset 24 | from datasets.DatasetSplitter import DatasetSplitter 25 | from datasets.MuscimaDataset import MuscimaDataset 26 | from datasets.PascalVocDataset import PascalVocDataset 27 | from models.MobileNetV2 import MobileNetV2 28 | from models.SimpleNetwork import SimpleNetwork 29 | 30 | 31 | def delete_dataset_directory(dataset_directory): 32 | print("Deleting dataset directory and creating it anew") 33 | 34 | if os.path.exists(dataset_directory): 35 | shutil.rmtree(dataset_directory) 36 | 37 | 38 | def download_datasets(dataset_directory): 39 | pascal_voc_dataset = PascalVocDataset(dataset_directory) 40 | pascal_voc_dataset.download_and_extract_dataset() 41 | muscima_dataset = MuscimaDataset(dataset_directory) 42 | muscima_dataset.download_and_extract_dataset() 43 | additional_dataset = AdditionalDataset(dataset_directory) 44 | additional_dataset.download_and_extract_dataset() 45 | 46 | 47 | def print_model_architecture_and_parameters(network): 48 | print(network) 49 | summary(network, (3, 224, 224)) 50 | 51 | 52 | def get_dataset_loaders(dataset_directory, minibatch_size) -> Tuple[DataLoader, DataLoader, DataLoader]: 53 | data_transform = transforms.Compose([ 54 | transforms.RandomRotation(10), 55 | transforms.Resize((224, 224)), 56 | transforms.RandomHorizontalFlip(), 57 | transforms.ToTensor() 58 | ]) 59 | 60 | number_of_workers = 8 61 | training_dataset = ImageFolder(root=os.path.join(dataset_directory, "training"), transform=data_transform) 62 | training_dataset_loader = DataLoader(training_dataset, batch_size=minibatch_size, shuffle=True, 63 | num_workers=number_of_workers) 64 | validation_dataset = ImageFolder(root=os.path.join(dataset_directory, "validation"), transform=data_transform) 65 | validation_dataset_loader = DataLoader(validation_dataset, batch_size=minibatch_size, shuffle=False, 66 | num_workers=number_of_workers) 67 | testing_dataset = ImageFolder(root=os.path.join(dataset_directory, "test"), transform=data_transform) 68 | testing_dataset_loader = DataLoader(testing_dataset, batch_size=minibatch_size, shuffle=False, 69 | num_workers=number_of_workers) 70 | 71 | return training_dataset_loader, validation_dataset_loader, testing_dataset_loader 72 | 73 | 74 | def get_model_by_name(model_name) -> Module: 75 | if model_name == "vgg": 76 | return VGG(vgg11_bn().features, num_classes=2) 77 | if model_name == "mobilenetv2": 78 | return MobileNetV2(num_classes=2) 79 | if model_name == "simple": 80 | return SimpleNetwork() 81 | if model_name == "squeezenet": 82 | return SqueezeNet(version=1.1,num_classes=2) 83 | 84 | raise Exception(f"Invalid model name: {model_name}.") 85 | 86 | 87 | def train_model(dataset_directory: str, 88 | model_name: str, 89 | delete_and_recreate_dataset_directory: bool, 90 | minibatch_size=32): 91 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 92 | 93 | print("Downloading and extracting datasets...") 94 | 95 | if delete_and_recreate_dataset_directory: 96 | delete_dataset_directory(dataset_directory) 97 | download_datasets(dataset_directory) 98 | dataset_splitter = DatasetSplitter(dataset_directory, dataset_directory) 99 | dataset_splitter.split_images_into_training_validation_and_test_set() 100 | 101 | print("Training on dataset...") 102 | 103 | model = get_model_by_name(model_name) 104 | model.to(device) 105 | print_model_architecture_and_parameters(model) 106 | training_dataset_loader, validation_dataset_loader, testing_dataset_loader = get_dataset_loaders(dataset_directory, 107 | minibatch_size) 108 | optimizer = Adadelta(model.parameters()) 109 | learning_rate_scheduler = ReduceLROnPlateau(optimizer, mode='max', patience=8, verbose=True) 110 | 111 | trainer = create_supervised_trainer(model, optimizer, CrossEntropyLoss(), device=device) 112 | validation_evaluator = create_supervised_evaluator(model, 113 | metrics={'accuracy': Accuracy(), 114 | 'cross-entropy': Loss(CrossEntropyLoss())}, 115 | device=device) 116 | 117 | iteration_between_progress_bar_updates = 1 118 | progress_description_template = "Training - loss: {:.2f}" 119 | progress_bar = tqdm( 120 | initial=0, leave=False, total=len(training_dataset_loader), 121 | desc=progress_description_template.format(0) 122 | ) 123 | 124 | @trainer.on(Events.ITERATION_COMPLETED) 125 | def log_training_loss(trainer: Engine): 126 | iter = (trainer.state.iteration - 1) % len(training_dataset_loader) + 1 127 | if iter % iteration_between_progress_bar_updates == 0: 128 | progress_bar.desc = progress_description_template.format(trainer.state.output) 129 | progress_bar.update(iteration_between_progress_bar_updates) 130 | 131 | @trainer.on(Events.EPOCH_COMPLETED) 132 | def log_validation_results(trainer: Engine): 133 | progress_bar.refresh() 134 | validation_evaluator.run(validation_dataset_loader) 135 | metrics = validation_evaluator.state.metrics 136 | learning_rate_scheduler.step(metrics['accuracy']) 137 | print(f"\nValidation - Epoch: {trainer.state.epoch} - " 138 | f"Avg accuracy: {metrics['accuracy']:.2f} - " 139 | f"Avg loss: {metrics['cross-entropy']:.2f}") 140 | progress_bar.n = progress_bar.last_print_n = 0 141 | 142 | def score_function(evaluator: Engine): 143 | validation_accuracy = evaluator.state.metrics['accuracy'] 144 | return validation_accuracy 145 | 146 | checkpoint_directory = "checkpoints" 147 | checkpoint_handler = ModelCheckpoint(checkpoint_directory, model_name, score_function=score_function) 148 | validation_evaluator.add_event_handler(Events.COMPLETED, checkpoint_handler, {'mymodel': model}) 149 | early_stopping_handler = EarlyStopping(patience=10, score_function=score_function, trainer=trainer) 150 | validation_evaluator.add_event_handler(Events.COMPLETED, early_stopping_handler) 151 | 152 | trainer.run(training_dataset_loader, max_epochs=50) 153 | progress_bar.close() 154 | 155 | print('Finished Training') 156 | 157 | print("Loading best model from check-point and testing...") 158 | best_model_name = os.listdir(checkpoint_directory)[0] 159 | best_model = torch.load(os.path.join(checkpoint_directory, best_model_name)) # type: Module 160 | testing_evaluator = create_supervised_evaluator(best_model, metrics={'accuracy': Accuracy()}, device=device) 161 | testing_evaluator.run(testing_dataset_loader) 162 | metrics = testing_evaluator.state.metrics 163 | print(f"\nTesting - Avg Accuracy {metrics['accuracy']:.2f}") 164 | 165 | 166 | if __name__ == "__main__": 167 | parser = argparse.ArgumentParser() 168 | parser.register("type", "bool", lambda v: v.lower() == "true") 169 | parser.add_argument("--dataset_directory", type=str, default="data", 170 | help="The directory, that is used for storing the images during training") 171 | parser.add_argument("--model_name", type=str, default="squeezenet", 172 | help="The model used for training the network. " 173 | "Currently allowed values are \'simple\', \'vgg\', \'squeezenet\', \'mobilenetv2\'") 174 | parser.add_argument("--delete_and_recreate_dataset_directory", dest="delete_and_recreate_dataset_directory", 175 | action="store_true", 176 | help="Whether to delete and recreate the dataset-directory (by downloading the appropriate " 177 | "files from the internet) or simply use whatever data currently is inside of that " 178 | "directory") 179 | parser.set_defaults(delete_and_recreate_dataset_directory=False) 180 | 181 | flags, unparsed = parser.parse_known_args() 182 | 183 | train_model(flags.dataset_directory, 184 | flags.model_name, 185 | flags.delete_and_recreate_dataset_directory) 186 | -------------------------------------------------------------------------------- /ModelGenerator-pytorch/VisualizeFilters.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import torch 4 | from torch.nn import Module, Conv2d 5 | import matplotlib.pyplot as plt 6 | 7 | 8 | def visualize_filters(): 9 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 10 | 11 | print("Loading best model from check-point and testing...") 12 | checkpoint_directory = "checkpoints" 13 | best_model_name = os.listdir(checkpoint_directory)[0] 14 | best_model = torch.load(os.path.join(checkpoint_directory, best_model_name)) # type: Module 15 | best_model.cpu() 16 | 17 | model_layers = [i for i in best_model.children()] 18 | for model_layer in model_layers: 19 | if isinstance(model_layer, Conv2d): 20 | tensor = model_layer.weight.data.numpy() 21 | plot_kernels(tensor, 8) 22 | 23 | def plot_kernels(tensor, number_of_columns): 24 | num_kernels = tensor.shape[0] 25 | num_rows = 1 + num_kernels // number_of_columns 26 | fig = plt.figure(figsize=(number_of_columns, num_rows)) 27 | for i in range(num_kernels): 28 | ax1 = fig.add_subplot(num_rows, number_of_columns, i + 1) 29 | ax1.imshow(tensor[i][0,:,:], cmap='gray') 30 | ax1.axis('off') 31 | ax1.set_xticklabels([]) 32 | ax1.set_yticklabels([]) 33 | 34 | plt.subplots_adjust(wspace=0.1, hspace=0.1) 35 | plt.show() 36 | 37 | 38 | if __name__ == "__main__": 39 | visualize_filters() 40 | -------------------------------------------------------------------------------- /ModelGenerator-pytorch/datasets/AdditionalDataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import zipfile 4 | 5 | import argparse 6 | import numpy 7 | 8 | from datasets.OnlineDataset import OnlineDataset 9 | 10 | 11 | class AdditionalDataset(OnlineDataset): 12 | """ Loads images from a custom directory and splits them into train and validation 13 | directories with a random generator """ 14 | 15 | def __init__(self, destination_directory: str): 16 | """ 17 | Create and initializes a new dataset. 18 | :param destination_directory: The root directory, into which the data will be placed. 19 | """ 20 | super().__init__(destination_directory) 21 | self.url = "https://github.com/apacha/MusicScoreClassifier/releases/download/v1.0/MusicScoreClassificationDataset.zip" 22 | self.dataset_filename = "MusicScoreClassificationDataset.zip" 23 | 24 | def download_and_extract_dataset(self): 25 | if not os.path.exists(self.dataset_filename): 26 | print("Downloading Additional Dataset...") 27 | self.download_file(self.url, self.dataset_filename) 28 | 29 | print("Extracting Additional Dataset...") 30 | absolute_path_to_temp_folder = os.path.abspath(os.path.join(".", "temp")) 31 | self.extract_dataset_into_temp_folder(absolute_path_to_temp_folder) 32 | self.copy_images_from_temp_directory_into_destination_directory(absolute_path_to_temp_folder) 33 | self.clean_up_temp_directory(absolute_path_to_temp_folder) 34 | 35 | def copy_images_from_temp_directory_into_destination_directory(self, absolute_path_to_temp_folder: str): 36 | print("Copying additional images from {0} and its subdirectories".format(absolute_path_to_temp_folder)) 37 | path_to_all_files = [os.path.join(absolute_path_to_temp_folder, "scores", file) for file in 38 | os.listdir(os.path.join(absolute_path_to_temp_folder, "scores"))] 39 | destination_score_image = os.path.join(self.destination_directory, "scores") 40 | os.makedirs(destination_score_image, exist_ok=True) 41 | print("Copying {0} score images...".format(len(path_to_all_files))) 42 | for score_image in path_to_all_files: 43 | shutil.copy(score_image, destination_score_image) 44 | path_to_all_files = [os.path.join(absolute_path_to_temp_folder, "other", file) for file in 45 | os.listdir(os.path.join(absolute_path_to_temp_folder, "other"))] 46 | destination_other_image = os.path.join(self.destination_directory, "other") 47 | os.makedirs(destination_other_image, exist_ok=True) 48 | print("Copying {0} other images...".format(len(path_to_all_files))) 49 | for other_image in path_to_all_files: 50 | shutil.copy(other_image, destination_other_image) 51 | 52 | def extract_dataset_into_temp_folder(self, absolute_path_to_temp_folder: str): 53 | archive = zipfile.ZipFile(self.dataset_filename, "r") 54 | archive.extractall(absolute_path_to_temp_folder) 55 | archive.close() 56 | 57 | 58 | if __name__ == "__main__": 59 | parser = argparse.ArgumentParser() 60 | parser.add_argument( 61 | "--dataset_directory", 62 | type=str, 63 | default="../data", 64 | help="The directory, where the extracted dataset will be copied to") 65 | 66 | flags, unparsed = parser.parse_known_args() 67 | 68 | dataset = AdditionalDataset(flags.dataset_directory) 69 | dataset.download_and_extract_dataset() 70 | -------------------------------------------------------------------------------- /ModelGenerator-pytorch/datasets/DatasetSplitter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import shutil 4 | from typing import List 5 | 6 | import numpy 7 | 8 | 9 | class DatasetSplitter: 10 | """ The abstract base class for the datasets used to train the model """ 11 | 12 | def __init__(self, 13 | source_directory: str, 14 | destination_directory: str): 15 | """ 16 | 17 | :param source_directory: The root directory, where all images currently reside. 18 | Must have two sub-folders called scores and other. 19 | :param destination_directory: The root directory, into which the data will be placed. 20 | Inside of this directory, the following structure contains the data: 21 | 22 | directory 23 | |- training 24 | | |- other 25 | | |- scores 26 | | 27 | |- validation 28 | | |- other 29 | | |- scores 30 | | 31 | |- test 32 | | |- other 33 | | |- scores 34 | 35 | """ 36 | self.source_directory = source_directory 37 | self.destination_directory = os.path.abspath(destination_directory) 38 | 39 | def get_random_training_validation_and_test_sample_indices(self, 40 | dataset_size: int, 41 | validation_percentage: float = 0.1, 42 | test_percentage: float = 0.1, 43 | seed:int = 0) -> (List[int], List[int], List[int]): 44 | """ 45 | Returns a reproducible set of random sample indices from the entire dataset population 46 | :param dataset_size: The population size 47 | :param validation_percentage: the percentage of the entire population size that should be used for validation 48 | :param test_percentage: the percentage of the entire population size that should be used for testing 49 | :param seed: An arbitrary seed that can be used to obtain repeatable pseudo-random indices 50 | :return: A triple of three list, containing indices of the training, validation and test sets 51 | """ 52 | random.seed(seed) 53 | all_indices = range(0, dataset_size) 54 | validation_sample_size = int(dataset_size * validation_percentage) 55 | test_sample_size = int(dataset_size * test_percentage) 56 | validation_sample_indices = random.sample(all_indices, validation_sample_size) 57 | test_sample_indices = random.sample((set(all_indices) - set(validation_sample_indices)), test_sample_size) 58 | training_sample_indices = list(set(all_indices) - set(validation_sample_indices) - set(test_sample_indices)) 59 | return training_sample_indices, validation_sample_indices, test_sample_indices 60 | 61 | def split_images_into_training_validation_and_test_set(self): 62 | print("Splitting data into training, validation and test sets...") 63 | 64 | for image_class in ["other", "scores"]: 65 | path_to_images_of_class = os.path.join(self.source_directory, image_class) 66 | number_of_images_in_class = len(os.listdir(path_to_images_of_class)) 67 | training_sample_indices, validation_sample_indices, test_sample_indices = \ 68 | self.get_random_training_validation_and_test_sample_indices(number_of_images_in_class) 69 | 70 | self.copy_files(image_class, path_to_images_of_class, training_sample_indices, "training") 71 | self.copy_files(image_class, path_to_images_of_class, validation_sample_indices, "validation") 72 | self.copy_files(image_class, path_to_images_of_class, test_sample_indices, "test") 73 | 74 | def copy_files(self, image_class, path_to_images_of_class, sample_indices, name_of_split): 75 | files = numpy.array(os.listdir(path_to_images_of_class))[sample_indices] 76 | destination_path = os.path.join(self.destination_directory, name_of_split, image_class) 77 | os.makedirs(destination_path, exist_ok=True) 78 | print("Copying {0} {2} images of {1}...".format(len(files), image_class, name_of_split)) 79 | for image in files: 80 | shutil.copy(os.path.join(path_to_images_of_class, image), destination_path) 81 | 82 | 83 | if __name__ == "__main__": 84 | dataset_splitter = DatasetSplitter("../data", 85 | "../data") 86 | dataset_splitter.split_images_into_training_validation_and_test_set() -------------------------------------------------------------------------------- /ModelGenerator-pytorch/datasets/ImageResizer.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | 3 | from PIL import Image 4 | from tqdm import tqdm 5 | 6 | 7 | def resize_images(path: str, width=224, height=224): 8 | for extension in ["png", "jpg"]: 9 | all_image_paths = glob(f"{path}/**/*.{extension}") 10 | for image_path in tqdm(all_image_paths): 11 | image = Image.open(image_path) #type:Image 12 | image = image.resize(size=(width, height), resample=Image.LANCZOS) 13 | image.save(image_path) 14 | 15 | 16 | if __name__ == "__main__": 17 | resize_images("data/training") 18 | resize_images("data/validation") 19 | resize_images("data/test") -------------------------------------------------------------------------------- /ModelGenerator-pytorch/datasets/MuscimaDataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import zipfile 3 | import shutil 4 | 5 | import argparse 6 | 7 | from datasets.OnlineDataset import OnlineDataset 8 | 9 | 10 | class MuscimaDataset(OnlineDataset): 11 | """ This dataset contains the Musicma Handwritten music scores database which consists of 12 | 1000 handwritten music scores from http://www.cvc.uab.es/cvcmuscima/index_database.html """ 13 | 14 | def __init__(self, destination_directory: str): 15 | super().__init__(destination_directory) 16 | self.url = "https://github.com/apacha/OMR-Datasets/releases/download/datasets/CVCMUSCIMA_WI.zip" 17 | self.dataset_filename = "CVCMUSCIMA_WI.zip" 18 | 19 | def download_and_extract_dataset(self): 20 | if not os.path.exists(self.dataset_filename): 21 | print("Downloading Muscima Dataset...") 22 | self.download_file(self.url) 23 | 24 | print("Extracting Muscima Dataset...") 25 | self.extract_dataset_into_temp_folder() 26 | absolute_image_directory = os.path.abspath(os.path.join(self.destination_directory, "scores")) 27 | self.copy_images_from_subdirectories_into_single_directory(absolute_image_directory) 28 | self.clean_up_temp_directory(os.path.abspath(os.path.join(".", "temp"))) 29 | 30 | def extract_dataset_into_temp_folder(self): 31 | archive = zipfile.ZipFile(self.dataset_filename, "r") 32 | archive.extractall("temp") 33 | archive.close() 34 | 35 | @staticmethod 36 | def copy_images_from_subdirectories_into_single_directory(absolute_image_directory: str) -> str: 37 | os.makedirs(absolute_image_directory, exist_ok=True) 38 | relative_path_to_writers = os.path.join(".", "temp", "CVCMUSCIMA_WI", "PNG_GT_Gray") 39 | absolute_path_to_writers = os.path.abspath(relative_path_to_writers) 40 | writer_directories_without_mac_system_directory = [os.path.join(absolute_path_to_writers, f) 41 | for f in os.listdir(absolute_path_to_writers) 42 | if f != ".DS_Store"] 43 | for writer_directory in writer_directories_without_mac_system_directory: 44 | images = [os.path.join(absolute_path_to_writers, writer_directory, f) 45 | for f in os.listdir(writer_directory) 46 | if f != ".DS_Store"] 47 | for image in images: 48 | destination_file = os.path.join(absolute_image_directory, 49 | os.path.basename(writer_directory) + "_" + os.path.basename(image)) 50 | shutil.copyfile(image, destination_file) 51 | 52 | return absolute_image_directory 53 | 54 | 55 | if __name__ == "__main__": 56 | parser = argparse.ArgumentParser() 57 | parser.add_argument( 58 | "--dataset_directory", 59 | type=str, 60 | default="../data", 61 | help="The directory, where the extracted dataset will be copied to") 62 | 63 | flags, unparsed = parser.parse_known_args() 64 | 65 | datasest = MuscimaDataset(flags.dataset_directory) 66 | datasest.download_and_extract_dataset() 67 | -------------------------------------------------------------------------------- /ModelGenerator-pytorch/datasets/OnlineDataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import shutil 4 | import urllib.parse as urlparse 5 | import urllib.request as urllib2 6 | from abc import ABC, abstractmethod 7 | from typing import List 8 | 9 | import numpy 10 | 11 | 12 | class OnlineDataset(ABC): 13 | """ The abstract base class for the datasets used to train the model """ 14 | 15 | def __init__(self, 16 | destination_directory: str): 17 | """ 18 | :param destination_directory: The root directory, into which the data will be placed. 19 | """ 20 | self.destination_directory = os.path.abspath(destination_directory) 21 | 22 | @abstractmethod 23 | def download_and_extract_dataset(self): 24 | """ Starts the download of the dataset and extracts it into the directory specified in the constructor """ 25 | pass 26 | 27 | def clean_up_temp_directory(self, temp_directory): 28 | print("Deleting temporary directory {0}".format(temp_directory)) 29 | shutil.rmtree(temp_directory) 30 | 31 | def download_file(self, url, destination_filename=None) -> str: 32 | u = urllib2.urlopen(url) 33 | scheme, netloc, path, query, fragment = urlparse.urlsplit(url) 34 | filename = os.path.basename(path) 35 | if not filename: 36 | filename = 'downloaded.file' 37 | if destination_filename: 38 | filename = destination_filename 39 | 40 | filename = os.path.abspath(filename) 41 | 42 | with open(filename, 'wb') as f: 43 | meta = u.info() 44 | meta_func = meta.getheaders if hasattr(meta, 'getheaders') else meta.get_all 45 | meta_length = meta_func("Content-Length") 46 | file_size = None 47 | if meta_length: 48 | file_size = int(meta_length[0]) 49 | print("Downloading: {0} Bytes: {1} into {2}".format(url, file_size, filename)) 50 | 51 | file_size_dl = 0 52 | block_sz = 8192 53 | status_counter = 0 54 | status_output_interval = 100 55 | while True: 56 | buffer = u.read(block_sz) 57 | if not buffer: 58 | break 59 | 60 | file_size_dl += len(buffer) 61 | f.write(buffer) 62 | status = "{0:16}".format(file_size_dl) 63 | if file_size: 64 | status += " [{0:6.2f}%]".format(file_size_dl * 100 / file_size) 65 | status += chr(13) 66 | status_counter += 1 67 | if status_counter == status_output_interval: 68 | status_counter = 0 69 | print(status) 70 | # print(status, end="", flush=True) Does not work unfortunately 71 | print() 72 | 73 | return filename 74 | -------------------------------------------------------------------------------- /ModelGenerator-pytorch/datasets/PascalVocDataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tarfile 4 | 5 | import argparse 6 | 7 | from datasets.OnlineDataset import OnlineDataset 8 | 9 | 10 | class PascalVocDataset(OnlineDataset): 11 | """ This dataset contains the Pascal VOC 2006 challenge database which consists over 12 | 2618 images of ten categories from http://host.robots.ox.ac.uk/pascal/VOC/databases.html#VOC2006 """ 13 | 14 | def __init__(self, destination_directory: str): 15 | super().__init__(destination_directory) 16 | self.url = "http://host.robots.ox.ac.uk/pascal/VOC/download/voc2006_trainval.tar" 17 | self.dataset_filename = "voc2006_trainval.tar" 18 | 19 | def download_and_extract_dataset(self): 20 | if not os.path.exists(self.dataset_filename): 21 | print("Downloading Pascal VOC Dataset ...") 22 | self.download_file(self.url) 23 | 24 | print("Extracting Pascal VOC Dataset...") 25 | 26 | temp_directory = os.path.abspath(os.path.join(".", "VOCdevkit")) 27 | absolute_image_directory = os.path.abspath(os.path.join(".", "VOCdevkit", "VOC2006", "PNGImages")) 28 | try: 29 | self.extract_dataset_into_temp_folder(temp_directory) 30 | self.copy_images_from_subdirectories_into_single_directory(absolute_image_directory) 31 | finally: 32 | self.clean_up_temp_directory(temp_directory) 33 | 34 | def extract_dataset_into_temp_folder(self, temp_directory: str): 35 | print("Extracting Pascal VOC dataset into temp directory") 36 | if os.path.exists(temp_directory): 37 | shutil.rmtree(temp_directory) 38 | tar = tarfile.open(self.dataset_filename, "r:") 39 | tar.extractall() 40 | tar.close() 41 | 42 | def copy_images_from_subdirectories_into_single_directory(self, absolute_image_directory: str): 43 | image_destination_directory = os.path.join(self.destination_directory, "other") 44 | os.makedirs(image_destination_directory, exist_ok=True) 45 | 46 | for image in [os.path.join(absolute_image_directory, name) for name in os.listdir(absolute_image_directory)]: 47 | shutil.copy(image, image_destination_directory) 48 | 49 | 50 | if __name__ == "__main__": 51 | parser = argparse.ArgumentParser() 52 | parser.add_argument( 53 | "--dataset_directory", 54 | type=str, 55 | default="../data", 56 | help="The directory, where the extracted dataset will be copied to") 57 | 58 | flags, unparsed = parser.parse_known_args() 59 | 60 | datasest = PascalVocDataset(flags.dataset_directory) 61 | datasest.download_and_extract_dataset() 62 | -------------------------------------------------------------------------------- /ModelGenerator-pytorch/datasets/ScoreClassificationDataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | from glob import glob 3 | from typing import Dict 4 | 5 | from PIL import Image 6 | from torch.utils.data import Dataset 7 | 8 | 9 | class ScoreClassificationDataset(Dataset): 10 | 11 | """ 12 | :param dataset_directory: Absolute path to the directory that must contain two 13 | folders named 'scores' and 'other' that will be used for the classification 14 | """ 15 | def __init__(self, dataset_directory, transform=None) -> None: 16 | self.transform = transform 17 | self.dataset_directory = dataset_directory 18 | score_image_paths = glob(os.path.join(dataset_directory, "scores/*.*")) 19 | other_image_paths = glob(os.path.join(dataset_directory, "other/*.*")) 20 | self.all_images = score_image_paths + other_image_paths 21 | self.classes = [0] * len(score_image_paths) + [1] * len(other_image_paths) 22 | self.classnames = {0: "score", 1:"other"} 23 | 24 | def __len__(self) -> int: 25 | return len(self.all_images) 26 | 27 | def __getitem__(self, index) -> Dict[str, object]: 28 | image = Image.open(self.all_images[index]) 29 | 30 | if self.transform: 31 | image = self.transform(image) 32 | 33 | sample = {'image': image, 'class': self.classes[index]} 34 | 35 | return sample -------------------------------------------------------------------------------- /ModelGenerator-pytorch/datasets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apacha/MusicScoreClassifier/203edfed510a753ac952eb326679a5b2bc03935e/ModelGenerator-pytorch/datasets/__init__.py -------------------------------------------------------------------------------- /ModelGenerator-pytorch/models/MobileNetV2.py: -------------------------------------------------------------------------------- 1 | import torch.nn as nn 2 | import math 3 | 4 | # Taken from https://github.com/ericsun99/MobileNet-V2-Pytorch/blob/master/MobileNetV2.py 5 | 6 | def conv_bn(inp, oup, stride): 7 | return nn.Sequential( 8 | nn.Conv2d(inp, oup, 3, stride, 1, bias=False), 9 | nn.BatchNorm2d(oup), 10 | nn.ReLU6(inplace=True) 11 | ) 12 | 13 | 14 | def conv_1x1_bn(inp, oup): 15 | return nn.Sequential( 16 | nn.Conv2d(inp, oup, 1, 1, 0, bias=False), 17 | nn.BatchNorm2d(oup), 18 | nn.ReLU6(inplace=True) 19 | ) 20 | 21 | 22 | class InvertedResidual(nn.Module): 23 | def __init__(self, inp, oup, stride, expand_ratio): 24 | super(InvertedResidual, self).__init__() 25 | self.stride = stride 26 | 27 | self.use_res_connect = self.stride == 1 and inp == oup 28 | 29 | self.conv = nn.Sequential( 30 | # pw 31 | nn.Conv2d(inp, inp * expand_ratio, 1, 1, 0, bias=False), 32 | nn.BatchNorm2d(inp * expand_ratio), 33 | nn.ReLU6(inplace=True), 34 | # dw 35 | nn.Conv2d(inp * expand_ratio, inp * expand_ratio, 3, stride, 1, groups=inp * expand_ratio, bias=False), 36 | nn.BatchNorm2d(inp * expand_ratio), 37 | nn.ReLU6(inplace=True), 38 | # pw-linear 39 | nn.Conv2d(inp * expand_ratio, oup, 1, 1, 0, bias=False), 40 | nn.BatchNorm2d(oup), 41 | ) 42 | 43 | def forward(self, x): 44 | if self.use_res_connect: 45 | return x + self.conv(x) 46 | else: 47 | return self.conv(x) 48 | 49 | 50 | class MobileNetV2(nn.Module): 51 | def __init__(self, num_classes=1000, input_size=224, width_mult=1.): 52 | super(MobileNetV2, self).__init__() 53 | # setting of inverted residual blocks 54 | self.interverted_residual_setting = [ 55 | # t, c, n, s 56 | [1, 16, 1, 1], 57 | [6, 24, 2, 2], 58 | [6, 32, 3, 2], 59 | [6, 64, 4, 2], 60 | [6, 96, 3, 1], 61 | [6, 160, 3, 2], 62 | [6, 320, 1, 1], 63 | ] 64 | 65 | # building first layer 66 | input_channel = int(32 * width_mult) 67 | self.last_channel = int(1280 * width_mult) if width_mult > 1.0 else 1280 68 | self.features = [conv_bn(3, input_channel, 2)] 69 | # building inverted residual blocks 70 | for t, c, n, s in self.interverted_residual_setting: 71 | output_channel = int(c * width_mult) 72 | for i in range(n): 73 | if i == 0: 74 | self.features.append(InvertedResidual(input_channel, output_channel, s, t)) 75 | else: 76 | self.features.append(InvertedResidual(input_channel, output_channel, 1, t)) 77 | input_channel = output_channel 78 | # building last several layers 79 | self.features.append(conv_1x1_bn(input_channel, self.last_channel)) 80 | self.features.append(nn.AvgPool2d(input_size//32)) 81 | # make it nn.Sequential 82 | self.features = nn.Sequential(*self.features) 83 | 84 | # building classifier 85 | self.classifier = nn.Sequential( 86 | nn.Dropout(), 87 | nn.Linear(self.last_channel, num_classes), 88 | ) 89 | 90 | 91 | def forward(self, x): 92 | x = self.features(x) 93 | x = x.view(-1, self.last_channel) 94 | x = self.classifier(x) 95 | return x -------------------------------------------------------------------------------- /ModelGenerator-pytorch/models/SimpleNetwork.py: -------------------------------------------------------------------------------- 1 | from torch import nn 2 | import torch.nn.functional as F 3 | 4 | 5 | class SimpleNetwork(nn.Module): 6 | """ A rudimentary configuration for starting """ 7 | 8 | def __init__(self): 9 | super(SimpleNetwork, self).__init__() 10 | self.pool = nn.MaxPool2d(2, 2) 11 | self.conv1 = nn.Conv2d(3, 64, 7, padding=1) 12 | self.conv2 = nn.Conv2d(64, 96, 3, padding=1) 13 | self.conv3 = nn.Conv2d(96, 128, 3, padding=1) 14 | self.conv4 = nn.Conv2d(128, 192, 3, padding=1) 15 | self.dropout = nn.Dropout(0.5) 16 | self.fc1 = nn.Linear(192 * 13 * 13, 64) 17 | self.fc2 = nn.Linear(64, 32) 18 | self.fc3 = nn.Linear(32, 2) 19 | 20 | def forward(self, x): 21 | x = self.pool(F.relu(self.conv1(x))) 22 | x = self.pool(F.relu(self.conv2(x))) 23 | x = self.pool(F.relu(self.conv3(x))) 24 | x = self.pool(F.relu(self.conv4(x))) 25 | x = x.view(x.size(0), -1) 26 | x = F.relu(self.fc1(x)) 27 | x = F.relu(self.fc2(x)) 28 | x = self.fc3(x) 29 | return x 30 | 31 | def name(self) -> str: 32 | """ Returns the name of this configuration """ 33 | return "simple" 34 | -------------------------------------------------------------------------------- /ModelGenerator-pytorch/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apacha/MusicScoreClassifier/203edfed510a753ac952eb326679a5b2bc03935e/ModelGenerator-pytorch/models/__init__.py -------------------------------------------------------------------------------- /ModelGenerator-pytorch/pytest.ini: -------------------------------------------------------------------------------- 1 | # content of pytest.ini 2 | # can also be defined in tox.ini or setup.cfg file, although the section 3 | # name in setup.cfg files should be "tool:pytest" 4 | [pytest] 5 | python_files=*Test.py 6 | python_classes=*Test 7 | python_functions=test_ -------------------------------------------------------------------------------- /ModelGenerator-pytorch/requirements.txt: -------------------------------------------------------------------------------- 1 | torch 2 | torchsummary 3 | torchvision 4 | # Optionally install via 'pip install git+https://github.com/szagoruyko/pytorchviz' 5 | # pytorchviz 6 | pytorch-ignite 7 | 8 | codecov 9 | pytest 10 | pytest-cov 11 | matplotlib 12 | scipy 13 | pillow 14 | tqdm -------------------------------------------------------------------------------- /ModelGenerator-pytorch/tests/OnlineDatasetTest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from TrainModel import delete_dataset_directory, download_datasets 5 | 6 | 7 | class OnlineDatasetTest(unittest.TestCase): 8 | def __init__(self, methodName: str = ...) -> None: 9 | super().__init__(methodName) 10 | 11 | def test_download_dataset(self): 12 | delete_dataset_directory("data") 13 | download_datasets("data") 14 | self.assertGreaterEqual(2, len(os.listdir("data"))) -------------------------------------------------------------------------------- /ModelGenerator-pytorch/tests/ScoreClassificationDatasetTest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import pytest 4 | from PIL.Image import Image 5 | from torchvision.transforms import Resize 6 | 7 | from datasets.ScoreClassificationDataset import ScoreClassificationDataset 8 | 9 | 10 | class ScoreClassificationDatasetTest(unittest.TestCase): 11 | def __init__(self, methodName: str = ...) -> None: 12 | super().__init__(methodName) 13 | self.dataset = ScoreClassificationDataset("data") 14 | 15 | def test_length(self): 16 | self.assertEqual(5618, len(self.dataset)) 17 | 18 | def test_get_item(self): 19 | first_item = self.dataset[0] 20 | self.assertEqual(0, first_item["class"]) 21 | 22 | def test_get_last_item(self): 23 | last_item = self.dataset[len(self.dataset) - 1] 24 | self.assertEqual(1, last_item["class"]) 25 | 26 | def test_get_item_expect_an_image(self): 27 | first_item = self.dataset[0] 28 | self.assertIsNotNone(first_item["image"]) 29 | self.assertIsInstance(first_item["image"], Image) 30 | 31 | def test_resize_image(self): 32 | resizer = Resize((128,128)) 33 | dataset = ScoreClassificationDataset("data", resizer) 34 | first_item = dataset[0] 35 | self.assertEqual(128, first_item["image"].width) 36 | -------------------------------------------------------------------------------- /ModelGenerator-pytorch/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apacha/MusicScoreClassifier/203edfed510a753ac952eb326679a5b2bc03935e/ModelGenerator-pytorch/tests/__init__.py -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/.gitignore: -------------------------------------------------------------------------------- 1 | /voc2006_trainval.tar 2 | /VOCdevkit/ 3 | /CVCMUSCIMA_WI.zip 4 | /data/ 5 | .idea 6 | /temp/ 7 | /simple-exported/ 8 | /export/ 9 | /results/ 10 | /MusicScoreClassificationDataset.zip 11 | /output_graph.pb 12 | /vgg.h5 13 | /output_graph/ 14 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/EvaluationPreprocessor.py: -------------------------------------------------------------------------------- 1 | import os 2 | from argparse import ArgumentParser 3 | 4 | import numpy 5 | from PIL import Image 6 | from scipy import ndimage 7 | from skimage.transform import resize 8 | from tqdm import tqdm 9 | 10 | 11 | def copy_and_resize(image_directory: str, target_directory: str, rescaled_width: int = 128, rescaled_height: int = 128): 12 | os.makedirs(target_directory, exist_ok=True) 13 | for image_class in ["other", "scores"]: 14 | index = 0 15 | image_class_directory = os.path.join(image_directory, image_class) 16 | class_target_directory = os.path.join(target_directory, image_class) 17 | os.makedirs(class_target_directory, exist_ok=True) 18 | for file in tqdm(os.listdir(image_class_directory)): 19 | # print(file) 20 | image = ndimage.imread(os.path.join(image_class_directory, file)) 21 | if image.ndim == 3: 22 | output_shape = (rescaled_width, rescaled_height, 3) 23 | else: 24 | output_shape = (rescaled_width, rescaled_height) 25 | 26 | resized = resize(image, output_shape=output_shape, preserve_range=True, mode='constant') 27 | destination_file_name = os.path.join(class_target_directory, image_class + "_" + str(index) + ".png") 28 | resized_image = Image.fromarray(resized.astype(numpy.uint8)) 29 | resized_image.save(destination_file_name) 30 | index += 1 31 | 32 | 33 | if __name__ == "__main__": 34 | parser = ArgumentParser() 35 | parser.add_argument( 36 | "-i", 37 | "--image_directory", 38 | dest="image_directory", 39 | type=str, 40 | default="C:\\Users\\Alex\\Repositories\\MusicScoreClassifier\\ModelGenerator\\data\\test", 41 | help="The directory that contains the entire image dataset (including two subfolders called other and scores for the two classes.", 42 | ) 43 | parser.add_argument( 44 | "-t", 45 | "--target_directory", 46 | dest="target_directory", 47 | type=str, 48 | default="C:\\Users\\Alex\\Repositories\\MusicScoreClassifier\\ModelGenerator\\data\\entire_dataset_128x128", 49 | help="The destination folder into which all files should be copied to", 50 | ) 51 | 52 | parser.add_argument("--width", type=int, default=128) 53 | parser.add_argument("--height", type=int, default=128) 54 | 55 | args = parser.parse_args() 56 | 57 | copy_and_resize(args.image_directory, args.target_directory, int(args.width), int(args.height)) 58 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/ExportModelToTensorflow.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import argparse 4 | import keras 5 | import shutil 6 | import tensorflow 7 | from keras import backend as K 8 | from keras.models import Sequential 9 | from tensorflow.contrib.session_bundle import exporter 10 | from tensorflow.core.protobuf import saver_pb2 11 | from tensorflow.python.client import session 12 | from tensorflow.python.framework import graph_io 13 | from tensorflow.python.ops import variables 14 | from tensorflow.python.tools import freeze_graph 15 | 16 | K.set_learning_phase(0) # all new operations will be in test mode from now on 17 | 18 | 19 | def export_model_to_tensorflow(path_to_trained_keras_model: str): 20 | print("Loading model for exporting to Protocol Buffer format...") 21 | model = keras.models.load_model(path_to_trained_keras_model) 22 | 23 | sess = K.get_session() 24 | 25 | # serialize the model and get its weights, for quick re-building 26 | config = model.get_config() 27 | weights = model.get_weights() 28 | 29 | # re-build a model where the learning phase is now hard-coded to 0 30 | new_model = Sequential.from_config(config) 31 | new_model.set_weights(weights) 32 | 33 | export_path = os.path.abspath(os.path.join("export", "simple")) # where to save the exported graph 34 | os.makedirs(export_path) 35 | checkpoint_state_name = "checkpoint_state" 36 | export_version = 1 # version number (integer) 37 | saver = tensorflow.train.Saver(sharded=True, name=checkpoint_state_name) 38 | model_exporter = exporter.Exporter(saver) 39 | signature = exporter.classification_signature(input_tensor=model.input, scores_tensor=model.output) 40 | 41 | # # Version 1 of exporter 42 | # model_exporter.init(sess.graph.as_graph_def(), default_graph_signature=signature) 43 | # model_exporter.export(export_path, tensorflow.constant(export_version), sess) 44 | # 45 | # # Version 2 of exporter 46 | # tensorflow.train.write_graph(sess.graph.as_graph_def(), logdir=".", name="simple.pbtxt", as_text=True) 47 | 48 | # Version 3 with Freezer from https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/tools/freeze_graph_test.py 49 | input_graph_name = "input_graph.pb" 50 | output_graph_name = "output_graph.pb" 51 | saver_write_version = saver_pb2.SaverDef.V2 52 | 53 | # We'll create an input graph that has a single variable containing 1.0, 54 | # and that then multiplies it by 2. 55 | saver = tensorflow.train.Saver(write_version=saver_write_version) 56 | checkpoint_path = saver.save(sess, export_path, global_step=0, latest_filename=checkpoint_state_name) 57 | graph_io.write_graph(sess.graph, export_path, input_graph_name) 58 | 59 | # We save out the graph to disk, and then call the const conversion 60 | # routine. 61 | input_graph_path = os.path.join(export_path, input_graph_name) 62 | input_saver_def_path = "" 63 | input_binary = False 64 | output_node_names = "output_node/Softmax" 65 | restore_op_name = "save/restore_all" 66 | filename_tensor_name = "save/Const:0" 67 | output_graph_path = os.path.join(export_path, output_graph_name) 68 | clear_devices = False 69 | freeze_graph.freeze_graph(input_graph_path, input_saver_def_path, 70 | input_binary, checkpoint_path, output_node_names, 71 | restore_op_name, filename_tensor_name, 72 | output_graph_path, clear_devices, "") 73 | 74 | shutil.copy(os.path.join("export", "simple", "output_graph.pb"), output_graph_name) 75 | shutil.rmtree("export") 76 | print("Exported model: {0}".format(os.path.abspath(output_graph_name))) 77 | 78 | 79 | if __name__ == "__main__": 80 | parser = argparse.ArgumentParser() 81 | parser.add_argument( 82 | "--path_to_trained_keras_model", 83 | type=str, 84 | default="mobilenetv2.h5", 85 | help="The path to the file that containes the trained keras model that should be converted to a Protobuf file") 86 | 87 | flags, unparsed = parser.parse_known_args() 88 | 89 | export_model_to_tensorflow(flags.path_to_trained_keras_model) 90 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/TestModel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys, os, inspect 3 | 4 | from argparse import ArgumentParser 5 | 6 | import keras 7 | import numpy 8 | import skimage 9 | from keras.utils import plot_model 10 | from scipy import ndimage 11 | from PIL import Image 12 | 13 | from skimage.transform import resize 14 | 15 | print("Parsing arguments ...") 16 | 17 | parser = ArgumentParser("Classify an RGB-image with a pre-trained classifier") 18 | parser.add_argument("-c", "--model", dest="model_path", help="path to the classifier (*.h5)") 19 | parser.add_argument("-i", "--image", dest="image_path", help="path to the rgb image to classify") 20 | args = parser.parse_args() 21 | 22 | if len(sys.argv) < 5: 23 | parser.print_help() 24 | sys.exit(-1) 25 | 26 | model_path = args.model_path 27 | image_path = args.image_path 28 | 29 | print(" Model: ", model_path) 30 | print(" Image: ", image_path) 31 | 32 | print("Loading image ...") 33 | input_image = ndimage.imread(image_path, mode="RGB") 34 | print(" Shape: {0}".format(input_image.shape)) 35 | 36 | print("Loading classifier...") 37 | classifier = keras.models.load_model(model_path) 38 | classifier.summary() 39 | input_shape = classifier.input_shape[1:4] 40 | print(" Input shape: {0}, Output: {1} classes".format(input_shape, classifier.output_shape[1])) 41 | 42 | print("Preprocessing image ...") 43 | print(" Resizing to " + str(input_shape)) 44 | normalized_input_image = resize(input_image, output_shape=input_shape, preserve_range=True) 45 | normalized_input_image = normalized_input_image.astype(numpy.float32) 46 | 47 | print(" Result: shape: {0}, dtype: {1}, mean: {2:.3f}, std: {3:.3f}".format(normalized_input_image.shape, 48 | normalized_input_image.dtype, 49 | numpy.mean(normalized_input_image), 50 | numpy.std(normalized_input_image))) 51 | 52 | print("Classifying image ...") 53 | 54 | scores = classifier.predict(numpy.array([normalized_input_image])).flatten() 55 | print(" Class scores: {0}".format(numpy.array2string(scores, formatter={'float_kind': lambda x: "%0.2f" % x}))) 56 | class_with_highest_probability = numpy.where(scores == scores.max())[0][0] 57 | 58 | class_names = ['other', 'scores'] 59 | print(" Image is most likely: {0} (certainty: {1:0.2f})".format(class_names[class_with_highest_probability], 60 | scores[class_with_highest_probability])) 61 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/TrainModel.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | from time import time 4 | 5 | import argparse 6 | 7 | import keras 8 | import numpy as np 9 | import shutil 10 | from keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau 11 | from keras.preprocessing.image import ImageDataGenerator 12 | 13 | from TrainingHistoryPlotter import TrainingHistoryPlotter 14 | from datasets.AdditionalDataset import AdditionalDataset 15 | from datasets.DatasetSplitter import DatasetSplitter 16 | from datasets.MuscimaDataset import MuscimaDataset 17 | from datasets.PascalVocDataset import PascalVocDataset 18 | from models.ConfigurationFactory import ConfigurationFactory 19 | 20 | 21 | def train_model(dataset_directory: str, 22 | model_name: str, 23 | show_plot_after_training: bool, 24 | delete_and_recreate_dataset_directory: bool): 25 | print("Downloading and extracting datasets...") 26 | 27 | if delete_and_recreate_dataset_directory: 28 | print("Deleting dataset directory and creating it anew") 29 | 30 | if os.path.exists(dataset_directory): 31 | shutil.rmtree(dataset_directory) 32 | 33 | pascal_voc_dataset = PascalVocDataset(dataset_directory) 34 | pascal_voc_dataset.download_and_extract_dataset() 35 | muscima_dataset = MuscimaDataset(dataset_directory) 36 | muscima_dataset.download_and_extract_dataset() 37 | additional_dataset = AdditionalDataset(dataset_directory) 38 | additional_dataset.download_and_extract_dataset() 39 | 40 | dataset_splitter = DatasetSplitter(dataset_directory, dataset_directory) 41 | dataset_splitter.split_images_into_training_validation_and_test_set() 42 | 43 | print("Training on dataset...") 44 | start_time = time() 45 | 46 | training_configuration = ConfigurationFactory.get_configuration_by_name(model_name) 47 | img_rows, img_cols = training_configuration.data_shape[0], training_configuration.data_shape[1] 48 | number_of_pixels_shift = training_configuration.number_of_pixel_shift 49 | 50 | train_generator = ImageDataGenerator(horizontal_flip=True, 51 | rotation_range=10, 52 | width_shift_range=number_of_pixels_shift / img_rows, 53 | height_shift_range=number_of_pixels_shift / img_cols, 54 | ) 55 | training_data_generator = train_generator.flow_from_directory(os.path.join(dataset_directory, "training"), 56 | target_size=(img_cols, img_rows), 57 | batch_size=training_configuration.training_minibatch_size, 58 | ) 59 | training_steps_per_epoch = np.math.ceil(training_data_generator.samples / training_data_generator.batch_size) 60 | 61 | validation_generator = ImageDataGenerator() 62 | validation_data_generator = validation_generator.flow_from_directory(os.path.join(dataset_directory, "validation"), 63 | target_size=(img_cols, img_rows), 64 | batch_size=training_configuration.training_minibatch_size) 65 | validation_steps_per_epoch = np.math.ceil(validation_data_generator.samples / validation_data_generator.batch_size) 66 | 67 | test_generator = ImageDataGenerator() 68 | test_data_generator = test_generator.flow_from_directory(os.path.join(dataset_directory, "test"), 69 | target_size=(img_cols, img_rows), 70 | batch_size=training_configuration.training_minibatch_size) 71 | test_steps_per_epoch = np.math.ceil(test_data_generator.samples / test_data_generator.batch_size) 72 | 73 | model = training_configuration.classifier() 74 | model.summary() 75 | 76 | print("Model {0} loaded.".format(training_configuration.name())) 77 | print(training_configuration.summary()) 78 | 79 | best_model_path = "{0}.h5".format(training_configuration.name()) 80 | 81 | model_checkpoint = ModelCheckpoint(best_model_path, monitor="val_acc", save_best_only=True, verbose=1) 82 | early_stop = EarlyStopping(monitor='val_acc', 83 | patience=training_configuration.number_of_epochs_before_early_stopping, 84 | verbose=1) 85 | learning_rate_reduction = ReduceLROnPlateau(monitor='val_acc', 86 | patience=training_configuration.number_of_epochs_before_reducing_learning_rate, 87 | verbose=1, 88 | factor=training_configuration.learning_rate_reduction_factor, 89 | min_lr=training_configuration.minimum_learning_rate) 90 | history = model.fit_generator( 91 | generator=training_data_generator, 92 | steps_per_epoch=training_steps_per_epoch, 93 | epochs=training_configuration.number_of_epochs, 94 | callbacks=[model_checkpoint, early_stop, learning_rate_reduction], 95 | validation_data=validation_data_generator, 96 | validation_steps=validation_steps_per_epoch 97 | ) 98 | 99 | print("Loading best model from check-point and testing...") 100 | best_model = keras.models.load_model(best_model_path) 101 | 102 | evaluation = best_model.evaluate_generator(test_data_generator, steps=test_steps_per_epoch) 103 | 104 | print(best_model.metrics_names) 105 | print("Loss : ", evaluation[0]) 106 | print("Accuracy : ", evaluation[1]) 107 | print("Error : ", 1 - evaluation[1]) 108 | end_time = time() 109 | print("Execution time: %.1fs" % (end_time - start_time)) 110 | 111 | TrainingHistoryPlotter.plot_history(history, 112 | "Results-{0}-{1}.png".format(training_configuration.name(), 113 | datetime.date.today()), 114 | show_plot=show_plot_after_training) 115 | 116 | 117 | if __name__ == "__main__": 118 | parser = argparse.ArgumentParser() 119 | parser.register("type", "bool", lambda v: v.lower() == "true") 120 | parser.add_argument("--dataset_directory", type=str, default="data", 121 | help="The directory, that is used for storing the images during training") 122 | parser.add_argument("--model_name", type=str, default="mobilenetv2", 123 | help="The model used for training the network. " 124 | "Currently allowed values are \'simple\', \'vgg\', \'xception\', \'mobilenetv2\'") 125 | parser.add_argument("--show_plot_after_training", nargs="?", const=True, type="bool", default=True, 126 | help="Whether to show a plot with the accuracies after training or not.") 127 | parser.add_argument("--delete_and_recreate_dataset_directory", dest="delete_and_recreate_dataset_directory", 128 | action="store_true", 129 | help="Whether to delete and recreate the dataset-directory (by downloading the appropriate " 130 | "files from the internet) or simply use whatever data currently is inside of that " 131 | "directory") 132 | parser.set_defaults(delete_and_recreate_dataset_directory=False) 133 | 134 | flags, unparsed = parser.parse_known_args() 135 | 136 | train_model(flags.dataset_directory, 137 | flags.model_name, 138 | flags.show_plot_after_training, 139 | flags.delete_and_recreate_dataset_directory) 140 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/TrainingHistoryPlotter.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from keras.callbacks import History 3 | from matplotlib import pyplot 4 | 5 | 6 | class TrainingHistoryPlotter: 7 | @staticmethod 8 | def plot_history(history: History, file_name: str, show_plot: bool = True): 9 | training_loss = history.history['loss'] 10 | training_accuracy = history.history['acc'] 11 | validation_loss = history.history['val_loss'] 12 | validation_accuracy = history.history['val_acc'] 13 | 14 | epoch_list = numpy.add(history.epoch, 1) # Add 1 so it starts with epoch 1 instead of 0 15 | 16 | fig = pyplot.figure(1) 17 | # fig.suptitle('TRAINNING vs VALIDATION', fontsize=14, fontweight='bold') 18 | 19 | fig.add_subplot(211) 20 | pyplot.xlabel("Epoch") 21 | pyplot.ylabel("Loss") 22 | pyplot.plot(epoch_list, training_loss, '--', linewidth=2, label='Training loss') 23 | pyplot.plot(epoch_list, validation_loss, label='Validation loss') 24 | pyplot.legend(loc='upper right') 25 | 26 | fig.add_subplot(212) 27 | pyplot.xlabel("Epoch") 28 | pyplot.ylabel("Accuracy") 29 | pyplot.plot(epoch_list, training_accuracy, '--', linewidth=2, label='Training accuracy') 30 | pyplot.plot(epoch_list, validation_accuracy, label='Validation accuracy') 31 | pyplot.legend(loc='lower right') 32 | 33 | # pyplot.subplots_adjust(wspace=0.1) 34 | pyplot.tight_layout() 35 | pyplot.savefig(file_name) 36 | 37 | if show_plot: 38 | pyplot.show() 39 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/datasets/AdditionalDataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import zipfile 4 | 5 | import argparse 6 | import numpy 7 | 8 | from datasets.Dataset import Dataset 9 | 10 | 11 | class AdditionalDataset(Dataset): 12 | """ Loads images from a custom directory and splits them into train and validation 13 | directories with a random generator """ 14 | 15 | def __init__(self, destination_directory: str): 16 | """ 17 | Create and initializes a new dataset. 18 | :param destination_directory: The root directory, into which the data will be placed. 19 | """ 20 | super().__init__(destination_directory) 21 | self.url = "https://github.com/apacha/MusicScoreClassifier/releases/download/v1.0/MusicScoreClassificationDataset.zip" 22 | self.dataset_filename = "MusicScoreClassificationDataset.zip" 23 | 24 | def download_and_extract_dataset(self): 25 | if not os.path.exists(self.dataset_filename): 26 | print("Downloading Additional Dataset...") 27 | self.download_file(self.url, self.dataset_filename) 28 | 29 | print("Extracting Additional Dataset...") 30 | absolute_path_to_temp_folder = os.path.abspath(os.path.join(".", "temp")) 31 | self.extract_dataset_into_temp_folder(absolute_path_to_temp_folder) 32 | self.copy_images_from_temp_directory_into_destination_directory(absolute_path_to_temp_folder) 33 | self.clean_up_temp_directory(absolute_path_to_temp_folder) 34 | 35 | def copy_images_from_temp_directory_into_destination_directory(self, absolute_path_to_temp_folder: str): 36 | print("Copying additional images from {0} and its subdirectories".format(absolute_path_to_temp_folder)) 37 | path_to_all_files = [os.path.join(absolute_path_to_temp_folder, "scores", file) for file in 38 | os.listdir(os.path.join(absolute_path_to_temp_folder, "scores"))] 39 | destination_score_image = os.path.join(self.destination_directory, "scores") 40 | os.makedirs(destination_score_image, exist_ok=True) 41 | print("Copying {0} score images...".format(len(path_to_all_files))) 42 | for score_image in path_to_all_files: 43 | shutil.copy(score_image, destination_score_image) 44 | path_to_all_files = [os.path.join(absolute_path_to_temp_folder, "other", file) for file in 45 | os.listdir(os.path.join(absolute_path_to_temp_folder, "other"))] 46 | destination_other_image = os.path.join(self.destination_directory, "other") 47 | os.makedirs(destination_other_image, exist_ok=True) 48 | print("Copying {0} other images...".format(len(path_to_all_files))) 49 | for other_image in path_to_all_files: 50 | shutil.copy(other_image, destination_other_image) 51 | 52 | def extract_dataset_into_temp_folder(self, absolute_path_to_temp_folder: str): 53 | archive = zipfile.ZipFile(self.dataset_filename, "r") 54 | archive.extractall(absolute_path_to_temp_folder) 55 | archive.close() 56 | 57 | 58 | if __name__ == "__main__": 59 | parser = argparse.ArgumentParser() 60 | parser.add_argument( 61 | "--dataset_directory", 62 | type=str, 63 | default="../data", 64 | help="The directory, where the extracted dataset will be copied to") 65 | 66 | flags, unparsed = parser.parse_known_args() 67 | 68 | dataset = AdditionalDataset(flags.dataset_directory) 69 | dataset.download_and_extract_dataset() 70 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/datasets/Dataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import shutil 4 | import urllib.parse as urlparse 5 | import urllib.request as urllib2 6 | from abc import ABC, abstractmethod 7 | from typing import List 8 | 9 | import numpy 10 | 11 | 12 | class Dataset(ABC): 13 | """ The abstract base class for the datasets used to train the model """ 14 | 15 | def __init__(self, 16 | destination_directory: str): 17 | """ 18 | :param destination_directory: The root directory, into which the data will be placed. 19 | """ 20 | self.destination_directory = os.path.abspath(destination_directory) 21 | 22 | @abstractmethod 23 | def download_and_extract_dataset(self): 24 | """ Starts the download of the dataset and extracts it into the directory specified in the constructor """ 25 | pass 26 | 27 | def clean_up_temp_directory(self, temp_directory): 28 | print("Deleting temporary directory {0}".format(temp_directory)) 29 | shutil.rmtree(temp_directory) 30 | 31 | def download_file(self, url, destination_filename=None) -> str: 32 | u = urllib2.urlopen(url) 33 | scheme, netloc, path, query, fragment = urlparse.urlsplit(url) 34 | filename = os.path.basename(path) 35 | if not filename: 36 | filename = 'downloaded.file' 37 | if destination_filename: 38 | filename = destination_filename 39 | 40 | filename = os.path.abspath(filename) 41 | 42 | with open(filename, 'wb') as f: 43 | meta = u.info() 44 | meta_func = meta.getheaders if hasattr(meta, 'getheaders') else meta.get_all 45 | meta_length = meta_func("Content-Length") 46 | file_size = None 47 | if meta_length: 48 | file_size = int(meta_length[0]) 49 | print("Downloading: {0} Bytes: {1} into {2}".format(url, file_size, filename)) 50 | 51 | file_size_dl = 0 52 | block_sz = 8192 53 | status_counter = 0 54 | status_output_interval = 100 55 | while True: 56 | buffer = u.read(block_sz) 57 | if not buffer: 58 | break 59 | 60 | file_size_dl += len(buffer) 61 | f.write(buffer) 62 | status = "{0:16}".format(file_size_dl) 63 | if file_size: 64 | status += " [{0:6.2f}%]".format(file_size_dl * 100 / file_size) 65 | status += chr(13) 66 | status_counter += 1 67 | if status_counter == status_output_interval: 68 | status_counter = 0 69 | print(status) 70 | # print(status, end="", flush=True) Does not work unfortunately 71 | print() 72 | 73 | return filename 74 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/datasets/DatasetSplitter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import shutil 4 | from typing import List 5 | 6 | import numpy 7 | 8 | 9 | class DatasetSplitter: 10 | """ The abstract base class for the datasets used to train the model """ 11 | 12 | def __init__(self, 13 | source_directory: str, 14 | destination_directory: str): 15 | """ 16 | 17 | :param source_directory: The root directory, where all images currently reside. 18 | Must have two sub-folders called scores and other. 19 | :param destination_directory: The root directory, into which the data will be placed. 20 | Inside of this directory, the following structure contains the data: 21 | 22 | directory 23 | |- training 24 | | |- other 25 | | |- scores 26 | | 27 | |- validation 28 | | |- other 29 | | |- scores 30 | | 31 | |- test 32 | | |- other 33 | | |- scores 34 | 35 | """ 36 | self.source_directory = source_directory 37 | self.destination_directory = os.path.abspath(destination_directory) 38 | 39 | def get_random_training_validation_and_test_sample_indices(self, 40 | dataset_size: int, 41 | validation_percentage: float = 0.1, 42 | test_percentage: float = 0.1, 43 | seed:int = 0) -> (List[int], List[int], List[int]): 44 | """ 45 | Returns a reproducible set of random sample indices from the entire dataset population 46 | :param dataset_size: The population size 47 | :param validation_percentage: the percentage of the entire population size that should be used for validation 48 | :param test_percentage: the percentage of the entire population size that should be used for testing 49 | :param seed: An arbitrary seed that can be used to obtain repeatable pseudo-random indices 50 | :return: A triple of three list, containing indices of the training, validation and test sets 51 | """ 52 | random.seed(seed) 53 | all_indices = range(0, dataset_size) 54 | validation_sample_size = int(dataset_size * validation_percentage) 55 | test_sample_size = int(dataset_size * test_percentage) 56 | validation_sample_indices = random.sample(all_indices, validation_sample_size) 57 | test_sample_indices = random.sample((set(all_indices) - set(validation_sample_indices)), test_sample_size) 58 | training_sample_indices = list(set(all_indices) - set(validation_sample_indices) - set(test_sample_indices)) 59 | return training_sample_indices, validation_sample_indices, test_sample_indices 60 | 61 | def split_images_into_training_validation_and_test_set(self): 62 | print("Splitting data into training, validation and test sets...") 63 | 64 | for image_class in ["other", "scores"]: 65 | path_to_images_of_class = os.path.join(self.source_directory, image_class) 66 | number_of_images_in_class = len(os.listdir(path_to_images_of_class)) 67 | training_sample_indices, validation_sample_indices, test_sample_indices = \ 68 | self.get_random_training_validation_and_test_sample_indices(number_of_images_in_class) 69 | 70 | self.copy_files(image_class, path_to_images_of_class, training_sample_indices, "training") 71 | self.copy_files(image_class, path_to_images_of_class, validation_sample_indices, "validation") 72 | self.copy_files(image_class, path_to_images_of_class, test_sample_indices, "test") 73 | 74 | def copy_files(self, image_class, path_to_images_of_class, sample_indices, name_of_split): 75 | files = numpy.array(os.listdir(path_to_images_of_class))[sample_indices] 76 | destination_path = os.path.join(self.destination_directory, name_of_split, image_class) 77 | os.makedirs(destination_path, exist_ok=True) 78 | print("Copying {0} {2} images of {1}...".format(len(files), image_class, name_of_split)) 79 | for image in files: 80 | shutil.copy(os.path.join(path_to_images_of_class, image), destination_path) 81 | 82 | 83 | if __name__ == "__main__": 84 | dataset_splitter = DatasetSplitter("../data", 85 | "../data") 86 | dataset_splitter.split_images_into_training_validation_and_test_set() -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/datasets/MuscimaDataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import zipfile 3 | import shutil 4 | 5 | import argparse 6 | 7 | from datasets.Dataset import Dataset 8 | 9 | 10 | class MuscimaDataset(Dataset): 11 | """ This dataset contains the Musicma Handwritten music scores database which consists of 12 | 1000 handwritten music scores from http://www.cvc.uab.es/cvcmuscima/index_database.html """ 13 | 14 | def __init__(self, destination_directory: str): 15 | super().__init__(destination_directory) 16 | self.url = "http://www.cvc.uab.es/cvcmuscima/CVCMUSCIMA_WI.zip" 17 | self.dataset_filename = "CVCMUSCIMA_WI.zip" 18 | 19 | def download_and_extract_dataset(self): 20 | if not os.path.exists(self.dataset_filename): 21 | print("Downloading Muscima Dataset...") 22 | self.download_file(self.url) 23 | 24 | print("Extracting Muscima Dataset...") 25 | self.extract_dataset_into_temp_folder() 26 | absolute_image_directory = os.path.abspath(os.path.join(self.destination_directory, "scores")) 27 | self.copy_images_from_subdirectories_into_single_directory(absolute_image_directory) 28 | self.clean_up_temp_directory(os.path.abspath(os.path.join(".", "temp"))) 29 | 30 | def extract_dataset_into_temp_folder(self): 31 | archive = zipfile.ZipFile(self.dataset_filename, "r") 32 | archive.extractall("temp") 33 | archive.close() 34 | 35 | @staticmethod 36 | def copy_images_from_subdirectories_into_single_directory(absolute_image_directory: str) -> str: 37 | os.makedirs(absolute_image_directory, exist_ok=True) 38 | relative_path_to_writers = os.path.join(".", "temp", "CVCMUSCIMA_WI", "PNG_GT_Gray") 39 | absolute_path_to_writers = os.path.abspath(relative_path_to_writers) 40 | writer_directories_without_mac_system_directory = [os.path.join(absolute_path_to_writers, f) 41 | for f in os.listdir(absolute_path_to_writers) 42 | if f != ".DS_Store"] 43 | for writer_directory in writer_directories_without_mac_system_directory: 44 | images = [os.path.join(absolute_path_to_writers, writer_directory, f) 45 | for f in os.listdir(writer_directory) 46 | if f != ".DS_Store"] 47 | for image in images: 48 | destination_file = os.path.join(absolute_image_directory, 49 | os.path.basename(writer_directory) + "_" + os.path.basename(image)) 50 | shutil.copyfile(image, destination_file) 51 | 52 | return absolute_image_directory 53 | 54 | 55 | if __name__ == "__main__": 56 | parser = argparse.ArgumentParser() 57 | parser.add_argument( 58 | "--dataset_directory", 59 | type=str, 60 | default="../data", 61 | help="The directory, where the extracted dataset will be copied to") 62 | 63 | flags, unparsed = parser.parse_known_args() 64 | 65 | datasest = MuscimaDataset(flags.dataset_directory) 66 | datasest.download_and_extract_dataset() 67 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/datasets/PascalVocDataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tarfile 4 | 5 | import argparse 6 | 7 | from datasets.Dataset import Dataset 8 | 9 | 10 | class PascalVocDataset(Dataset): 11 | """ This dataset contains the Pascal VOC 2006 challenge database which consists over 12 | 2618 images of ten categories from http://host.robots.ox.ac.uk/pascal/VOC/databases.html#VOC2006 """ 13 | 14 | def __init__(self, destination_directory: str): 15 | super().__init__(destination_directory) 16 | self.url = "http://host.robots.ox.ac.uk/pascal/VOC/download/voc2006_trainval.tar" 17 | self.dataset_filename = "voc2006_trainval.tar" 18 | 19 | def download_and_extract_dataset(self): 20 | if not os.path.exists(self.dataset_filename): 21 | print("Downloading Pascal VOC Dataset ...") 22 | self.download_file(self.url) 23 | 24 | print("Extracting Pascal VOC Dataset...") 25 | 26 | temp_directory = os.path.abspath(os.path.join(".", "VOCdevkit")) 27 | absolute_image_directory = os.path.abspath(os.path.join(".", "VOCdevkit", "VOC2006", "PNGImages")) 28 | try: 29 | self.extract_dataset_into_temp_folder(temp_directory) 30 | self.copy_images_from_subdirectories_into_single_directory(absolute_image_directory) 31 | finally: 32 | self.clean_up_temp_directory(temp_directory) 33 | 34 | def extract_dataset_into_temp_folder(self, temp_directory: str): 35 | print("Extracting Pascal VOC dataset into temp directory") 36 | if os.path.exists(temp_directory): 37 | shutil.rmtree(temp_directory) 38 | tar = tarfile.open(self.dataset_filename, "r:") 39 | tar.extractall() 40 | tar.close() 41 | 42 | def copy_images_from_subdirectories_into_single_directory(self, absolute_image_directory: str): 43 | image_destination_directory = os.path.join(self.destination_directory, "other") 44 | os.makedirs(image_destination_directory, exist_ok=True) 45 | 46 | for image in [os.path.join(absolute_image_directory, name) for name in os.listdir(absolute_image_directory)]: 47 | shutil.copy(image, image_destination_directory) 48 | 49 | 50 | if __name__ == "__main__": 51 | parser = argparse.ArgumentParser() 52 | parser.add_argument( 53 | "--dataset_directory", 54 | type=str, 55 | default="../data", 56 | help="The directory, where the extracted dataset will be copied to") 57 | 58 | flags, unparsed = parser.parse_known_args() 59 | 60 | datasest = PascalVocDataset(flags.dataset_directory) 61 | datasest.download_and_extract_dataset() 62 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/datasets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apacha/MusicScoreClassifier/203edfed510a753ac952eb326679a5b2bc03935e/ModelGenerator-tensorflow/datasets/__init__.py -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/models/ConfigurationFactory.py: -------------------------------------------------------------------------------- 1 | from models import TrainingConfiguration 2 | from models.MobileNetV2Configuration import MobileNetV2Configuration 3 | from models.SimpleConfiguration import SimpleConfiguration 4 | from models.VggConfiguration import VggConfiguration 5 | from models.XceptionConfiguration import XceptionConfiguration 6 | 7 | 8 | class ConfigurationFactory: 9 | @staticmethod 10 | def get_configuration_by_name(name: str = "simple") -> TrainingConfiguration: 11 | configurations = [] 12 | configurations.append(SimpleConfiguration()) 13 | configurations.append(VggConfiguration()) 14 | configurations.append(XceptionConfiguration()) 15 | configurations.append(MobileNetV2Configuration()) 16 | 17 | for i in range(len(configurations)): 18 | if configurations[i].name() == name: 19 | return configurations[i] 20 | 21 | raise Exception("No configuration found by name {0}".format(name)) 22 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/models/MobileNetV2Configuration.py: -------------------------------------------------------------------------------- 1 | from keras import Model 2 | from keras.applications import MobileNetV2 3 | from keras.layers import Dense 4 | from keras.optimizers import Adadelta 5 | from keras_applications.xception import Xception 6 | 7 | from models.TrainingConfiguration import TrainingConfiguration 8 | 9 | 10 | class MobileNetV2Configuration(TrainingConfiguration): 11 | 12 | def __init__(self): 13 | super().__init__(data_shape=(224, 224, 3), learning_rate=1.0, training_minibatch_size=16) 14 | 15 | def classifier(self) -> Model: 16 | """ Returns the classifier of this configuration """ 17 | model = MobileNetV2(include_top=False, weights='imagenet', input_shape=self.data_shape, pooling='avg') 18 | dense = Dense(2, activation='softmax', name='predictions')(model.output) 19 | model = Model(model.input, dense, name='xception') 20 | 21 | # optimizer = SGD(lr=self.learning_rate, momentum=self.nesterov_momentum, nesterov=True) 22 | optimizer = Adadelta(lr=self.learning_rate) 23 | model.compile(optimizer, loss="categorical_crossentropy", metrics=["accuracy"]) 24 | return model 25 | 26 | def name(self) -> str: 27 | """ Returns the name of this configuration """ 28 | return "mobilenetv2" 29 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/models/SimpleConfiguration.py: -------------------------------------------------------------------------------- 1 | from keras.layers import Convolution2D, Activation, MaxPooling2D, Flatten, Dense, Conv2D, BatchNormalization, \ 2 | AveragePooling2D, Dropout 3 | from keras.models import Sequential 4 | from keras.optimizers import SGD 5 | from keras.regularizers import l2 6 | 7 | from models.TrainingConfiguration import TrainingConfiguration 8 | 9 | 10 | class SimpleConfiguration(TrainingConfiguration): 11 | """ A rudimentary configuration for starting """ 12 | 13 | def classifier(self) -> Sequential: 14 | """ Returns the classifier of this configuration """ 15 | classifier = Sequential() 16 | 17 | self.add_convolution(classifier, 64, 7, self.weight_decay, strides=(2, 2), input_shape=self.data_shape) 18 | classifier.add(MaxPooling2D()) 19 | 20 | self.add_convolution(classifier, 96, 3, self.weight_decay) 21 | classifier.add(MaxPooling2D()) 22 | 23 | self.add_convolution(classifier, 128, 3, self.weight_decay) 24 | classifier.add(MaxPooling2D()) 25 | 26 | self.add_convolution(classifier, 192, 3, self.weight_decay) 27 | classifier.add(MaxPooling2D()) 28 | 29 | classifier.add(Flatten()) # Flatten 30 | classifier.add(Dropout(0.5)) 31 | classifier.add(Dense(units=2, kernel_regularizer=l2(self.weight_decay))) 32 | classifier.add(Activation('softmax', name="output_node")) 33 | 34 | stochastic_gradient_descent = SGD(lr=self.learning_rate, momentum=self.nesterov_momentum, nesterov=True) 35 | classifier.compile(stochastic_gradient_descent, loss="categorical_crossentropy", metrics=["accuracy"]) 36 | return classifier 37 | 38 | def add_convolution(self, classifier, filters, kernel_size, weight_decay, strides=(1, 1), input_shape=None): 39 | if input_shape is None: 40 | classifier.add(Convolution2D(filters, kernel_size, strides=strides, padding='same', kernel_regularizer=l2(weight_decay))) 41 | else: 42 | classifier.add( 43 | Convolution2D(filters, kernel_size, padding='same', kernel_regularizer=l2(weight_decay), input_shape=input_shape)) 44 | classifier.add(BatchNormalization()) 45 | classifier.add(Activation('relu')) 46 | 47 | def name(self) -> str: 48 | """ Returns the name of this configuration """ 49 | return "simple" 50 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/models/TrainingConfiguration.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from keras.engine import Model 4 | 5 | 6 | class TrainingConfiguration(ABC): 7 | def __init__(self, 8 | data_shape: tuple = (256, 256, 3), 9 | number_of_epochs: int = 200, 10 | number_of_epochs_before_early_stopping: int = 10, 11 | number_of_epochs_before_reducing_learning_rate: int = 5, 12 | training_minibatch_size: int = 16, 13 | initialization: str = "he_normal", 14 | learning_rate: float = 0.001, 15 | learning_rate_reduction_factor: float = 0.5, 16 | minimum_learning_rate: float = 0.0001, 17 | weight_decay: float = 0.0001, 18 | nesterov_momentum: float = 0.9, 19 | number_of_pixel_shift=24.0 20 | ): 21 | self.data_shape = data_shape 22 | self.number_of_epochs = number_of_epochs 23 | self.number_of_epochs_before_early_stopping = number_of_epochs_before_early_stopping 24 | self.number_of_epochs_before_reducing_learning_rate = number_of_epochs_before_reducing_learning_rate 25 | self.training_minibatch_size = training_minibatch_size 26 | self.initialization = initialization 27 | self.learning_rate = learning_rate 28 | self.learning_rate_reduction_factor = learning_rate_reduction_factor 29 | self.minimum_learning_rate = minimum_learning_rate 30 | self.weight_decay = weight_decay 31 | self.nesterov_momentum = nesterov_momentum 32 | self.number_of_pixel_shift = number_of_pixel_shift 33 | 34 | @abstractmethod 35 | def classifier(self) -> Model: 36 | """ Returns the classifier of this configuration """ 37 | pass 38 | 39 | @abstractmethod 40 | def name(self) -> str: 41 | """ Returns the name of this configuration """ 42 | pass 43 | 44 | def summary(self) -> str: 45 | """ Returns the string that summarizes this configuration """ 46 | summary = "Training for {0:d} epochs with initial learning rate of {1}, weight-decay of {2} and Nesterov Momentum of {3} ...\n" \ 47 | .format(self.number_of_epochs, self.learning_rate, self.weight_decay, self.nesterov_momentum) 48 | summary += "Additional parameters: Initialization: {0}, Minibatch-size: {1}, Early stopping after {2} epochs without improvement\n" \ 49 | .format(self.initialization, self.training_minibatch_size, self.number_of_epochs_before_early_stopping) 50 | summary += "Data-Shape: {0}, Reducing learning rate by factor to {1} respectively if not improved validation accuracy after {2} epochs" \ 51 | .format(self.data_shape, self.learning_rate_reduction_factor, 52 | self.number_of_epochs_before_reducing_learning_rate) 53 | return summary 54 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/models/VggConfiguration.py: -------------------------------------------------------------------------------- 1 | from keras.layers import Activation, BatchNormalization, Convolution2D, Dense, Dropout, Flatten, MaxPooling2D 2 | from keras.models import Sequential 3 | from keras.optimizers import SGD 4 | from keras.regularizers import l2 5 | 6 | from models.TrainingConfiguration import TrainingConfiguration 7 | 8 | 9 | class VggConfiguration(TrainingConfiguration): 10 | """ A rudimentary configuration for starting """ 11 | 12 | def classifier(self) -> Sequential: 13 | """ Returns the classifier of this configuration """ 14 | classifier = Sequential() 15 | 16 | self.add_convolution(classifier, 16, 3, self.weight_decay, input_shape=self.data_shape) 17 | self.add_convolution(classifier, 16, 3, self.weight_decay) 18 | classifier.add(MaxPooling2D()) 19 | 20 | self.add_convolution(classifier, 32, 3, self.weight_decay) 21 | self.add_convolution(classifier, 32, 3, self.weight_decay) 22 | classifier.add(MaxPooling2D()) 23 | 24 | self.add_convolution(classifier, 64, 3, self.weight_decay) 25 | self.add_convolution(classifier, 64, 3, self.weight_decay) 26 | self.add_convolution(classifier, 64, 3, self.weight_decay) 27 | classifier.add(MaxPooling2D()) 28 | 29 | self.add_convolution(classifier, 128, 3, self.weight_decay) 30 | self.add_convolution(classifier, 128, 3, self.weight_decay) 31 | self.add_convolution(classifier, 128, 3, self.weight_decay) 32 | classifier.add(MaxPooling2D()) 33 | 34 | self.add_convolution(classifier, 192, 3, self.weight_decay) 35 | self.add_convolution(classifier, 192, 3, self.weight_decay) 36 | self.add_convolution(classifier, 192, 3, self.weight_decay) 37 | self.add_convolution(classifier, 192, 3, self.weight_decay) 38 | classifier.add(MaxPooling2D()) 39 | 40 | classifier.add(Flatten()) # Flatten 41 | classifier.add(Dropout(0.5)) 42 | classifier.add(Dense(units=2, kernel_regularizer=l2(self.weight_decay))) 43 | classifier.add(Activation('softmax', name="output_node")) 44 | 45 | stochastic_gradient_descent = SGD(lr=self.learning_rate, momentum=self.nesterov_momentum, nesterov=True) 46 | classifier.compile(stochastic_gradient_descent, loss="categorical_crossentropy", metrics=["accuracy"]) 47 | return classifier 48 | 49 | def add_convolution(self, classifier, filters, kernel_size, weight_decay, strides=(1, 1), input_shape=None): 50 | if input_shape is None: 51 | classifier.add(Convolution2D(filters, kernel_size, strides=strides, padding='same', 52 | kernel_regularizer=l2(weight_decay))) 53 | else: 54 | classifier.add( 55 | Convolution2D(filters, kernel_size, padding='same', kernel_regularizer=l2(weight_decay), 56 | input_shape=input_shape)) 57 | classifier.add(BatchNormalization()) 58 | classifier.add(Activation('relu')) 59 | 60 | def name(self) -> str: 61 | """ Returns the name of this configuration """ 62 | return "vgg" 63 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/models/XceptionConfiguration.py: -------------------------------------------------------------------------------- 1 | from keras import Model 2 | from keras.applications import Xception 3 | from keras.layers import Dense 4 | from keras.optimizers import Adadelta 5 | 6 | from models.TrainingConfiguration import TrainingConfiguration 7 | 8 | 9 | class XceptionConfiguration(TrainingConfiguration): 10 | """ A rudimentary configuration for starting """ 11 | 12 | def __init__(self): 13 | super().__init__(data_shape = (299, 299, 3), learning_rate=1.0, training_minibatch_size=4) 14 | 15 | def classifier(self) -> Model: 16 | """ Returns the classifier of this configuration """ 17 | model = Xception(include_top=False, weights='imagenet', input_shape=self.data_shape, pooling='avg') 18 | dense = Dense(2, activation='softmax', name='predictions')(model.output) 19 | model = Model(model.input, dense, name='xception') 20 | 21 | # optimizer = SGD(lr=self.learning_rate, momentum=self.nesterov_momentum, nesterov=True) 22 | optimizer = Adadelta(lr=self.learning_rate) 23 | model.compile(optimizer, loss="categorical_crossentropy", metrics=["accuracy"]) 24 | return model 25 | 26 | def name(self) -> str: 27 | """ Returns the name of this configuration """ 28 | return "xception" 29 | -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apacha/MusicScoreClassifier/203edfed510a753ac952eb326679a5b2bc03935e/ModelGenerator-tensorflow/models/__init__.py -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/requirements.txt: -------------------------------------------------------------------------------- 1 | keras 2 | tensorflow 3 | # tensorflow-gpu # Alternatively if available 4 | codecov 5 | pytest 6 | pytest-cov 7 | matplotlib 8 | scipy 9 | pillow 10 | tqdm -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apacha/MusicScoreClassifier/203edfed510a753ac952eb326679a5b2bc03935e/ModelGenerator-tensorflow/tests/__init__.py -------------------------------------------------------------------------------- /ModelGenerator-tensorflow/tests/tensorflow_test.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | 3 | 4 | class SquareTest(tf.test.TestCase): 5 | 6 | def testSquare(self): 7 | with self.test_session(): 8 | x = tf.square([2, 3]) 9 | self.assertAllEqual(x.eval(), [4, 9]) 10 | 11 | 12 | if __name__ == '__main__': 13 | tf.test.main() 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Music Score Classifier 2 | 3 | This repository is the model trainer part of the Mobile Music Score Classifier, which is a mobile Android application that takes the live camera-feed and classifies the image in real-time into either music scores, or something else and displays the result in the application. It is part of a set of three tools: 4 | 5 | |[Model Trainer](https://github.com/apacha/MusicScoreClassifier)|[Mobile App](https://github.com/apacha/MobileMusicScoreClassifier)|[Manual Classifier](https://github.com/apacha/ManualMusicScoreClassifier)| 6 | |:----:|:-----:|:-----:| 7 | |Trains a deep network to automatically classify images into scores or something else.|Mobile Android application that uses a trained model to perform real-time classification on a mobile device.|A small C#/WPF application that can be used manually classify images, used during evaluation| 8 | |[![Build Status](https://travis-ci.org/apacha/MusicScoreClassifier.svg?branch=master)](https://travis-ci.org/apacha/MusicScoreClassifier)|[![Build Status](https://travis-ci.org/apacha/MobileMusicScoreClassifier.svg?branch=master)](https://travis-ci.org/apacha/MobileMusicScoreClassifier)|[![Build status](https://ci.appveyor.com/api/projects/status/4715vyioa98eje0k?svg=true)](https://ci.appveyor.com/project/apacha/manualmusicscoreclassifier)| 9 | |[![codecov](https://codecov.io/gh/apacha/MusicScoreClassifier/branch/master/graph/badge.svg)](https://codecov.io/gh/apacha/MusicScoreClassifier)||| 10 | 11 | You might also be interested to check out my [follow-up work](https://github.com/apacha/MusicSymbolClassifier). 12 | 13 | # Running the application 14 | This repository contains several scripts that can be used independently of each other. 15 | Before running them, make sure that you have the necessary requirements installed. Note that there are two versions, depending on the Deep Learning framework you prefer: Tensorflow/Keras or PyTorch. 16 | 17 | ## Requirements 18 | 19 | This application has been tested with the following versions, but older and newer versions of Tensorflow and Keras are very likely to work exactly the same: 20 | 21 | - Python 3.6 22 | - Keras 2.2.4 23 | - Tensorflow 1.11.0 (or optionally tensorflow-gpu 1.11.0) 24 | 25 | or 26 | 27 | - Python 3.6 28 | - PyTorch 1.0 29 | 30 | Optional: If you want to print the graph of the model being trained, install [GraphViz for Windows](https://graphviz.gitlab.io/_pages/Download/Download_windows.html) via and add /bin to the PATH or run `sudo apt-get install graphviz` on Ubuntu (see https://github.com/fchollet/keras/issues/3210) 31 | 32 | We recommend [Anaconda](https://www.continuum.io/downloads) or 33 | [Miniconda](https://conda.io/miniconda.html) as Python distribution (we did so for preparing Travis-CI and it worked). To accelerate training even further, you can make use of your GPU, by installing tensorflow-gpu instead of tensorflow 34 | via pip (note that you can only have one of them) and the required Nvidia drivers. 35 | 36 | ## Training the model 37 | 38 | `python TrainModel.py` can be used to training the convolutional neural network. 39 | It will automatically download and prepare three separate datasets for training with 40 | Keras and Tensorflow (MUSCIMA dataset of handwritten music scores, 41 | Pascal VOC dataset of general purpose images and an additional dataset that 42 | was created for this project, containing 1000 realistic score images and 1000 43 | images of other documents and objects). 44 | 45 | The result of this training is a .h5 (e.g. mobilenetv2.h5) file that contains the trained model. 46 | 47 | _Troubleshooting_: If for some reason the download of any of the datasets fails, stop the script, remove the partially 48 | downloaded file and restart the script. 49 | 50 | ## Using a trained model for inference 51 | You can download a trained model from [here](https://github.com/apacha/MusicScoreClassifier/releases). 52 | 53 | To classify an image, you can use the `TestModel.py` script and call it like this: `python TextModel.py -c mobilenetv2.h5 -i image_to_classify.jpg` 54 | 55 | ## Exporting the Model for being used in Tensorflow 56 | 57 | Since the Android App only uses Tensorflow, the resulting Keras model (despite having a tensorflow model inside) 58 | has to be exported into a Protobuf file. This is a bit cumbersome, because Tensorflow separates between 59 | the model description and the actual weights. To get both of them into one file, one has to freeze the model. 60 | 61 | `python ExportModelToTensorflow.py --path_to_trained_keras_model vgg.h5` will take the file `vgg.h5` and create 62 | a file called `output_graph.pb` that is ready to be used in the Android application. 63 | 64 | # Additional Dataset 65 | If you are just interested in the additional dataset that was created for this project, 66 | it can be downloaded from [here](https://github.com/apacha/MusicScoreClassifier/releases/download/v1.0/MusicScoreClassificationDataset.zip). 67 | If you are using this dataset or the code from this repository, please consider citing the following [publication](https://alexanderpacha.files.wordpress.com/2018/06/icmla-2017-paper-towards-self-learning-optical-music-recognition-published.pdf): 68 | 69 | ```text 70 | @InProceedings{Pacha2017a, 71 | author = {Pacha, Alexander and Eidenberger, Horst}, 72 | title = {Towards Self-Learning Optical Music Recognition}, 73 | booktitle = {2017 16th IEEE International Conference on Machine Learning and Applications (ICMLA)}, 74 | year = {2017}, 75 | pages = {795--800}, 76 | doi = {10.1109/ICMLA.2017.00-60}, 77 | } 78 | ``` 79 | 80 | # License 81 | 82 | Published under MIT License, 83 | 84 | Copyright (c) 2019 [Alexander Pacha](http://alexanderpacha.com), [TU Wien](https://www.ims.tuwien.ac.at/people/alexander-pacha) 85 | 86 | Permission is hereby granted, free of charge, to any person obtaining a copy 87 | of this software and associated documentation files (the "Software"), to deal 88 | in the Software without restriction, including without limitation the rights 89 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 90 | copies of the Software, and to permit persons to whom the Software is 91 | furnished to do so, subject to the following conditions: 92 | 93 | The above copyright notice and this permission notice shall be included in all 94 | copies or substantial portions of the Software. 95 | 96 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 97 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 98 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 99 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 100 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 101 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 102 | SOFTWARE. 103 | --------------------------------------------------------------------------------