omr.sheet.LinesBuilder.java Source code

Java tutorial

Introduction

Here is the source code for omr.sheet.LinesBuilder.java

Source

//----------------------------------------------------------------------------//
//                                                                            //
//                          L i n e s B u i l d e r                           //
//                                                                            //
//----------------------------------------------------------------------------//
// <editor-fold defaultstate="collapsed" desc="hdr">                          //
//  Copyright (C) Herve Bitteur 2000-2010. All rights reserved.               //
//  This software is released under the GNU General Public License.           //
//  Goto http://kenai.com/projects/audiveris to report bugs or suggestions.   //
//----------------------------------------------------------------------------//
// </editor-fold>
package omr.sheet;

import omr.Main;

import omr.constant.Constant;
import omr.constant.ConstantSet;

import omr.glyph.GlyphLag;
import omr.glyph.GlyphSection;
import omr.glyph.GlyphsModel;
import omr.glyph.ui.GlyphBoard;
import omr.glyph.ui.GlyphLagView;
import omr.glyph.ui.GlyphsController;

import omr.lag.HorizontalOrientation;
import omr.lag.JunctionDeltaPolicy;
import omr.lag.Run;
import omr.lag.Section;
import omr.lag.SectionsBuilder;
import omr.lag.ui.RunBoard;
import omr.lag.ui.ScrollLagView;
import omr.lag.ui.SectionBoard;

import omr.log.Logger;

import omr.math.Population;

import omr.sheet.picture.Picture;
import omr.sheet.ui.PixelBoard;
import omr.sheet.ui.SheetsController;

import omr.step.Step;
import omr.step.StepException;

import omr.stick.StickSection;

import omr.ui.BoardsPane;

import org.jdesktop.application.AbstractBean;
import org.jdesktop.application.Action;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartFrame;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.RefineryUtilities;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.util.*;
import java.util.List;

import javax.swing.*;

/**
 * Class <code>LinesBuilder</code> is dedicated to the retrieval of the grid of
 * staff lines. The various series of staves lines are detected, and their lines
 * are carefully cleaned up when an object crosses them. Note that staves are
 * not yet gathered into Systems, this will be done in the BarsBuilder
 * processing.
 *
 * @author Herv Bitteur
 */
public class LinesBuilder extends GlyphsModel {
    //~ Static fields/initializers ---------------------------------------------

    /** Specific application parameters */
    private static final Constants constants = new Constants();

    /** Usual logger utility */
    private static final Logger logger = Logger.getLogger(LinesBuilder.class);

    //~ Instance fields --------------------------------------------------------

    /** Series of horizontal peaks that signal staff areas */
    private List<StaffInfo> staves = new ArrayList<StaffInfo>();

    /** Related scale */
    private Scale scale;

    /** Lag view on staff lines, if so desired */
    private GlyphLagView lagView;

    /** Data needed for displayChart */
    private int[] histo;
    private int maxHisto;
    private double histoRatio;

    //~ Constructors -----------------------------------------------------------

    //--------------//
    // LinesBuilder //
    //--------------//
    /**
     * Just allocate the LinesBuilder
     *
     * @param sheet the sheet on which the analysis is performed.
     */
    public LinesBuilder(Sheet sheet) {
        super(sheet, new GlyphLag("hLag", StickSection.class, new HorizontalOrientation()), Step.LINES);
    }

    //~ Methods ----------------------------------------------------------------

    //-----------//
    // getStaves //
    //-----------//
    /**
     * Report the list of staves found in the sheet
     *
     * @return the collection of staves found
     */
    public List<StaffInfo> getStaves() {
        return staves;
    }

    //-----------//
    // buildInfo //
    //-----------//
    /**
     * Perform the retrieval of the various staves
     * @throws StepException is processing must stop
     */
    public void buildInfo() throws StepException {
        // Check output needed from previous steps
        scale = sheet.getScale(); // Will run Scale if not yet done
        sheet.getSkew(); // Will run Skew  if not yet done

        Picture picture = sheet.getPicture();

        // Populate the lag
        SectionsBuilder<GlyphLag, GlyphSection> lagBuilder;
        lagBuilder = new SectionsBuilder<GlyphLag, GlyphSection>(lag,
                new JunctionDeltaPolicy(scale.toPixels(constants.maxDeltaLength)));
        lagBuilder.createSections(picture, 0); // 0 = minRunLength
        sheet.setHorizontalLag(lag);

        retrieveStaves(retrievePeaks(picture.getHeight()));

        // Clean up the staff lines in the found staves.
        cleanup();

        // Determine limits in ordinate for each staff area
        if (!staves.isEmpty()) {
            computeStaffLimits();
        }

        // User feedback
        if (staves.size() > 1) {
            logger.info(staves.size() + " staves");
        } else if (!staves.isEmpty()) {
            logger.info(staves.size() + " staff");
        } else {
            logger.warning(
                    "No staff found." + " Check Line plot." + " Check Staff Lines ratio in score parameters.");
        }

        sheet.getBench().recordStaveCount(staves.size());

        // Display the resulting lag is so asked for
        if (constants.displayFrame.getValue() && (Main.getGui() != null)) {
            displayFrame();
        }

        if (staves.isEmpty()) {
            throw new StepException("Cannot proceed without staves");
        }
    }

