Please bookmark this page to avoid losing your image tool!

Music Scanner Identifier Tool

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
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;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Music Scanner Identifier Tool is designed to analyze images of sheet music to detect and highlight musical elements. Using image processing techniques, the tool identifies horizontal staff lines and groups them into staves, while also detecting musical notes or chords within those staves. It provides a visual summary of the detected musical structure, which can be useful for musicians, music students, or developers working on Optical Music Recognition (OMR) applications to verify the accuracy of automated sheet music scanning.

Leave a Reply

Your email address will not be published. Required fields are marked *