org.audiveris.omr.sheet.grid.StaffProjector.java Source code

Java tutorial

Introduction

Here is the source code for org.audiveris.omr.sheet.grid.StaffProjector.java

Source

//------------------------------------------------------------------------------------------------//
//                                                                                                //
//                                   S t a f f P r o j e c t o r                                  //
//                                                                                                //
//------------------------------------------------------------------------------------------------//
// <editor-fold defaultstate="collapsed" desc="hdr">
//
//  Copyright  Herv Bitteur and others 2000-2017. All rights reserved.
//
//  This program is free software: you can redistribute it and/or modify it under the terms of the
//  GNU Affero General Public License as published by the Free Software Foundation, either version
//  3 of the License, or (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
//  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//  See the GNU Affero General Public License for more details.
//
//  You should have received a copy of the GNU Affero General Public License along with this
//  program.  If not, see <http://www.gnu.org/licenses/>.
//------------------------------------------------------------------------------------------------//
// </editor-fold>
package org.audiveris.omr.sheet.grid;

import ij.process.ByteProcessor;

import org.audiveris.omr.constant.ConstantSet;
import org.audiveris.omr.math.AreaUtil;
import org.audiveris.omr.math.AreaUtil.CoreData;
import org.audiveris.omr.math.GeoPath;
import org.audiveris.omr.math.Projection;
import org.audiveris.omr.sheet.Picture;
import org.audiveris.omr.sheet.Scale;
import org.audiveris.omr.sheet.Scale.InterlineScale;
import org.audiveris.omr.sheet.Sheet;
import org.audiveris.omr.sheet.Staff;
import org.audiveris.omr.sheet.grid.StaffPeak.Attribute;
import org.audiveris.omr.sig.GradeImpacts;
import org.audiveris.omr.sig.inter.BarlineInter;
import org.audiveris.omr.sig.inter.Inter;
import org.audiveris.omr.ui.Colors;
import org.audiveris.omr.util.HorizontalSide;
import static org.audiveris.omr.util.HorizontalSide.*;
import org.audiveris.omr.util.Navigable;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartFrame;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

import org.jgrapht.Graph;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.Color;
import java.awt.Point;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

import javax.swing.WindowConstants;

/**
 * Class {@code StaffProjector} is in charge of analyzing a staff projection onto
 * x-axis, in order to retrieve barlines candidates as well as staff start and stop
 * abscissae.
 * <p>
 * To retrieve bar lines candidates, we analyze the vertical interior of staff because this is where
 * a barline must be present.
 * The potential bar portions outside staff height are much less typical of a barline.
 * <p>
 * A peak in staff projection can result from:<ol>
 * <li>A thick or thin <b>bar line</b>:<br>
 * <img alt="Image of bar lines"
 * src="http://upload.wikimedia.org/wikipedia/commons/thumb/c/c0/Barlines.svg/400px-Barlines.svg.png">
 *
 * <li>A <b>bracket</b> portion:<br>
 * <img alt="Image of bracket"  width="250" height="216"
 * src="http://donrathjr.com/wp-content/uploads/2010/08/Brackets-and-Braces-4a.png">
 *
 * <li>A <b>brace</b> portion:<br>
 * <img alt="Image of brace"
 * src="http://upload.wikimedia.org/wikipedia/commons/thumb/2/28/Brace_(music).png/240px-Brace_(music).png">
 *
 * <li>An Alto <b>C-clef</b> portion:<br>
 * <img alt="Image of alto clef"
 * src="http://upload.wikimedia.org/wikipedia/commons/thumb/6/68/Alto_clef_with_ref.svg/90px-Alto_clef_with_ref.svg.png">
 * <br>
 * Such C-clef artifacts are detected later, based on their abscissa offset from the measure
 * start (be it bar-based start or lines-only start).
 *
 * <li>A <b>stem</b> (with note heads located outside the staff height).
 * <li>Just <b>garbage</b>.
 * </ol>
 * <p>
 * Before this class is used, staves are only defined by their lines made of long horizontal
 * sections.
 * This gives a good vertical definition (sufficient to allow the x-axis projection) but a very poor
 * horizontal definition.
 * To retrieve precise staff start and stop abscissae, the projection can tell which abscissa values
 * are outside the staff abscissa range, since the lack of staff lines results in a projection value
 * close to zero.
 * <p>
 * The projection also gives indication about lack of chunk (beam or head) on each side of a bar
 * candidate, but this indication is very weak and limited to the staff height portion.
 *
 * @author Herv Bitteur
 */
public class StaffProjector {
    //~ Static fields/initializers -----------------------------------------------------------------

    private static final Constants constants = new Constants();

    private static final Logger logger = LoggerFactory.getLogger(StaffProjector.class);

    //~ Instance fields ----------------------------------------------------------------------------
    /** Underlying sheet. */
    @Navigable(false)
    private final Sheet sheet;

    /** Related scale. */
    private final Scale scale;

    /** Scale-dependent parameters. */
    private final Parameters params;

    /** Staff to analyze. */
    private final Staff staff;

    /** Pixel source. */
    private final ByteProcessor pixelFilter;

    /** Sequence of all blank regions found, whatever their width. */
    private final List<Blank> allBlanks = new ArrayList<Blank>();

    /** Selected (wide) ending blank region on each staff side. */
    private final Map<HorizontalSide, Blank> endingBlanks = new EnumMap<HorizontalSide, Blank>(
            HorizontalSide.class);

