I found a solution that’s likely to be not the most elegant, but it seems to work.
Here’s the ofApp.cpp code for anyone interested:
#include "ofApp.h"
//--------------------------------------------------------------
void ofApp::setup(){
// ofxOpenCV doesn't have image loading.
// So first, load the .png file into a temporary ofImage.
ofImage scoreOfImage;
scoreOfImage.loadImage("test_score.png");
scoreOfImage.setImageType(OF_IMAGE_GRAYSCALE);
ofImage scoreOfImage2;
scoreOfImage2.loadImage("test_score2.png");
scoreOfImage2.setImageType(OF_IMAGE_GRAYSCALE);
// Set the ofxCvImage from the pixels of this ofImage.
imgW = scoreOfImage.getWidth();
imgH = scoreOfImage.getHeight();
unsigned char *scorePixels = scoreOfImage.getPixels().getData();
grayImage.setFromPixels(scorePixels, imgW, imgH);
int imgW2 = scoreOfImage2.getWidth();
int imgH2 = scoreOfImage2.getHeight();
unsigned char *scorePixels2 = scoreOfImage2.getPixels().getData();
grayBg.setFromPixels(scorePixels2, imgW2, imgH2);
grayDiff.allocate(imgW, imgH);
grayDiff.absDiff(grayBg, grayImage);
grayDiff.threshold(30);
contourFinder.findContours(grayDiff, 2, (imgW*imgH)/4, 96, false, true);
ofSetWindowShape((imgW* 2), imgH);
ofSetWindowPosition(10, 10);
ofRectangle allRects[contourFinder.nBlobs];
for (int i = 0; i < contourFinder.nBlobs; i++) {
allRects[i] = contourFinder.blobs.at(i).boundingRect;
}
rectHeight = 30;
// use + (with shift pressed) and - to set a differnt rectOffset
rectOffset = 35;
// overflown 2D array to hold the indices of Y coords
// to be sure we'll track them all, we use nBlobs for both dimensions
yIdx = new int * [contourFinder.nBlobs];
for (int i = 0; i < contourFinder.nBlobs; i++) {
yIdx[i] = new int [contourFinder.nBlobs];
}
yIdxSizes = new int [contourFinder.nBlobs];
allYs = new int [contourFinder.nBlobs];
for (int i = 0; i < contourFinder.nBlobs; i++) {
allYs[i] = allRects[i].y;
yIdxSizes[i] = 0;
}
// this will tell us how many rows have data
numRows = 0;
// set the number of pixels the contours should be far away from each other
// to belong to the same group
thresh = 50;
int iOffset = 0;
// run through all Y coords and group their indices according to their position
for (int i = 0; i < contourFinder.nBlobs; i++) {
int thisY = allYs[i];
int colIdx = 0;
bool idxExists = false;
for (int j = 0; j < contourFinder.nBlobs; j++) {
if (i) {
for (int k = 0; k < i; k++) {
for (int l = 0; l < yIdxSizes[k]; l++) {
if (j == yIdx[k][l]) {
idxExists = true;
break;
}
else {
idxExists = false;
}
}
if (idxExists) {
break;
}
}
}
if (idxExists) {
continue;
}
else if (abs(thisY - allYs[j]) < thresh) {
yIdx[i+iOffset][colIdx++] = j;
yIdxSizes[i+iOffset]++;
numRows = (i+iOffset) + 1;
}
}
// if the current Y coord is the same as the previous one
// we'll end up with a row of 0 size
// in this case offset i by -1 to store the next values
// to the row that got nothing
if (yIdxSizes[i+iOffset] == 0) {
iOffset--;
}
}
// once groupped, sort all the X coords of each group
int maxYsize = 0;
for (int i = 0; i < numRows; i++) {
if (yIdxSizes[i] > maxYsize) {
maxYsize = yIdxSizes[i];
}
}
xSorted = new int * [numRows];
for (int i = 0; i < numRows; i++) {
xSorted[i] = new int [maxYsize];
}
for (int i = 0; i < numRows; i++) {
for (int j = 0; j < yIdxSizes[i]; j++) {
xSorted[i][j] = allRects[yIdx[i][j]].x;
}
}
for (int i = 0; i < numRows; i++) {
sort(xSorted[i], xSorted[i] + yIdxSizes[i]);
}
ySorted = new int [numRows];
for (int i = 0; i < numRows; i++) {
ySorted[i] = allYs[yIdx[i][0]];
}
sort(ySorted, ySorted + numRows);
// make a temporary copy of the x-coords so we can sort them by y-coord
int tempXsorted[numRows][maxYsize];
// we need to copy the size of each array in the 2D array as well
int tempYidxSizes[numRows];
// create an array to hold the indices in order
int idxInOrder[numRows];
for (int i = 0; i < numRows; i++) {
for (int j = 0; j < yIdxSizes[i]; j++) {
tempXsorted[i][j] = xSorted[i][j];
}
tempYidxSizes[i] = yIdxSizes[i];
}
for (int i = 0; i < numRows; i++) {
for (int j = 0; j < numRows; j++) {
// since the first (grey) note might not be an f, but an e or g
// better make a difference comparison
if (abs(ySorted[i] - allYs[yIdx[j][0]]) < 50) {
idxInOrder[i] = j;
break;
}
}
}
for (int i = 0; i < numRows; i++) {
yIdxSizes[i] = tempYidxSizes[idxInOrder[i]];
for (int j = 0; j < yIdxSizes[i]; j++) {
xSorted[i][j] = tempXsorted[idxInOrder[i]][j];
}
}
for (int i = 0; i < numRows; i++) {
// add a steady offset
ySorted[i] -= rectOffset;
}
whichStave = 0;
nextBar = 0;
beginning = xSorted[whichStave][nextBar];
step = (xSorted[whichStave][nextBar+1] - beginning) / 3;
pos = beginning;
pos += 5;
timeRef = ofGetElapsedTimeMillis();
timeDiff = 125;
idx = 0;
drawRect = 1;
halfTime = 0;
}
//--------------------------------------------------------------
void ofApp::update(){
timeStamp = ofGetElapsedTimeMillis();
if ((timeStamp - timeRef) > timeDiff) {
halfTime++;
halfTime %= 2;
if (!halfTime) {
idx++;
if (idx >= 4) {
idx = 0;
nextBar += 2;
if (nextBar >= yIdxSizes[whichStave]) {
nextBar = 0;
whichStave++;
if (whichStave >= numRows) {
whichStave = 0;
}
}
beginning = xSorted[whichStave][nextBar];
step = (xSorted[whichStave][nextBar+1] - beginning) / 3;
}
pos = beginning + (step * idx);
pos += 5;
}
drawRect++;
drawRect %= 2;
timeRef = ofGetElapsedTimeMillis();
}
}
//--------------------------------------------------------------
void ofApp::draw(){
ofSetHexColor(0xffffff);
grayBg.draw(0, 0, imgW, imgH);
contourFinder.draw(imgW, 0, imgW, imgH);
ofColor c(255, 255, 255);
ofRectangle r(imgW, 0, imgW, imgH);
ofSetColor(c);
ofDrawRectangle(r);
//ofColor c(255, 0, 0);
c.r = 255;
c.g = 0;
c.b = 0;
ofSetColor(c);
//ofRectangle indicator = contourFinder.blobs.at(idx).boundingRect;
ofRectangle indicator(pos, ySorted[whichStave], 5, rectHeight);
//ofSetColor(c);
if (drawRect > 0) {
ofDrawRectangle(indicator);
}
}
I’m creating two different scores, one with the first and last note of each bar in grey, and one in just black and white. For this to actually work, it would be best for the grey notes and their black counter parts to be the same note every time (in this case it’s F), to provide consistency in the height of the contours.
If the score contains a different note at the beginning and the end of every bar, three scores should be created: the original, and two with Fs (probably replacing the original note, or adding an F alongside the original) at the beginning and end of each bar, one with greyed notes and one with black.
contourFinder should be able to detect number of bars times two contours.