Skip to main content

music21_rs/omr/
evaluators.rs

1//! Measuring how well the correctors do: music21's `omr.evaluators`. A
2//! score read by optical music recognition is held to the same score
3//! entered by hand, its ground truth, by how many measures' rhythms differ.
4
5use crate::error::{Error, Result};
6use crate::stream::Stream;
7
8use super::correctors::ScoreCorrector;
9
10/// How many measures it takes to turn one list of measure rhythms into
11/// another, a measure put in or left out costing one and one replaced two:
12/// music21's `minEditDist`.
13pub fn min_edit_distance(target: &[String], source: &[String]) -> usize {
14    let (n, m) = (target.len(), source.len());
15    let mut distance = vec![vec![0; m + 1]; n + 1];
16    for i in 1..=n {
17        distance[i][0] = distance[i - 1][0] + 1;
18    }
19    for j in 1..=m {
20        distance[0][j] = distance[0][j - 1] + 1;
21    }
22    for i in 1..=n {
23        for j in 1..=m {
24            let substitution = if source[j - 1] == target[i - 1] { 0 } else { 2 };
25            distance[i][j] = (distance[i - 1][j] + 1)
26                .min(distance[i][j - 1] + 1)
27                .min(distance[i - 1][j - 1] + substitution);
28        }
29    }
30    distance[n][m]
31}
32
33/// A score read by optical music recognition and its ground truth, each
34/// ready to be corrected: music21's `OmrGroundTruthPair`.
35#[derive(Clone, Debug)]
36pub struct OmrGroundTruthPair {
37    omr: ScoreCorrector,
38    ground: ScoreCorrector,
39}
40
41impl OmrGroundTruthPair {
42    /// The pair, each score in a corrector.
43    ///
44    /// # Errors
45    ///
46    /// As [`ScoreCorrector::new`].
47    pub fn new(omr: Stream, ground: Stream) -> Result<Self> {
48        Ok(Self {
49            omr: ScoreCorrector::new(omr)?,
50            ground: ScoreCorrector::new(ground)?,
51        })
52    }
53
54    /// The score read by optical music recognition.
55    pub fn omr(&self) -> &ScoreCorrector {
56        &self.omr
57    }
58
59    /// The same, to be corrected.
60    pub fn omr_mut(&mut self) -> &mut ScoreCorrector {
61        &mut self.omr
62    }
63
64    /// The ground truth.
65    pub fn ground(&self) -> &ScoreCorrector {
66        &self.ground
67    }
68
69    /// The edit distances between the measure rhythms of each part of the
70    /// recognised score and those of the same part of the ground truth,
71    /// summed: music21's `getDifferences`.
72    ///
73    /// # Errors
74    ///
75    /// A ground truth with fewer parts, which music21 fails on.
76    pub fn differences(&self) -> Result<usize> {
77        let theirs = self.ground.all_hashes();
78        let mut total = 0;
79        for (part, ours) in self.omr.all_hashes().into_iter().enumerate() {
80            let truth = theirs
81                .get(part)
82                .ok_or_else(|| Error::Omr("list index out of range".to_string()))?;
83            total += min_edit_distance(ours, truth);
84        }
85        Ok(total)
86    }
87}
88
89/// What [`evaluate_correcting_model`] finds: music21's dictionary of the
90/// same.
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93pub struct Evaluation {
94    /// The edit distance from the ground truth before correcting.
95    pub original_edit_distance: usize,
96    /// The edit distance after.
97    pub new_edit_distance: usize,
98    /// How many measures were flagged.
99    pub number_of_flagged_measures: usize,
100    /// How many measures the recognised score has.
101    pub total_number_of_measures: usize,
102}
103
104/// Corrects a recognised score and measures it against its ground truth
105/// before and after: music21's `evaluateCorrectingModel`. Measures are
106/// flagged against each one's own time signature; `original_differences`
107/// stands for the distance before, where it is known.
108///
109/// # Errors
110///
111/// As [`OmrGroundTruthPair::differences`],
112/// [`ScoreCorrector::find_incorrect_measures`] and [`ScoreCorrector::run`].
113pub fn evaluate_correcting_model(
114    omr: Stream,
115    ground: Stream,
116    original_differences: Option<usize>,
117) -> Result<Evaluation> {
118    let mut pair = OmrGroundTruthPair::new(omr, ground)?;
119    let original_edit_distance = match original_differences {
120        Some(differences) => differences,
121        None => pair.differences()?,
122    };
123    let corrector = pair.omr_mut();
124    let mut horizontal = Vec::new();
125    let (mut flagged, mut total) = (0, 0);
126    for part in 0..corrector.single_parts().len() {
127        flagged += corrector.find_incorrect_measures(part, false)?.len();
128        horizontal.push(
129            corrector
130                .single_part_mut(part)
131                .run_horizontal_correction_model(),
132        );
133        total += corrector.single_parts()[part].hashed_notes().len();
134    }
135    let vertical = corrector.run_vertical_correction_model()?;
136    corrector.generate_corrected_score(&horizontal, &vertical)?;
137    Ok(Evaluation {
138        original_edit_distance,
139        new_edit_distance: pair.differences()?,
140        number_of_flagged_measures: flagged,
141        total_number_of_measures: total,
142    })
143}
144
145/// How many measures not flagged have their rhythm somewhere else -- in
146/// another measure of their part, or at the same place in another part --
147/// out of how many there are: music21's `autoCorrelationBestMeasure`, as
148/// `(measures, matches)`.
149///
150/// # Errors
151///
152/// As [`ScoreCorrector::new`] and
153/// [`ScoreCorrector::find_incorrect_measures`], or parts of different
154/// lengths where music21 runs out of measures.
155pub fn auto_correlation_best_measure(score: Stream) -> Result<(usize, usize)> {
156    let mut corrector = ScoreCorrector::new(score)?;
157    let hashes: Vec<Vec<String>> = corrector
158        .all_hashes()
159        .into_iter()
160        .map(<[String]>::to_vec)
161        .collect();
162    let (mut measures, mut matches) = (0, 0);
163    for (part, own) in hashes.iter().enumerate() {
164        let incorrect = corrector.find_incorrect_measures(part, false)?.to_vec();
165        for (i, hash) in own.iter().enumerate() {
166            if incorrect.contains(&i) {
167                continue;
168            }
169            measures += 1;
170            let mut found = own
171                .iter()
172                .enumerate()
173                .any(|(j, other)| i != j && other == hash);
174            if !found {
175                for (other_part, theirs) in hashes.iter().enumerate() {
176                    if other_part == part {
177                        continue;
178                    }
179                    let other = theirs
180                        .get(i)
181                        .ok_or_else(|| Error::Omr("list index out of range".to_string()))?;
182                    if other == hash {
183                        found = true;
184                        break;
185                    }
186                }
187            }
188            if found {
189                matches += 1;
190            }
191        }
192    }
193    Ok((measures, matches))
194}