    /** Sequence of peaks found. */
    private final List<StaffPeak> peaks = new ArrayList<StaffPeak>();

    /** (Unmodifiable) view on peaks. */
    private final List<StaffPeak> peaksView = Collections.unmodifiableList(peaks);

    /** Graph of all peaks, linked by alignment/connection. */
    private final Graph<StaffPeak, BarAlignment> peakGraph;

    /** Count of cumulated foreground pixels, indexed by abscissa. */
    private Projection projection;

    /** Initial brace peak, if any. */
    private StaffPeak bracePeak;

    //~ Constructors -------------------------------------------------------------------------------
    /**
     * Creates a new {@code StaffProjector} object.
     *
     * @param sheet     containing sheet
     * @param staff     staff to analyze
     * @param peakGraph sheet graph of peaks
     */
    public StaffProjector(Sheet sheet, Staff staff, PeakGraph peakGraph) {
        this.sheet = sheet;
        this.staff = staff;
        this.peakGraph = peakGraph;

        Picture picture = sheet.getPicture();
        pixelFilter = picture.getSource(Picture.SourceKey.BINARY);

        scale = sheet.getScale();
        params = new Parameters(scale, staff.getSpecificInterline());
    }

    //~ Methods ------------------------------------------------------------------------------------
    //----------------//
    // checkLinesRoot //
    //----------------//
    /**
     * Check for presence of lines roots right before first bar.
     * <p>
     * We cannot rely on current lines definition, since they are based only on long chunks.
     * Hence we use staff projection which, when no brace is present, should be below lines
     * threshold for some abscissa range.
     * <p>
     * If not, this means some portion of lines is present, hence the (group of) peaks found are not
     * bars (but perhaps peaks of C-Clef) so there is no start bar and staff left abscissa must be
     * defined by lines roots.
     */
    public void checkLinesRoot() {
        if ((getBracePeak() != null) || peaks.isEmpty()) {
            return;
        }

        try {
            final int iStart = getStartPeakIndex();

            if (iStart != -1) {
                final StaffPeak firstPeak = peaks.get(0);

                // There must be a significant blank just before first peak
                Blank blank = selectBlank(LEFT, firstPeak.getStart(), params.minSmallBlankWidth);

                if (blank != null) {
                    int gap = firstPeak.getStart() - 1 - blank.stop;

                    if (gap > params.maxLeftExtremum) {
                        // Root portion found, so unset start peak and define true line start.
                        peaks.get(iStart).unset(Attribute.STAFF_LEFT_END);
                        staff.setAbscissa(LEFT, blank.stop + 1);
                    }
                } else {
                    logger.warn("Staff#{} no clear end on LEFT", staff.getId());
                }
            }
        } catch (Exception ex) {
            logger.warn("Error in checkLinesRoot on staff#{} {}", staff.getId(), ex.toString(), ex);
        }
    }

    //---------------//
    // findBracePeak //
    //---------------//
    /**
     * Try to find a brace-compatible peak on left side of provided abscissa.
     *
     * @param minLeft  provided minimum abscissa on left
     * @param maxRight provided maximum abscissa on right
     * @return a brace peak, or null
     */
    public StaffPeak findBracePeak(int minLeft, int maxRight) {
        final int minValue = params.braceThreshold;
        final Blank leftBlank = endingBlanks.get(LEFT);
        final int xMin;

        if (leftBlank != null) {
            if ((leftBlank.stop + 2) >= maxRight) { // +2 to cope with blank-peak gap
                // Large blank just before bar, look even farther on left
                maxRight = leftBlank.start - 1;

                Blank prevBlank = selectBlank(LEFT, maxRight, params.minWideBlankWidth);

                if (prevBlank != null) {
                    xMin = prevBlank.stop;
                } else {
                    xMin = minLeft;
                }
            } else {
                xMin = Math.max(minLeft, leftBlank.stop);
            }
        } else {
            xMin = Math.max(minLeft, 0);
        }

        int braceStop = -1;
        int braceStart = -1;
        int bestValue = 0;
        boolean valleyHit = false;

        // Browse from right to left
        // First finding valley left of bar, then brace peak if any
        for (int x = maxRight; x >= xMin; x--) {
            int value = projection.getValue(x);

            if (value >= minValue) {
                if (!valleyHit) {
                    continue;
                }

                if (braceStop == -1) {
                    braceStop = x;
                }

                braceStart = x;
                bestValue = Math.max(bestValue, value);
            } else if (!valleyHit) {
                valleyHit = true;
            } else if (braceStop != -1) {
                return createBracePeak(braceStart, braceStop, maxRight);
            }
        }

        // Brace peak on going (stuck on left side of image)?
        if (braceStart >= 0) {
            return createBracePeak(braceStart, braceStop, maxRight);
        }

        return null;
    }

    //--------------//
    // getBracePeak //
    //--------------//
    /**
     * @return the bracePeak
     */
    public StaffPeak getBracePeak() {
        return bracePeak;
    }

    //-------------//
    // getLastPeak //
    //-------------//
    /**
     * Report the last peak in peaks sequence
     *
     * @return last peak or null
     */
    public StaffPeak getLastPeak() {
        if (peaks.isEmpty()) {
            return null;
        }

        return peaks.get(peaks.size() - 1);
    }