    //--------------//
    // displayChart //
    //--------------//
    /**
     * Build and display the histogram of projections
     */
    public void displayChart() {
        writePlot(histo, maxHisto, histoRatio);
    }

    //---------//
    // cleanup //
    //---------//
    private void cleanup() {
        for (StaffInfo staff : staves) {
            staff.cleanup();
        }
    }

    //-----------------//
    // computeInterval //
    //-----------------//
    private double computeInterval(Peak from, Peak to) {
        return (double) ((to.getTop() + to.getBottom()) - from.getTop() - from.getBottom()) / 2;
    }

    //--------------------//
    // computeStaffLimits //
    //--------------------//
    private void computeStaffLimits() {
        StaffInfo prevStaff = null;

        for (StaffInfo staff : staves) {
            // Very first staff
            if (prevStaff == null) {
                staff.setAreaTop(0);
            } else {
                // Top of staff area, defined as middle ordinate between
                // ordinate of last line of previous staff and ordinate of
                // first line of current staff
                int middle = (prevStaff.getLastLine().yAt(prevStaff.getLeft())
                        + staff.getFirstLine().yAt(staff.getLeft())) / 2;
                prevStaff.setAreaBottom(middle);
                staff.setAreaTop(middle);
            }

            // Remember this staff for next one
            prevStaff = staff;
        }

        // Bottom of last staff
        prevStaff.setAreaBottom(Integer.MAX_VALUE);
    }

    //--------------//
    // displayFrame //
    //--------------//
    private void displayFrame() {
        // Sections that, as members of staff lines, will be treated as specific
        List<GlyphSection> members = new ArrayList<GlyphSection>();

        /*
         * Populate the specific sections, to hide or display the removed
         * line sections. This assumes StraightLineInfo implementation (?)
         */

        // Browse StaffInfos
        for (StaffInfo staff : staves) {
            // Browse LineInfos
            for (LineInfo line : staff.getLines()) {
                members.addAll(line.getSections());
            }
        }

        GlyphsController controller = new GlyphsController(this);
        lagView = new MyView(lag, members, controller);

        final String unit = sheet.getRadix() + ":LinesBuilder";
        BoardsPane boardsPane = new BoardsPane(sheet, lagView, new PixelBoard(unit, sheet), new RunBoard(unit, lag),
                new SectionBoard(unit, lag.getLastVertexId(), lag), new GlyphBoard(unit, controller, null));

        // Create a hosting frame for the view
        ScrollLagView slv = new ScrollLagView(lagView);
        sheet.getAssembly().addViewTab(Step.LINES, slv, boardsPane);
    }

    //---------------//
    // retrievePeaks //
    //---------------//
    /**
     * Peaks are detected in the horizontal projections. The resulting list of
     * peaks is made of all peaks higher than a given threshold, so they are not
     * all related to staff lines. This will be filtered later.
     *
     * @param height the picture height
     *
     * @return the raw list of detected peaks
     */
    private List<Peak> retrievePeaks(int height) {
        // Peaks found in horizontal histogram
        List<Peak> peaks = new ArrayList<Peak>();

        // Initialize histogram to zeroes
        histo = new int[height];
        Arrays.fill(histo, 0);

        // Visit all vertices (sections) of the lag
        for (GlyphSection section : lag.getVertices()) {
            int y = section.getFirstPos();

            for (Run run : section.getRuns()) {
                histo[y++] += run.getLength();
            }
        }

        // Determine histogram threshold
        maxHisto = 0;

        for (int i = histo.length - 1; i >= 0; i--) {
            if (histo[i] > maxHisto) {
                maxHisto = histo[i];
            }
        }

        histoRatio = sheet.getHistoRatio();

        // Write histo data if so asked for
        if (constants.plotting.getValue()) {
            displayChart();
        }

        int threshold = (int) (maxHisto * histoRatio);

        // Determine peaks in the histogram
        Peak peak = null;

        for (int i = 0; i < histo.length; i++) {
            if (histo[i] >= threshold) {
                if (peak == null) { // Entering a peak
                    peak = new Peak(i, histo[i]);
                } else { // Extending a peak
                    peak.extend(i, histo[i]);
                }
            } else {
                if (peak != null) {
                    // Exiting a peak, let's record it
                    peaks.add(peak);
                    peak = null;
                }
            }
        }

        // Make sure we don't forget a last peak
        if (peak != null) {
            peaks.add(peak);
        }

        // Dump peaks for debugging
        if (logger.isFineEnabled()) {
            logger.fine("Peak nb = " + peaks.size());

            int i = 0;

            for (Peak pk : peaks) {
                if (logger.isFineEnabled()) {
                    logger.fine(i++ + " " + pk);
                }
            }
        }

        return peaks;
    }

