You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, thresholdOffset = "0.85", minLineLengthRatio = "0.4") {
// Parse parameters
const threshMult = parseFloat(thresholdOffset);
const lineRatio = parseFloat(minLineLengthRatio);
// Initial setup
const canvas = document.createElement('canvas');
canvas.width = originalImg.width;
canvas.height = originalImg.height;
const ctx = canvas.getContext('2d');
// Draw original sheet music
ctx.drawImage(originalImg, 0, 0);
const w = canvas.width;
const h = canvas.height;
// Safety check for empty dimensions
if (w === 0 || h === 0) return canvas;
const imgData = ctx.getImageData(0, 0, w, h);
const data = imgData.data;
// STEP 1: Grayscale and find Average Luminance
let sumLum = 0;
const lums = new Uint8Array(w * h);
for (let i = 0; i < w * h; i++) {
let r = data[i * 4];
let g = data[i * 4 + 1];
let b = data[i * 4 + 2];
let lum = 0.299 * r + 0.587 * g + 0.114 * b;
lums[i] = lum;
sumLum += lum;
}
const avgLum = sumLum / (w * h);
// Determine dynamic binarization threshold based on overall brightness
const binarizedThresh = avgLum * threshMult;
// STEP 2: Binarization and Horizontal Projection Profile
const isDark = new Uint8Array(w * h);
const rowCounts = new Uint32Array(h);
for (let y = 0; y < h; y++) {
let darkCount = 0;
for (let x = 0; x < w; x++) {
if (lums[y * w + x] < binarizedThresh) {
isDark[y * w + x] = 1;
darkCount++;
}
}
rowCounts[y] = darkCount;
}
// STEP 3: Detect Horizontal Lines (Staff Lines)
const thresholdCount = w * lineRatio;
const lines = [];
let inLine = false;
let startY = 0;
for (let y = 0; y < h; y++) {
if (rowCounts[y] > thresholdCount) {
if (!inLine) {
inLine = true;
startY = y;
}
} else {
if (inLine) {
inLine = false;
let endY = y - 1;
lines.push({ center: startY + (endY - startY) / 2, top: startY, bottom: endY });
}
}
}
if (inLine) {
let endY = h - 1;
lines.push({ center: startY + (endY - startY) / 2, top: startY, bottom: endY });
}
// STEP 4: Group lines into Staves (Sets of 5 roughly equidistant lines)
const staves = [];
for (let i = 0; i <= lines.length - 5; i++) {
let l1 = lines[i];
let l2 = lines[i+1];
let l3 = lines[i+2];
let l4 = lines[i+3];
let l5 = lines[i+4];
// Ensure consecutive spacing is similar
let space1 = l2.center - l1.center;
let space2 = l3.center - l2.center;
let space3 = l4.center - l3.center;
let space4 = l5.center - l4.center;
let avgSpace = (space1 + space2 + space3 + space4) / 4;
let expectedMaxDeviation = avgSpace * 0.4; // 40% Tolerance per spacing jump
let maxDeviation = Math.max(
Math.abs(space1 - avgSpace),
Math.abs(space2 - avgSpace),
Math.abs(space3 - avgSpace),
Math.abs(space4 - avgSpace)
);
// Sanity checks: At least 2px apart, and tightly grouped.
if (avgSpace >= 2 && maxDeviation < expectedMaxDeviation) {
staves.push({
yTop: l1.center,
yBottom: l5.center,
avgSpace: avgSpace
});
i += 4; // Jump ahead to not cross-contaminate next stave checks
}
}
// STEP 5: Visualizations & Drawing Context Overlays
staves.forEach((stave, staveIdx) => {
let pad = stave.avgSpace * 1.5;
let sTop = Math.max(0, Math.floor(stave.yTop - pad));
let sBottom = Math.min(h - 1, Math.floor(stave.yBottom + pad));
let sHeight = sBottom - sTop;
// Bounding Box for Stave
ctx.lineWidth = Math.max(2, w * 0.003);
ctx.strokeStyle = '#E83E8C'; // Magenta bounds
ctx.strokeRect(0, sTop, w, sHeight);
// Render Stave Labels
let fontSize = Math.max(14, stave.avgSpace * 2);
ctx.font = `bold ${fontSize}px sans-serif`;
ctx.fillStyle = '#E83E8C';
ctx.fillText(`Stave ${staveIdx + 1}`, 10, sTop - 5);
// Vertical Projection inner stave (To mock extracting Notes/Chords/Clefs)
let colCounts = new Uint32Array(w);
for (let x = 0; x < w; x++) {
let darkSum = 0;
for (let y = sTop; y <= sBottom; y++) {
darkSum += isDark[y * w + x];
}
colCounts[x] = darkSum;
}
let noteThreshold = stave.avgSpace * 0.8;
let inNote = false;
let startX = 0;
let notes = [];
for (let x = 0; x < w; x++) {
if (colCounts[x] > noteThreshold) {
if (!inNote) {
inNote = true;
startX = x;
}
} else {
if (inNote) {
inNote = false;
let endX = x - 1;
// Ignore noise elements smaller than a dot
if (endX - startX > w * 0.001) {
notes.push({ startX, endX });
}
}
}
}
if (inNote) {
notes.push({startX, endX: w-1});
}
// Draw Identified Data Nodes (Notes / Keys)
ctx.fillStyle = 'rgba(0, 123, 255, 0.35)'; // Semitransparent Blue overlay
ctx.strokeStyle = '#007BFF';
ctx.lineWidth = Math.max(1, w * 0.001);
notes.forEach(n => {
let nWidth = n.endX - n.startX;
// Bound check on overly wide elements imitating note blocks
if (nWidth < w * 0.4) {
ctx.fillRect(n.startX, sTop, nWidth, sHeight);
ctx.strokeRect(n.startX, sTop, nWidth, sHeight);
}
});
});
// STEP 6: Apply UI Information Overlay Header
let scaleF = Math.max(1, w / 800);
let headerH = 85 * scaleF;
let bWidth = Math.min(320 * scaleF, w);
ctx.fillStyle = 'rgba(20, 20, 20, 0.85)';
ctx.fillRect(0, 0, bWidth, headerH);
ctx.fillStyle = '#00FF41';
ctx.font = `bold ${22 * scaleF}px monospace`;
ctx.fillText(`♪ MUSIC OMR SUMMARY`, 15 * scaleF, 30 * scaleF);
ctx.fillStyle = '#FFF';
ctx.font = `${15 * scaleF}px monospace`;
ctx.fillText(`Detected Staves: ${staves.length}`, 15 * scaleF, 55 * scaleF);
ctx.fillText(`Detected Sys Lines: ${lines.length}`, 15 * scaleF, 75 * scaleF);
// STEP 7: Produce Scanner Container Output Element
const container = document.createElement('div');
container.style.position = 'relative';
container.style.display = 'inline-block';
container.style.maxWidth = '100%';
container.style.overflow = 'hidden';
container.style.border = '3px solid #333';
container.style.boxShadow = '0 10px 30px rgba(0,0,0,0.5)';
container.style.borderRadius = '5px';
canvas.style.display = 'block';
canvas.style.maxWidth = '100%';
canvas.style.height = 'auto';
// Scanline Animation Object
const scanline = document.createElement('div');
scanline.style.position = 'absolute';
scanline.style.top = '0';
scanline.style.left = '0';
scanline.style.width = '100%';
scanline.style.height = '4px';
scanline.style.backgroundColor = '#00FF41';
scanline.style.boxShadow = '0 0 20px 8px rgba(0, 255, 65, 0.5)';
scanline.style.zIndex = '10';
// Inject dynamic CSS once per page config
if (!document.getElementById('music-scanner-anim')) {
const style = document.createElement('style');
style.id = 'music-scanner-anim';
style.innerHTML = `
@keyframes scan-animation {
0% { top: 0%; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { top: 100%; opacity: 0; }
}
`;
document.head.appendChild(style);
}
scanline.style.animation = 'scan-animation 3s infinite linear';
container.appendChild(canvas);
container.appendChild(scanline);
return container;
}
Apply Changes