    //----------//
    // getPeaks //
    //----------//
    /**
     * Get a view on projector peaks.
     *
     * @return the (unmodifiable) list of peaks
     */
    public List<StaffPeak> getPeaks() {
        return peaksView;
    }

    //----------//
    // getStaff //
    //----------//
    /**
     * Report the underlying staff for this projector.
     *
     * @return the staff
     */
    public Staff getStaff() {
        return staff;
    }

    //-------------------//
    // getStartPeakIndex //
    //-------------------//
    /**
     * Report the index of the start peak, if any
     *
     * @return start peak index, or -1 if none
     */
    public int getStartPeakIndex() {
        for (int i = 0; i < peaks.size(); i++) {
            if (peaks.get(i).isStaffEnd(LEFT)) {
                return i;
            }
        }

        return -1;
    }

    //------------------//
    // hasStandardBlank //
    //------------------//
    /**
     * Check whether there is a blank of at least standard width, within the provided
     * abscissa range.
     *
     * @param start range start
     * @param stop  range stop
     * @return true if standard blank was found
     */
    public boolean hasStandardBlank(int start, int stop) {
        if (stop <= start) {
            return false;
        }

        Blank blank = selectBlank(RIGHT, start, params.minStandardBlankWidth);

        return (blank != null) && (blank.start <= stop);
    }

    //------------//
    // insertPeak //
    //------------//
    /**
     * Insert a new peak right before an existing one.
     *
     * @param toInsert the new peak to insert
     * @param before   the existing peak before which insertion must be done
     */
    public void insertPeak(StaffPeak toInsert, StaffPeak before) {
        int index = peaks.indexOf(before);

        if (index == -1) {
            throw new IllegalArgumentException("insertPeak() before a non-existing peak");
        }

        peaks.add(index, toInsert);
        peakGraph.addVertex(toInsert);
    }

    //------//
    // plot //
    //------//
    /**
     * Display a chart of the projection.
     */
    public void plot() {
        if (projection == null) {
            computeProjection();
            computeLineThresholds();
        }

        new Plotter().plot();
    }

    //---------//
    // process //
    //---------//
    /**
     * Process the staff projection on x-axis to retrieve peaks that may represent bars.
     */
    public void process() {
        logger.debug("StaffProjector analyzing staff#{}", staff.getId());

        // Cumulate pixels for each abscissa
        computeProjection();

        // Adjust thresholds according to actual line thicknesses in this staff
        computeLineThresholds();

        // Retrieve all regions without staff lines
        findAllBlanks();

        // Select the wide blanks that limit staff search in abscissa
        selectEndingBlanks();

        // Retrieve peaks as barline raw candidates
        findPeaks();
    }

    //----------------//
    // refineRightEnd //
    //----------------//
    /**
     * Try to use the extreme peak on staff right side, to refine the precise abscissa
     * where the staff ends.
     * <p>
     * When this method is called, the staff sides are defined only by the ends of the lines built
     * with long sections.
     * An extreme peak can be used as abscissa reference only if it is either beyond current staff
     * end or sufficiently close to the end.
     * If no such peak is found, we stop right before the blank region assuming that this is a
     * measure with no outside bar.
     */
    public void refineRightEnd() {
        final int linesEnd = staff.getAbscissa(RIGHT); // As defined by end of long staff sections
        int staffEnd = linesEnd;
        StaffPeak endPeak = null;
        Integer peakEnd = null;

        // Look for a suitable peak
        if (!peaks.isEmpty()) {
            StaffPeak peak = peaks.get(peaks.size() - 1);

            if (peak != null) {
                // Check side position of peak wrt staff, it must be external
                final int peakMid = (peak.getStart() + peak.getStop()) / 2;
                final int toPeak = peakMid - linesEnd;

                if (toPeak >= 0) {
                    endPeak = peak;
                    peakEnd = endPeak.getStop();
                    staffEnd = peakEnd;
                }
            }
        }

        // Continue and stop at first small blank region encountered or image limit.
        // Then keep the additional line chunk if long enough.
        // If not, use peak mid as staff end.
        final Blank blank = selectBlank(RIGHT, staffEnd, params.minSmallBlankWidth);
        final int xMax = (blank != null) ? (blank.start - 1) : (sheet.getWidth() - 1);

        if (endPeak != null) {
            if ((xMax - peakEnd) > params.maxRightExtremum) {
                // We have significant line chunks beyond bar, hence peak is not the limit
                logger.debug("Staff#{} RIGHT set at blank {} (vs {})", staff.getId(), xMax, linesEnd);
                staff.setAbscissa(RIGHT, xMax);
            } else {
                // No significant line chunks, ignore them and stay with peak as the limit
                final int peakMid = (endPeak.getStart() + endPeak.getStop()) / 2;
                logger.debug("Staff#{} RIGHT set at peak {} (vs {})", staff.getId(), peakMid, linesEnd);
                staff.setAbscissa(RIGHT, peakMid);
                endPeak.setStaffEnd(RIGHT);
            }
        } else {
            logger.debug("Staff#{} RIGHT set at blank {} (vs {})", staff.getId(), xMax, linesEnd);
            staff.setAbscissa(RIGHT, xMax);
        }
    }

    //------------//
    // removePeak //
    //------------//
    /**
     * Remove a peak from the sequence of peaks.
     *
     * @param peak the peak to remove
     */
    public void removePeak(StaffPeak peak) {
        if (peak.isVip()) {
            logger.info("VIP {} removing {}", this, peak);
        }

        peaks.remove(peak);
        peakGraph.removeVertex(peak);
    }