    //----------------//
    // retrieveStaves //
    //----------------//
    /**
     * Staff are detected in the list of (raw) peaks, simply by looking for
     * regular series of peaks.
     *
     * @param peaks the raw list of peaks found
     */
    private void retrieveStaves(List<Peak> peaks) throws StepException {
        // One single iterator, since from peak area to peak area, we keep
        // moving forward in an ordered list of vertices
        ArrayList<GlyphSection> vertices = new ArrayList<GlyphSection>(lag.getVertices());
        Collections.sort(vertices, Section.idComparator);

        ListIterator<GlyphSection> vi = vertices.listIterator();

        // Maximum deviation accepted in the series of peaks in a staff
        final double maxDeviation = scale.toPixelsDouble(constants.maxInterlineDeviation);

        // Maximum difference in interval between a 6th line and the average
        // interval in the previous 5 lines
        final double maxDiff = scale.toPixelsDouble(constants.maxInterlineDiffFrac);

        // Desired length of series (TODO)
        final int interlineNb = 4;

        int firstPeak = 0;
        int lastPeak = 0;
        Population intervals = new Population();
        LineBuilder.reset();

        // Use a new staff retriever
        StaffBuilder staffBuilder = new StaffBuilder(sheet, lag, vi);

        // Browse through the peak list
        Peak prevPeak = null;

        for (ListIterator<Peak> li = peaks.listIterator(); li.hasNext();) {
            // Get peak at hand
            Peak peak = li.next();

            if (logger.isFineEnabled()) {
                logger.fine((li.nextIndex() - 1) + " " + peak);
            }

            // If very first one, we don't yet have intervals
            if (li.nextIndex() == 1) {
                prevPeak = peak;

                continue;
            }

            // Compute interval with previous peak
            double interval = computeInterval(prevPeak, peak);

            if (logger.isFineEnabled()) {
                logger.fine("interval=" + interval);
            }

            intervals.includeValue(interval);
            prevPeak = peak;

            // Check for regularity of current series
            if (intervals.getCardinality() > 1) {
                double stdDev = intervals.getStandardDeviation();

                if (logger.isFineEnabled()) {
                    logger.fine("stdDev=" + (float) stdDev);
                }

                if (stdDev > maxDeviation) {
                    if (logger.isFineEnabled()) {
                        logger.fine("Interval gap detected");
                    }

                    intervals.reset(interval);
                } else if (intervals.getCardinality() == interlineNb) {
                    if (logger.isFineEnabled()) {
                        logger.fine("End of staff");
                    }

                    // We have a suitable series.  However, let's look for a
                    // better sixth one if any on the other side of the staff
                    lastPeak = li.nextIndex() - 1;
                    firstPeak = lastPeak - interlineNb;

                    if (li.hasNext()) {
                        Peak nextPeak = li.next();
                        interval = computeInterval(peak, nextPeak);

                        if ((Math.abs(interval - intervals.getMeanValue()) <= maxDiff) // Good candidate, compare with first one
                                && (nextPeak.getMax() > peaks.get(firstPeak).getMax())) {
                            if (logger.isFineEnabled()) {
                                logger.fine("Moving to sixth line");
                            }

                            // Fix computation of interval value
                            intervals.excludeValue(computeInterval(peaks.get(firstPeak), peaks.get(firstPeak + 1)));
                            intervals.includeValue(interval);

                            // Update indices
                            firstPeak++;
                            lastPeak++;
                        } else {
                            li.previous(); // Undo the move to the sixth peak
                        }
                    }

                    // We now have a set of peaks that signals a staff area
                    if (logger.isFineEnabled()) {
                        logger.fine("Staff from peaks " + firstPeak + " to " + lastPeak);
                    }

                    staves.add(staffBuilder.buildInfo(peaks.subList(firstPeak, lastPeak + 1),
                            intervals.getMeanValue()));

                    if (logger.isFineEnabled()) {
                        System.out.println();
                    }

                    // Move to the next peak, candidate for starting a new
                    // staff
                    if (li.hasNext()) {
                        intervals.reset();
                        prevPeak = li.next();

                        if (logger.isFineEnabled()) {
                            logger.fine((li.nextIndex() - 1) + " " + prevPeak);
                        }
                    }
                }
            }
        }
    }

