Source code for music21.omr.evaluators

# ------------------------------------------------------------------------------
# Name:         omr/evaluators.py
# Purpose:      music21 module for evaluating correcting of output from OMR software
#
# Authors:      Maura Church
#               Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2014 Maura Church, Michael Scott Asato Cuthbert,
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
This module takes two XML files and displays the number of measures that
differ between the two before and after running the combined correction models
'''
from __future__ import annotations

from music21.omr import correctors
from music21 import converter

globalDebug = False


[docs] class OmrGroundTruthPair: ''' Object for making comparisons between an OMR score and the GroundTruth Takes in a path to the OMR and a path to the groundTruth (or a pair of music21.stream.Score objects). See below for examples. ''' def __init__(self, omr=None, ground=None): self._overriddenDebug = None self.numberOfDifferences = None if hasattr(omr, 'filePath'): self.omrPath = omr.filePath self.omrM21Score = omr else: self.omrPath = omr self.omrM21Score = None self.omrScore = self.getOmrScore() if hasattr(ground, 'filePath'): self.groundPath = ground.filePath self.groundM21Score = ground else: self.groundPath = ground self.groundM21Score = None self.groundScore = self.getGroundScore() @property def debug(self): ''' Returns either the debug value set for this evaluator, or globalDebug ''' if self._overriddenDebug is None: return globalDebug else: return self._overriddenDebug @debug.setter def debug(self, newDebug): self._overriddenDebug = newDebug
[docs] def parseAll(self): ''' Parse both scores. ''' self.omrScore = self.getOmrScore() self.groundScore = self.getGroundScore()
[docs] def hashAll(self): ''' store the Hashes for both scores. ''' self.omrScore.getAllHashes() self.groundScore.getAllHashes()
[docs] def getOmrScore(self): ''' Returns a ScoreCorrector object of the OMR score. does not store it anywhere >>> omrPath = omr.correctors.K525omrShortPath >>> ground = omr.correctors.K525groundTruthShortPath >>> omrGTP = omr.evaluators.OmrGroundTruthPair(omr=omrPath, ground=ground) >>> ssOMR = omrGTP.getOmrScore() >>> ssOMR <music21.omr.correctors.ScoreCorrector object at 0x...> ''' if self.debug: print('parsing OMR score') if not self.omrM21Score: self.omrM21Score = converter.parse(self.omrPath) return correctors.ScoreCorrector(self.omrM21Score)
[docs] def getGroundScore(self): ''' Returns a ScoreCorrector object of the Ground truth score >>> omrPath = omr.correctors.K525omrShortPath >>> ground = omr.correctors.K525groundTruthShortPath >>> omrGTP = omr.evaluators.OmrGroundTruthPair(omr=omrPath, ground=ground) >>> ssGT = omrGTP.getGroundScore() >>> ssGT <music21.omr.correctors.ScoreCorrector object at 0x...> ''' if self.debug: print('parsing Ground Truth score') if not self.groundM21Score: self.groundM21Score = converter.parse(self.groundPath) return correctors.ScoreCorrector(self.groundM21Score)
# def getDifferencesBetweenAlignedScores(self): # ''' # Returns the number of differences (int) between # two scores with aligned indices # ''' # self.numberOfDifferences = 0 # aList = self.omrScore.getAllHashes() # bList = self.groundScore.getAllHashes() # for i in range(len(aList)): # for j in range(min(len(aList[i]), len(bList[i]))): # a = aList[i][j] # b = bList[i][j] # s = difflib.SequenceMatcher(None, a, b) # ratio = s.ratio() # measureErrors = (1-ratio) * len(a) # self.numberOfDifferences += measureErrors # return self.numberOfDifferences
[docs] def substCost(self, x, y): ''' define the substitution cost for x and y (2 if x and y are unequal else 0) ''' if x == y: return 0 else: return 2
[docs] def insertCost(self, x): ''' define the insertion cost for x and y (1) ''' return 1
[docs] def deleteCost(self, x): ''' define the deletion cost for x and y (1) ''' return 1
[docs] def minEditDist(self, target, source): ''' Computes the min edit distance from target to source. Figure 3.25 ''' n = len(target) m = len(source) distance = [[0 for i in range(m + 1)] for j in range(n + 1)] for i in range(1, n + 1): distance[i][0] = distance[i - 1][0] + self.insertCost(target[i - 1]) for j in range(1, m + 1): distance[0][j] = distance[0][j - 1] + self.deleteCost(source[j - 1]) for i in range(1, n + 1): for j in range(1, m + 1): distance[i][j] = min(distance[i - 1][j] + 1, distance[i][j - 1] + 1, distance[i - 1][j - 1] + self.substCost(source[j - 1], target[i - 1])) return distance[n][m]
[docs] def getDifferences(self): ''' Returns the total edit distance as an Int between the two scores This function is based on James H. Martin's minimum edit distance. >>> omrPath = omr.correctors.K525omrShortPath >>> ground = omr.correctors.K525groundTruthShortPath >>> omrGTP = omr.evaluators.OmrGroundTruthPair(omr=omrPath, ground=ground) >>> differences = omrGTP.getDifferences() >>> differences 32 ''' self.numberOfDifferences = 0 omrList = self.omrScore.getAllHashes() gtList = self.groundScore.getAllHashes() for partNum in range(len(omrList)): omrPart = omrList[partNum] gtPart = gtList[partNum] measureErrors = self.minEditDist(omrPart, gtPart) self.numberOfDifferences += measureErrors return self.numberOfDifferences
[docs] def evaluateCorrectingModel(omrPath, groundTruthPath, debug=None, originalDifferences=None, runOnePart=False): # noinspection PyShadowingNames ''' Get a dictionary showing the efficacy of the omr.correctors.ScoreCorrector on an OMR Score by comparing it to the GroundTruth. Set debug to True to see a lot of intermediary steps. >>> omrFilePath = omr.correctors.K525omrShortPath >>> groundTruthFilePath = omr.correctors.K525groundTruthShortPath >>> returnDict = omr.evaluators.evaluateCorrectingModel(omrFilePath, groundTruthFilePath) >>> for name in sorted(list(returnDict.keys())): ... (name, returnDict[name]) ('newEditDistance', 20) ('numberOfFlaggedMeasures', 13) ('originalEditDistance', 32) ('totalNumberOfMeasures', 84) ''' if debug is None: debug = globalDebug # declare part number (0 indexed) if running single part pn = 1 # get number of differences T omrGTP = OmrGroundTruthPair(omr=omrPath, ground=groundTruthPath) if debug: print('getting differences') if originalDifferences is None: numberOfDifferences = omrGTP.getDifferences() else: numberOfDifferences = originalDifferences if debug: print('Original edit distance', numberOfDifferences) myOmrScore = omrGTP.omrScore s = myOmrScore if debug: print('Running Horizontal Model (Prior-based-on-distance)') correctingArrayHorAllPart = [] numberOfIncorrectMeasures = 0 numberOfTotalMeasures = 0 if runOnePart is True: scorePart = s.singleParts[pn] incorrectMeasureIndices = scorePart.getIncorrectMeasureIndices() if debug: print('Incorrect measure indices:', incorrectMeasureIndices) print('Hashed notes:', s.singleParts[pn].hashedNotes) scorePart.runHorizontalCorrectionModel() else: for tempPN in range(len(s.singleParts)): scorePart = s.singleParts[tempPN] incorrectMeasureIndices = scorePart.getIncorrectMeasureIndices() numberOfIncorrectMeasures += len(incorrectMeasureIndices) correctingArrayHorOnePart = scorePart.runHorizontalCorrectionModel() correctingArrayHorAllPart.append(correctingArrayHorOnePart) numberOfTotalMeasures += len(s.singleParts[tempPN].hashedNotes) if debug: print('for each entry in the array below, we have ') print('[flagged measure part, flagged measure index, source measure part, ' + 'source measure index, source measure probability]') print('HORIZONTAL CORRECTING ARRAY', correctingArrayHorAllPart) print('**********************************') print('Running Vertical Model (Prior-based-on-Parts)') correctingArrayVertAllPart = s.runVerticalCorrectionModel() if debug: print('for each entry in the array below, we have ') print('[flagged measure part, flagged measure index, source measure part,' + ' source measure index, source measure probability]') print('VERTICAL CORRECTING MEASURES', correctingArrayVertAllPart) print('**********************************') print('Finding best from Horizontal and Vertical and replacing flagged ' + 'measures with source measures') priorScore = s.generateCorrectedScore(correctingArrayHorAllPart, correctingArrayVertAllPart) if debug: print('done replacing flagged measures with source measures') print(priorScore) # get new number of differences newNumberOfDifferences = omrGTP.getDifferences() if debug: print('new edit distance', newNumberOfDifferences) print('number of flagged measures originally', numberOfIncorrectMeasures) print('total number of measures', numberOfTotalMeasures) s.score.show() returnDict = {'originalEditDistance': numberOfDifferences, 'newEditDistance': newNumberOfDifferences, 'numberOfFlaggedMeasures': numberOfIncorrectMeasures, 'totalNumberOfMeasures': numberOfTotalMeasures} return returnDict
[docs] def autoCorrelationBestMeasure(inputScore): ''' Essentially it's the ratio of amount of rhythmic similarity within a piece, which gives an upper bound on what the omr.corrector.prior measure should be able to achieve for the flagged measures. If a piece has low rhythmic similarity in general, then there's no way for a correct match to be found within the unflagged measures in the piece. Returns a tuple of the total number of NON-flagged measures and the total number of those measures that have a rhythmic match. Takes in a stream.Score. >>> c = converter.parse(omr.correctors.K525omrShortPath) # first 21 measures >>> totalUnflagged, totalUnflaggedWithMatches = omr.evaluators.autoCorrelationBestMeasure(c) >>> (totalUnflagged, totalUnflaggedWithMatches) (71, 64) >>> print( float(totalUnflaggedWithMatches) / totalUnflagged ) 0.901... Schoenberg has low autoCorrelation. >>> c = corpus.parse('schoenberg/opus19/movement6') >>> totalUnflagged, totalUnflaggedWithMatches = omr.evaluators.autoCorrelationBestMeasure(c) >>> (totalUnflagged, totalUnflaggedWithMatches) (18, 6) >>> print( float(totalUnflaggedWithMatches) / totalUnflagged ) 0.333... ''' ss = correctors.ScoreCorrector(inputScore) allHashes = ss.getAllHashes() totalMeasures = 0 totalMatches = 0 singleParts = ss.singleParts for pNum, pHashArray in enumerate(allHashes): incorrectIndices = singleParts[pNum].getIncorrectMeasureIndices() for i, mHash in enumerate(pHashArray): if i in incorrectIndices: continue totalMeasures += 1 match = False # horizontal search for j, nHash in enumerate(pHashArray): if i == j: continue if mHash == nHash: match = True break # vertical search if match is False: for otherPNum in range(len(singleParts)): if otherPNum == pNum: continue otherHash = allHashes[otherPNum][i] if otherHash == mHash: match = True break if match is True: totalMatches += 1 return (totalMeasures, totalMatches)
if __name__ == '__main__': import music21 music21.mainTest()