    //-------------//
    // removePeaks //
    //-------------//
    /**
     * Remove some peaks from the sequence of peaks.
     *
     * @param toRemove the peaks to remove
     */
    public void removePeaks(Collection<? extends StaffPeak> toRemove) {
        for (StaffPeak peak : toRemove) {
            removePeak(peak);
        }
    }

    //--------------//
    // setBracePeak //
    //--------------//
    /**
     * @param bracePeak the bracePeak to set
     */
    public void setBracePeak(StaffPeak bracePeak) {
        this.bracePeak = bracePeak;
    }

    //----------//
    // toString //
    //----------//
    @Override
    public String toString() {
        return "StaffProjector#" + staff.getId();
    }

    //-------------//
    // browseRange //
    //-------------//
    /**
     * (Try to) create one or more relevant peaks at provided range.
     * <p>
     * This is governed by derivative peaks.
     * For the time being, this is just a wrapper on top of createPeak meant to address the case of
     * wide ranges above the bar threshold, which need to be further split.
     *
     * @param rangeStart starting abscissa of range
     * @param rangeStop  stopping abscissa of range
     * @return the sequence of created peak instances, perhaps empty
     */
    private List<StaffPeak> browseRange(final int rangeStart, final int rangeStop) {
        logger.debug("Staff#{} browseRange [{}..{}]", staff.getId(), rangeStart, rangeStop);

        final List<StaffPeak> list = new ArrayList<StaffPeak>();
        int start = rangeStart;
        int stop;

        for (int x = rangeStart; x <= rangeStop; x++) {
            final int der = projection.getDerivative(x);

            if (der >= params.minDerivative) {
                int maxDer = der;

                for (int xx = x + 1; xx <= rangeStop; xx++) {
                    int xxDer = projection.getDerivative(xx);

                    if (xxDer > maxDer) {
                        maxDer = xxDer;
                        x = xx;
                    } else {
                        break;
                    }
                }

                start = x;
            } else if (der <= -params.minDerivative) {
                int minDer = der;

                for (int xx = x + 1; xx <= xClamp(rangeStop + 1); xx++) {
                    int xxDer = projection.getDerivative(xx);

                    if (xxDer <= minDer) {
                        minDer = xxDer;
                        x = xx;
                    } else {
                        break;
                    }
                }

                if (x == rangeStop) {
                    x = rangeStop + 1;
                }

                stop = x;

                if ((start != -1) && (start < stop)) {
                    StaffPeak peak = createPeak(start, stop - 1);

                    if (peak != null) {
                        list.add(peak);
                    }

                    start = -1;
                }
            }
        }

        // A last peak?
        if (start != -1) {
            StaffPeak peak = createPeak(start, rangeStop);

            if (peak != null) {
                list.add(peak);
            }
        }

        return list;
    }

    //---------------------------//
    // computeCoreLinesThickness //
    //---------------------------//
    /**
     * Using the current definition on staff lines (made of only long filaments so far)
     * estimate the cumulated staff line thicknesses for the staff.
     * <p>
     * NOTA: Since we may have holes in lines, and short sections have been left apart, the
     * measurement is under-estimated.
     *
     * @return the estimate of cumulated lines heights
     */
    private double computeCoreLinesThickness() {
        double linesHeight = 0;

        for (LineInfo line : staff.getLines()) {
            linesHeight += line.getThickness();
        }

        logger.debug("Staff#{} linesHeight: {}", staff.getId(), linesHeight);

        return linesHeight;
    }

    //-----------------------//
    // computeLineThresholds //
    //-----------------------//
    /**
     * Compute thresholds that closely depend on actual line thickness in this staff.
     */
    private void computeLineThresholds() {
        final double linesCumul = computeCoreLinesThickness();
        final double lineThickness = linesCumul / staff.getLines().size();

        params.linesThreshold = (int) Math.rint(linesCumul);
        params.blankThreshold = (int) Math.rint(constants.blankThreshold.getValue() * lineThickness);
        params.chunkThreshold = (4 * scale.getMaxFore())
                + InterlineScale.toPixels(staff.getSpecificInterline(), constants.chunkThreshold);
        logger.debug("Staff#{} linesThreshold:{} chunkThreshold:{}", staff.getId(), params.linesThreshold,
                params.chunkThreshold);
    }

    //-------------------//
    // computeProjection //
    //-------------------//
    /**
     * Compute, for each abscissa value, the foreground pixels cumulated between
     * first line and last line of staff.
     */
    private void computeProjection() {
        projection = new Projection.Short(0, sheet.getWidth() - 1);

        final LineInfo firstLine = staff.getFirstLine();
        final LineInfo lastLine = staff.getLastLine();
        final int dx = params.staffAbscissaMargin;
        final int xMin = xClamp(staff.getAbscissa(LEFT) - dx);
        final int xMax = xClamp(staff.getAbscissa(RIGHT) + dx);

        for (int x = xMin; x <= xMax; x++) {
            int yMin = firstLine.yAt(x);
            int yMax = lastLine.yAt(x);
            short count = 0;

            for (int y = yMin; y <= yMax; y++) {
                if (pixelFilter.get(x, y) == 0) {
                    count++;
                }
            }

            projection.increment(x, count);
        }
    }