    //-----------//
    // writePlot //
    //-----------//
    private void writePlot(int[] histo, int maxHisto, double ratio) {
        XYSeriesCollection dataset = new XYSeriesCollection();

        // Threshold line
        XYSeries thresholdSeries = new XYSeries("Staff ratio used" + " [" + ratio + "]");
        thresholdSeries.add(0, ratio);
        thresholdSeries.add(-histo.length + 1, ratio);
        dataset.addSeries(thresholdSeries);

        // Projection data
        XYSeries dataSeries = new XYSeries("Projections");

        for (int i = 0; i < histo.length; i++) {
            dataSeries.add(-i, histo[i] / (double) maxHisto);
        }

        dataset.addSeries(dataSeries);

        // Chart
        JFreeChart chart = ChartFactory.createXYLineChart(sheet.getRadix() + " (Horizontal Projections)", // Title
                "Ordinate", "Ratios of horizontal counts", dataset, // Dataset
                PlotOrientation.HORIZONTAL, // orientation,
                true, // Show legend
                false, // Show tool tips
                false // urls
        );

        // Hosting frame
        ChartFrame frame = new ChartFrame(sheet.getRadix() + " - Staff Lines", chart, true);
        frame.pack();
        frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        RefineryUtilities.centerFrameOnScreen(frame);
        frame.setVisible(true);
    }

    //~ Inner Classes ----------------------------------------------------------

    //-----------------//
    // LinesParameters //
    //-----------------//
    public static class LinesParameters extends AbstractBean {
        //~ Methods ------------------------------------------------------------

        //-------------//
        // getInstance //
        //-------------//
        public static LinesParameters getInstance() {
            return Holder.INSTANCE;
        }

        //-----------------//
        // setLinePainting //
        //-----------------//
        public void setLinePainting(boolean value) {
            boolean oldValue = constants.displayOriginalStaffLines.getValue();
            constants.displayOriginalStaffLines.setValue(value);
            firePropertyChange("linePainting", oldValue, constants.displayOriginalStaffLines.getValue());
        }

        //----------------//
        // isLinePainting //
        //----------------//
        public boolean isLinePainting() {
            return constants.displayOriginalStaffLines.getValue();
        }

        //-------------//
        // toggleLines //
        //-------------//
        /**
         * Action that toggles the display of original pixels for the staff
         * lines
         * @param e the event that triggered this action
         */
        @Action(selectedProperty = "linePainting")
        public void toggleLines(ActionEvent e) {
            // Trigger a repaint if needed
            Sheet currentSheet = SheetsController.selectedSheet();

            if (currentSheet != null) {
                LinesBuilder builder = currentSheet.getLinesBuilder();

                if ((builder != null) && (builder.lagView != null)) {
                    builder.lagView.repaint();
                }
            }
        }

        //~ Inner Classes ------------------------------------------------------

        private static class Holder {
            //~ Static fields/initializers -------------------------------------

            public static final LinesParameters INSTANCE = new LinesParameters();
        }
    }

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

        /** Should we display a frame on Lags ? */
        Constant.Boolean displayFrame = new Constant.Boolean(true, "Should we display a frame on Lags ?");

        /** Should we display original staff lines */
        Constant.Boolean displayOriginalStaffLines = new Constant.Boolean(false,
                "Should we display original staff lines?");

        /** Maximum difference in length of two consecutives runs in the same section */
        Scale.Fraction maxDeltaLength = new Scale.Fraction(0.2d,
                "Maximum difference in length of two consecutives runs in the same section");

        /** Maximum deviation in the series of interline values in a staff */
        Scale.Fraction maxInterlineDeviation = new Scale.Fraction(0.5d,
                "Maximum deviation in the series of interline values in a staff");

        /** Maximum difference between a new interline and the current staff value */
        Scale.Fraction maxInterlineDiffFrac = new Scale.Fraction(0.1d,
                "Maximum difference between a new interline and the current staff value");

        /** Should we produce a chart of computed data ? */
        Constant.Boolean plotting = new Constant.Boolean(false, "Should we produce a chart of computed data ?");
    }

    //--------//
    // MyView //
    //--------//
    private class MyView extends GlyphLagView {
        //~ Constructors -------------------------------------------------------

        //--------//
        // MyView //
        //--------//
        public MyView(GlyphLag lag, List<GlyphSection> specifics, GlyphsController controller) {
            super(lag, specifics, constants.displayOriginalStaffLines, controller, null);
            setName("LinesBuilder-View");
        }

        //~ Methods ------------------------------------------------------------

        //-------------//
        // renderItems //
        //-------------//
        @Override
        protected void renderItems(Graphics g) {
            // Draw the line info, lineset by lineset
            g.setColor(Color.black);

            for (StaffInfo staff : staves) {
                staff.render(g);
            }
        }
    }
}