# ------------------------------------------------------------------------------
# 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()