    //-----------------//
    // createBracePeak //
    //-----------------//
    /**
     * Precisely define the bounds of a brace candidate peak.
     *
     * @param rawStart starting abscissa at peak threshold
     * @param rawStop  stopping abscissa at peak threshold
     * @param maxRight maximum abscissa on right
     * @return a peak with proper abscissa values, or null
     */
    private StaffPeak createBracePeak(int rawStart, int rawStop, int maxRight) {
        // Extend left abscissa until a blank (no-staff) or image left side is reached
        Blank leftBlank = null;

        for (Blank blank : allBlanks) {
            if (blank.stop >= rawStart) {
                break;
            }

            leftBlank = blank;
        }

        int start = (leftBlank != null) ? leftBlank.stop : rawStart;
        int val = projection.getValue(start);

        for (int x = start - 1; x >= 0; x--) {
            int nextVal = projection.getValue(x);

            if (nextVal < val) {
                val = nextVal;
                start = x;
            } else {
                break;
            }
        }

        // Perhaps there is no real blank between brace and bar, so use lowest point in valley
        int bestVal = Integer.MAX_VALUE;

        int stop = -1;

        for (int x = rawStop; x <= maxRight; x++) {
            val = projection.getValue(x);

            if (val < bestVal) {
                bestVal = val;
                stop = x;
            }
        }

        if (stop == -1) {
            return null;
        }

        final int xMid = (start + stop) / 2;

        final int yTop = staff.getFirstLine().yAt(xMid);

        final int yBottom = staff.getLastLine().yAt(xMid);

        StaffPeak brace = new StaffPeak(staff, yTop, yBottom, start, stop, null);

        brace.set(Attribute.BRACE);

        brace.computeDeskewedCenter(sheet.getSkew());

        return brace;
    }

    //------------//
    // createPeak //
    //------------//
    /**
     * (Try to) create a relevant peak at provided location.
     *
     * @param rawStart raw starting abscissa of peak
     * @param rawStop  raw stopping abscissa of peak
     * @return the created peak instance or null if failed
     */
    private StaffPeak createPeak(final int rawStart, final int rawStop) {
        final int minValue = params.barThreshold;
        final int totalHeight = 4 * staff.getSpecificInterline();
        final double valueRange = totalHeight - minValue;

        // Compute precise start & stop abscissae
        PeakSide newStart = refinePeakSide(rawStart, rawStop, -1);

        if (newStart == null) {
            return null;
        }

        final int start = newStart.abscissa;
        PeakSide newStop = refinePeakSide(rawStart, rawStop, +1);

        if (newStop == null) {
            return null;
        }

        final int stop = newStop.abscissa;

        // Check peak width is not huge
        if ((stop - start + 1) > params.maxBarWidth) {
            return null;
        }

        // Retrieve highest value
        int value = 0;

        for (int x = start; x <= stop; x++) {
            value = Math.max(value, projection.getValue(x));
        }

        // Compute largest white gap
        final int xMid = (start + stop) / 2;
        final int yTop = staff.getFirstLine().yAt(xMid);
        final int yBottom = staff.getLastLine().yAt(xMid);

        // If peak is very thin, thicken the lookup area
        final int width = stop - start + 1;
        final int dx = (width <= 2) ? 1 : 0;
        GeoPath leftLine = new GeoPath(new Line2D.Double(start - dx, yTop, start - dx, yBottom));
        GeoPath rightLine = new GeoPath(new Line2D.Double(stop + dx, yTop, stop + dx, yBottom));
        final CoreData data = AreaUtil.verticalCore(pixelFilter, leftLine, rightLine);

        if (data.gap > params.gapThreshold) {
            return null;
        }

        // Compute black core & impacts
        double coreImpact = (value - minValue) / valueRange;
        double gapImpact = 1 - ((double) data.gap / params.gapThreshold);
        GradeImpacts impacts = new BarlineInter.Impacts(coreImpact, gapImpact, newStart.grade, newStop.grade);
        double grade = impacts.getGrade();

        if (grade >= Inter.minGrade) {
            StaffPeak bar = new StaffPeak(staff, yTop, yBottom, start, stop, impacts);
            bar.computeDeskewedCenter(sheet.getSkew());
            logger.debug("Staff#{} {}", staff.getId(), bar);

            return bar;
        }

        return null;
    }

    //---------------//
    // findAllBlanks //
    //---------------//
    /**
     * Look for all "blank" regions (regions without staff lines).
     */
    private void findAllBlanks() {
        final int maxValue = params.blankThreshold;
        final int sheetWidth = sheet.getWidth();

        int start = -1;
        int stop = -1;

        for (int x = 0; x < sheetWidth; x++) {
            if (projection.getValue(x) <= maxValue) {
                // No line detected
                if (start == -1) {
                    start = x;
                }

                stop = x;
            } else if (start != -1) {
                allBlanks.add(new Blank(start, stop));
                start = -1;
            }
        }

        // Finish ongoing region if any
        if (start != -1) {
            allBlanks.add(new Blank(start, stop));
        }

        logger.debug("Staff#{} left:{} right:{} allBlanks:{}", staff.getId(), staff.getAbscissa(LEFT),
                staff.getAbscissa(RIGHT), allBlanks);
    }

    //-----------//
    // findPeaks //
    //-----------//
    /**
     * Retrieve the relevant (bar line) peaks in the staff projection.
     * This populates the 'peaks' sequence.
     */
    private void findPeaks() {
        final int minValue = params.barThreshold;

        final Blank leftBlank = endingBlanks.get(LEFT);
        final int xMin = (leftBlank != null) ? leftBlank.stop : 0;

        final Blank rightBlank = endingBlanks.get(RIGHT);
        final int xMax = (rightBlank != null) ? rightBlank.start : (sheet.getWidth() - 1);

        int start = -1;
        int stop = -1;

        for (int x = xMin; x <= xMax; x++) {
            int value = projection.getValue(x);

            if (value >= minValue) {
                if (start == -1) {
                    start = x;
                }

                stop = x;
            } else if (start != -1) {
                for (StaffPeak peak : browseRange(start, stop)) {
                    peaks.add(peak);
                    peakGraph.addVertex(peak);

                    // Make sure peaks do not overlap
                    x = Math.max(x, peak.getStop());
                }

                start = -1;
            }
        }

        // Finish ongoing peak if any (case of a peak stuck to right side of image)
        if (start != -1) {
            StaffPeak peak = createPeak(start, stop);

            if (peak != null) {
                peaks.add(peak);
                peakGraph.addVertex(peak);
            }
        }

        logger.debug("Staff#{} peaks:{}", staff.getId(), peaks);
    }

    //----------------//
    // refinePeakSide //
    //----------------//
    /**
     * Use extrema of first derivative to refine peak side abscissa.
     * Maximum for left side, minimum for right side.
     * Absolute derivative value indicates if the peak side is really steep: this should exclude
     * most of: braces, arpeggiato, stems with heads on left or right side.
     * <p>
     * Update: the test on derivative absolute value is not sufficient to discard braces.
     *
     * @param xStart raw abscissa that starts peak
     * @param xStop  raw abscissa that stops peak
     * @param dir    -1 for going left, +1 for going right
     * @return the best peak side, or null if none
     */
    private PeakSide refinePeakSide(int xStart, int xStop, int dir) {
        // Additional check range
        final int dx = params.barRefineDx;

        // Beginning and ending x values
        final double mid = (xStop + xStart) / 2.0;
        final int x1 = (dir > 0) ? (int) Math.ceil(mid) : (int) Math.floor(mid);
        final int x2 = (dir > 0) ? xClamp(xStop + dx) : xClamp(xStart - dx);

        int bestDer = 0; // Best derivative so far
        Integer bestX = null; // Abscissa at best derivative

        for (int x = x1; (dir * (x2 - x)) >= 0; x += dir) {
            final int der = projection.getDerivative(x);

            if ((dir * (bestDer - der)) > 0) {
                bestDer = der;
                bestX = x;
            }
        }

        bestDer = Math.abs(bestDer);

        if ((bestDer >= params.minDerivative) && (bestX != null)) {
            int x = (dir > 0) ? (bestX - 1) : bestX;
            double derImpact = (double) bestDer / (params.barThreshold - params.minDerivative);

            return new PeakSide(x, derImpact);
        } else {
            // Perhaps we have reached image border?
            int border = (dir > 0) ? (sheet.getWidth() - 1) : 0;

            if (x2 == border) {
                final int der = projection.getValue(border);

                if (der >= params.minDerivative) {
                    double derImpact = (double) der / (params.barThreshold - params.minDerivative);

                    return new PeakSide(border, derImpact);
                }
            }

            return null; // Invalid
        }
    }

    //-------------//
    // selectBlank //
    //-------------//
    /**
     * Report the relevant blank region on desired staff side.
     * <p>
     * We try to pick up a wide enough region if any.
     * <p>
     * TODO: The selection could be revised in a second phase performed at sheet level, since
     * poor-quality staves may exhibit abnormal blank regions.
     *
     * @param side     desired side
     * @param start    abscissa for starting search
     * @param minWidth minimum blank width
     * @return the relevant blank region found, null if none was found
     */
    private Blank selectBlank(HorizontalSide side, int start, int minWidth) {
        final int dir = (side == LEFT) ? (-1) : 1;
        final int rInit = (side == LEFT) ? (allBlanks.size() - 1) : 0;
        final int rBreak = (side == LEFT) ? (-1) : allBlanks.size();

        for (int ir = rInit; ir != rBreak; ir += dir) {
            Blank blank = allBlanks.get(ir);
            int mid = (blank.start + blank.stop) / 2;

            // Make sure we are on desired side of the staff
            if ((dir * (mid - start)) > 0) {
                int width = blank.getWidth();

                // Stop on first significant blank
                if (width >= minWidth) {
                    return blank;
                }
            }
        }

        return null;
    }

    //--------------------//
    // selectEndingBlanks //
    //--------------------//
    /**
     * Select the pair of ending blanks that limit peak search.
     */
    private void selectEndingBlanks() {
        if (allBlanks.isEmpty()) {
            return;
        }

        for (HorizontalSide side : HorizontalSide.values()) {
            // Look for the first really wide blank encountered
            Blank blank = selectBlank(side, staff.getAbscissa(side), params.minWideBlankWidth);

            if (blank != null) {
                endingBlanks.put(side, blank);
            }
        }

        logger.debug("Staff#{} endingBlanks:{}", staff.getId(), endingBlanks);
    }

    //--------//
    // xClamp //
    //--------//
    /**
     * Clamp the provided abscissa value within legal values that
     * are precisely [0..sheet.getWidth() -1].
     *
     * @param x the abscissa to clamp
     * @return the clamped abscissa
     */
    private int xClamp(int x) {
        if (x < 0) {
            return 0;
        }

        if (x > (sheet.getWidth() - 1)) {
            return sheet.getWidth() - 1;
        }

        return x;
    }

    //~ Inner Classes ------------------------------------------------------------------------------
    //----------//
    // PeakSide //
    //----------//
    /**
     * Describes the (left or right) side of a peak.
     */
    static class PeakSide {
        //~ Instance fields ------------------------------------------------------------------------

        /** Precise side abscissa. */
        final int abscissa;

        /** Quality based on derivative absolute value. */
        final double grade;

        //~ Constructors ---------------------------------------------------------------------------
        public PeakSide(int abscissa, double grade) {
            this.abscissa = abscissa;
            this.grade = grade;
        }
    }

    //-----------//
    // Constants //
    //-----------//
    private static final class Constants extends ConstantSet {
        //~ Instance fields ------------------------------------------------------------------------

        private final Scale.Fraction staffAbscissaMargin = new Scale.Fraction(15,
                "Abscissa margin for checks around staff");

        private final Scale.Fraction barChunkDx = new Scale.Fraction(0.4,
                "Abscissa margin for chunks check around bar");

        private final Scale.Fraction barRefineDx = new Scale.Fraction(0.25,
                "Abscissa margin for refining peak sides");

        private final Scale.Fraction minDerivative = new Scale.Fraction(0.4,
                "Minimum absolute derivative for peak side");

        private final Scale.Fraction barThreshold = new Scale.Fraction(2.5,
                "Minimum cumul value to detect bar peak");

        private final Scale.Fraction braceThreshold = new Scale.Fraction(1.1,
                "Minimum cumul value to detect brace peak");

        private final Scale.Fraction gapThreshold = new Scale.Fraction(0.85,
                "Maximum vertical gap length in a bar");

        private final Scale.Fraction chunkThreshold = new Scale.Fraction(0.8,
                "Maximum cumul value to detect chunk (on top of lines)");

        private final Scale.LineFraction blankThreshold = new Scale.LineFraction(2.5,
                "Maximum cumul value (in LineFraction) to detect no-line regions");

        private final Scale.Fraction minSmallBlankWidth = new Scale.Fraction(0.1,
                "Minimum width for a small blank region (right of lines)");

        private final Scale.Fraction minStandardBlankWidth = new Scale.Fraction(1.0,
                "Minimum width for a standard blank region (left of lines)");

        private final Scale.Fraction minWideBlankWidth = new Scale.Fraction(2.0,
                "Minimum width for a wide blank region (to limit peaks search)");

        private final Scale.Fraction maxBarWidth = new Scale.Fraction(1.5, "Maximum bar width");

        private final Scale.Fraction maxLeftExtremum = new Scale.Fraction(0.15,
                "Maximum length between actual lines left end and left ending bar");

        private final Scale.Fraction maxRightExtremum = new Scale.Fraction(0.3,
                "Maximum length between right ending bar and actual lines right end");
    }

    //-------//
    // Blank //
    //-------//
    /**
     * An abscissa region where no staff lines are detected and thus indicates possible
     * end of staff.
     */
    private static class Blank implements Comparable<Blank> {
        //~ Instance fields ------------------------------------------------------------------------

        /** First abscissa in region. */
        private final int start;

        /** Last abscissa in region. */
        private final int stop;

        //~ Constructors ---------------------------------------------------------------------------
        public Blank(int start, int stop) {
            this.start = start;
            this.stop = stop;
        }

        //~ Methods --------------------------------------------------------------------------------
        @Override
        public int compareTo(Blank that) {
            return Integer.compare(this.start, that.start);
        }

        public int getWidth() {
            return stop - start + 1;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("Blank(").append(start).append("-").append(stop).append(")");

            return sb.toString();
        }
    }

    //------------//
    // Parameters //
    //------------//
    private static class Parameters {
        //~ Instance fields ------------------------------------------------------------------------

        final int staffAbscissaMargin;

        final int barChunkDx;

        final int barRefineDx;

        final int minSmallBlankWidth;

        final int minStandardBlankWidth;

        final int minWideBlankWidth;

        final int maxBarWidth;

        final int maxLeftExtremum;

        final int maxRightExtremum;

        // Following thresholds depend of staff (specific?) interline scale
        final int minDerivative;

        final int barThreshold;

        final int braceThreshold;

        final int gapThreshold;

        // Following thresholds depend on actual line height within this staff
        int linesThreshold;

        int blankThreshold;

        int chunkThreshold;

        //~ Constructors ---------------------------------------------------------------------------
        public Parameters(Scale scale, int staffSpecific) {
            {
                // Use sheet large interline value
                final InterlineScale large = scale.getInterlineScale();
                staffAbscissaMargin = large.toPixels(constants.staffAbscissaMargin);
                barChunkDx = large.toPixels(constants.barChunkDx);
                barRefineDx = large.toPixels(constants.barRefineDx);
                minSmallBlankWidth = large.toPixels(constants.minSmallBlankWidth);
                minStandardBlankWidth = large.toPixels(constants.minStandardBlankWidth);
                minWideBlankWidth = large.toPixels(constants.minWideBlankWidth);
                maxBarWidth = large.toPixels(constants.maxBarWidth);
                maxLeftExtremum = large.toPixels(constants.maxLeftExtremum);
                maxRightExtremum = large.toPixels(constants.maxRightExtremum);
            }

            {
                // Use staff specific interline value
                final InterlineScale specific = scale.getInterlineScale(staffSpecific);
                minDerivative = specific.toPixels(constants.minDerivative);
                barThreshold = specific.toPixels(constants.barThreshold);
                braceThreshold = specific.toPixels(constants.braceThreshold);
                gapThreshold = specific.toPixels(constants.gapThreshold);
            }
        }
    }

    //---------//
    // Plotter //
    //---------//
    /**
     * Handles the display of projection chart.
     */
    private class Plotter {
        //~ Instance fields ------------------------------------------------------------------------

        final XYSeriesCollection dataset = new XYSeriesCollection();

        // Chart
        final JFreeChart chart = ChartFactory.createXYLineChart(sheet.getId() + " staff#" + getStaff().getId(), // Title
                "Abscissae - staff interline:" + staff.getSpecificInterline(), // X-Axis label
                "Counts", // Y-Axis label
                dataset, // Dataset
                PlotOrientation.VERTICAL, // orientation,
                true, // Show legend
                false, // Show tool tips
                false // urls
        );

        final XYPlot plot = (XYPlot) chart.getPlot();

        final XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();

        // Series index
        int index = -1;

        //~ Methods --------------------------------------------------------------------------------
        public void plot() {
            plot.setRenderer(renderer);

            final int xMin = 0;
            final int xMax = sheet.getWidth() - 1;

            {
                // Values
                XYSeries valueSeries = new XYSeries("Cumuls", false); // No autosort

                for (int x = xMin; x <= xMax; x++) {
                    valueSeries.add(x, projection.getValue(x));
                }

                add(valueSeries, Colors.CHART_VALUE, false);
            }

            {
                // Derivatives
                XYSeries derivativeSeries = new XYSeries("Derivatives", false); // No autosort

                for (int x = xMin; x <= xMax; x++) {
                    derivativeSeries.add(x, projection.getDerivative(x));
                }

                add(derivativeSeries, Colors.CHART_DERIVATIVE, false);
            }

            {
                // Derivatives positive threshold
                XYSeries derSeries = new XYSeries("Der+", false); // No autosort

                derSeries.add(xMin, params.minDerivative);
                derSeries.add(xMax, params.minDerivative);
                add(derSeries, Colors.CHART_DERIVATIVE, false);
            }

            {
                // Derivatives negative threshold
                XYSeries derSeries = new XYSeries("Der-", false); // No autosort

                derSeries.add(xMin, -params.minDerivative);
                derSeries.add(xMax, -params.minDerivative);
                add(derSeries, Colors.CHART_DERIVATIVE, false);
            }

            {
                // Theoretical staff height (assuming a 5-line staff)
                XYSeries heightSeries = new XYSeries("StaffHeight", false); // No autosort
                int totalHeight = 4 * staff.getSpecificInterline();
                heightSeries.add(xMin, totalHeight);
                heightSeries.add(xMax, totalHeight);
                add(heightSeries, Color.BLACK, true);
            }

            {
                // BarPeak min threshold
                XYSeries minSeries = new XYSeries("MinBar", false); // No autosort
                minSeries.add(xMin, params.barThreshold);
                minSeries.add(xMax, params.barThreshold);
                add(minSeries, Color.GREEN, true);
            }

            {
                // Chunk threshold (assuming a 5-line staff)
                XYSeries chunkSeries = new XYSeries("MaxChunk", false); // No autosort
                chunkSeries.add(xMin, params.chunkThreshold);
                chunkSeries.add(xMax, params.chunkThreshold);
                add(chunkSeries, Color.YELLOW, true);
            }

            {
                // BracePeak min threshold
                XYSeries minSeries = new XYSeries("MinBrace", false); // No autosort
                minSeries.add(xMin, params.braceThreshold);
                minSeries.add(xMax, params.braceThreshold);
                add(minSeries, Color.ORANGE, true);
            }

            {
                // Cumulated staff lines (assuming a 5-line staff)
                XYSeries linesSeries = new XYSeries("Lines", false); // No autosort
                linesSeries.add(xMin, params.linesThreshold);
                linesSeries.add(xMax, params.linesThreshold);
                add(linesSeries, Color.MAGENTA, true);
            }

            {
                // Threshold for no staff
                final int nostaff = params.blankThreshold;
                XYSeries holeSeries = new XYSeries("NoStaff", false); // No autosort
                holeSeries.add(xMin, nostaff);
                holeSeries.add(xMax, nostaff);
                add(holeSeries, Color.CYAN, true);
            }

            {
                // Zero
                XYSeries zeroSeries = new XYSeries("Zero", false); // No autosort
                zeroSeries.add(xMin, 0);
                zeroSeries.add(xMax, 0);
                add(zeroSeries, Colors.CHART_ZERO, true);
            }

            // Hosting frame
            ChartFrame frame = new ChartFrame(sheet.getId() + " staff#" + getStaff().getId(), chart, true);
            frame.pack();
            frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
            frame.setLocation(new Point(20 * getStaff().getId(), 20 * getStaff().getId()));
            frame.setVisible(true);
        }

        private void add(XYSeries series, Color color, boolean displayShapes) {
            dataset.addSeries(series);
            renderer.setSeriesPaint(++index, color);
            renderer.setSeriesShapesVisible(index, displayShapes);
        }
    }
}