Java tutorial
//----------------------------------------------------------------------------// // // // S c a l e B u i l d e 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 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 General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. //----------------------------------------------------------------------------// // </editor-fold> package org.audiveris.omr.sheet; import org.audiveris.omr.Main; import static org.audiveris.omr.WellKnowns.LINE_SEPARATOR; import org.audiveris.omr.constant.Constant; import org.audiveris.omr.constant.ConstantSet; import org.audiveris.omr.math.Histogram; import org.audiveris.omr.math.Histogram.MaxEntry; import org.audiveris.omr.math.Histogram.PeakEntry; import org.audiveris.omr.run.FilterDescriptor; import org.audiveris.omr.run.Orientation; import org.audiveris.omr.run.Run; import org.audiveris.omr.run.RunsTable; import org.audiveris.omr.run.RunsTableFactory; import org.audiveris.omr.score.Score; import org.audiveris.omr.sheet.picture.Picture; import org.audiveris.omr.sheet.ui.SheetsController; import org.audiveris.omr.step.StepException; import org.audiveris.omr.util.StopWatch; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Point; import java.util.Arrays; import java.util.List; import javax.swing.JOptionPane; import javax.swing.WindowConstants; /** * Class {@code ScaleBuilder} encapsulates the computation of a sheet * scale, by adding the most frequent foreground run length to the most * frequent background run length, since this gives the average * interline value. * * <p>A second foreground peak usually gives the average beam thickness. * And similarly, a second background peak may indicate a series of staves * with a different interline than the main series.</p> * * <p>Internally, additional validity checks are performed:<ol> * <li>Method {@link #checkStaves} looks at foreground and background * peak populations. * <p>If these counts are below quorum values (see constants.quorumRatio), * we can suspect that the page does not contain regularly spaced staff lines. * </p></li> * <li>Method {@link #checkResolution} looks at foreground and background * peak keys. * <p>If we have not been able to retrieve the main run length for background * or for foreground, then we suspect a wrong image format. In that case, * the safe action is to stop the processing, by throwing a StepException. * If the main interline value is below a certain threshold * (see constants.minResolution), then we suspect that the picture is not * a music sheet (it may rather be an image, a page of text, ...).</p></li> * </ol> * * <p>If we have doubts about the page at hand and if this page is part of a * multi-page score, we propose to simply discard this sheet. In batch, the * page is discarded without asking for confirmation.</p> * * @see Scale * * @author Herv Bitteur */ public class ScaleBuilder { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(ScaleBuilder.class); //~ Instance fields -------------------------------------------------------- // /** Related sheet. */ private Sheet sheet; /** Keeper of run length histograms, for foreground & background. */ private HistoKeeper histoKeeper; /** Histogram on foreground runs. */ private Histogram<Integer> foreHisto; /** Histogram on background runs. */ private Histogram<Integer> backHisto; /** Absolute population percentage for validating an extremum. */ private final double quorumRatio = constants.quorumRatio.getValue(); /** Relative population percentage for reading foreground spread. */ private final double foreSpreadRatio = constants.foreSpreadRatio.getValue(); /** Relative population percentage for reading background spread. */ private final double backSpreadRatio = constants.backSpreadRatio.getValue(); /** Foreground peak. */ private PeakEntry<Double> forePeak; /** Second frequent length of foreground runs found, if any. */ private MaxEntry<Integer> beamEntry; /** Most frequent length of background runs found. */ private PeakEntry<Double> backPeak; /** Second frequent length of background runs found, if any. */ private PeakEntry<Double> secondBackPeak; /** Resulting scale, if any. */ private Scale scale; //~ Constructors ----------------------------------------------------------- //--------------// // ScaleBuilder // //--------------// /** * Constructor to enable scale computation on a given sheet. * * @param sheet the sheet at hand */ public ScaleBuilder(Sheet sheet) { this.sheet = sheet; } //~ Methods ---------------------------------------------------------------- //--------------// // displayChart // //--------------// /** * Display the scale histograms. */ public void displayChart() { if (histoKeeper != null) { histoKeeper.writePlot(); } else { logger.warn("No scale data available"); } } //---------------// // retrieveScale // //---------------// /** * Retrieve the global scale values by processing the provided * picture runs, make decisions about the validity of current * picture as a music page and store the results as a {@link Scale} * instance in the related sheet. * * @throws StepException if processing must stop for this sheet. */ public void retrieveScale() throws StepException { Picture picture = sheet.getPicture(); // Binarization: Retrieve the whole table of foreground runs histoKeeper = new HistoKeeper(picture.getHeight() - 1); FilterDescriptor desc = sheet.getPage().getFilterParam().getTarget(); logger.info("{}{} {}", sheet.getLogPrefix(), "Binarization", desc); sheet.getPage().getFilterParam().setActual(desc); StopWatch watch = new StopWatch("Binarization " + sheet.getPage().getId() + " " + desc); watch.start("Vertical runs"); RunsTableFactory factory = new RunsTableFactory(Orientation.VERTICAL, desc.getFilter(picture), 0); RunsTable wholeVertTable = factory.createTable("whole"); sheet.setWholeVerticalTable(wholeVertTable); factory = null; // To allow garbage collection ASAP if (constants.printWatch.isSet()) { watch.print(); } // Build the two histograms histoKeeper.buildHistograms(wholeVertTable, picture.getWidth(), picture.getHeight()); // Retrieve the various histograms peaks retrievePeaks(); // Check this page looks like music staves. If not, throw StepException checkStaves(); // Check we have acceptable resolution. If not, throw StepException checkResolution(); // Here, we keep going on with scale data scale = new Scale(computeLine(), computeInterline(), computeBeam(), computeSecondInterline()); logger.info("{}{}", sheet.getLogPrefix(), scale); sheet.getBench().recordScale(scale); sheet.setScale(scale); } //-----------------// // checkResolution // //-----------------// /** * Check global interline value, to detect pictures with too low * resolution or pictures which do not represent music staves. * * @throws StepException if processing must stop on this sheet */ private void checkResolution() throws StepException { if (forePeak == null) { throw new StepException("Missing black peak"); } if (backPeak == null) { throw new StepException("Missing white peak"); } int interline = (int) (forePeak.getKey().best + backPeak.getKey().best); if (interline < constants.minResolution.getValue()) { makeDecision(sheet.getId() + LINE_SEPARATOR + "With an interline value of " + interline + " pixels," + LINE_SEPARATOR + "either this page contains no staves," + LINE_SEPARATOR + "or the picture resolution is too low (try 300 DPI)."); } } //-------------// // checkStaves // //-------------// /** * Check we have foreground and background run peaks, with * significant percentage of runs population, otherwise we are not * looking at staves and the picture represents something else. * * @throws StepException if processing must stop on this sheet */ private void checkStaves() throws StepException { String error = null; if ((forePeak == null) || (forePeak.getValue() < quorumRatio)) { error = "No significant black lines found."; } else if ((backPeak == null) || (backPeak.getValue() < quorumRatio)) { error = "No regularly spaced lines found."; } if (error != null) { makeDecision(sheet.getId() + LINE_SEPARATOR + error + LINE_SEPARATOR + "This sheet does not seem to contain staff lines."); } } //-------------// // computeBeam // //-------------// private Integer computeBeam() { if (beamEntry != null) { return beamEntry.getKey(); } else { if (backPeak != null) { logger.info("{}{}", sheet.getLogPrefix(), "No beam peak found, computing a default value"); return (int) Math.rint(0.7 * backPeak.getKey().best); } else { return null; } } } //------------------// // computeInterline // //------------------// private Scale.Range computeInterline() { if ((forePeak != null) && (backPeak != null)) { int min = (int) Math.rint(forePeak.getKey().first + backPeak.getKey().first); int best = (int) Math.rint(forePeak.getKey().best + backPeak.getKey().best); int max = (int) Math.rint(forePeak.getKey().second + backPeak.getKey().second); return new Scale.Range(min, best, max); } else { return null; } } //-------------// // computeLine // //-------------// /** * Compute the range for line thickness. * The computation of line max is key for the rest of the application, * since it governs the threshold between horizontal and vertical lags. * * @return the line range */ private Scale.Range computeLine() { if (forePeak != null) { int min = (int) Math.rint(forePeak.getKey().first); int best = (int) Math.rint(forePeak.getKey().best); int max = (int) Math.ceil(forePeak.getKey().second); return new Scale.Range(min, best, max); } else { return null; } } //------------------------// // computeSecondInterline // //------------------------// private Scale.Range computeSecondInterline() { if (secondBackPeak != null) { int min = (int) Math.rint(forePeak.getKey().first + secondBackPeak.getKey().first); int best = (int) Math.rint(forePeak.getKey().best + secondBackPeak.getKey().best); int max = (int) Math.rint(forePeak.getKey().second + secondBackPeak.getKey().second); return new Scale.Range(min, best, max); } else { return null; } } //---------// // getPeak // //---------// private PeakEntry<Double> getPeak(Histogram<?> histo, double spreadRatio, int index) { PeakEntry<Double> peak = null; // Find peak(s) using quorum threshold List<PeakEntry<Double>> peaks = histo.getDoublePeaks(histo.getQuorumValue(quorumRatio)); if (index < peaks.size()) { peak = peaks.get(index); // Refine peak using spread threshold peaks = histo.getDoublePeaks(histo.getQuorumValue(peak.getValue() * spreadRatio)); if (index < peaks.size()) { peak = peaks.get(index); } } return peak; } //--------------// // makeDecision // //--------------// /** * An abnormal situation has been found, as detailed in provided msg, * now how should we proceed, depending on batch mode or user answer. * * @param msg the problem description * @throws StepException thrown when processing must stop */ private void makeDecision(String msg) throws StepException { logger.warn(msg.replaceAll(LINE_SEPARATOR, " ")); Score score = sheet.getScore(); if (Main.getGui() != null) { // Make sheet visible to the user SheetsController.getInstance().showAssembly(sheet); } if ((Main.getGui() == null) || (Main.getGui().displayModelessConfirm( msg + LINE_SEPARATOR + "OK for discarding this sheet?") == JOptionPane.OK_OPTION)) { if (score.isMultiPage()) { sheet.remove(false); throw new StepException("Sheet removed"); } else { throw new StepException("Sheet ignored"); } } } //---------------// // retrievePeaks // //---------------// private void retrievePeaks() throws StepException { StringBuilder sb = new StringBuilder(sheet.getLogPrefix()); // Foreground peak forePeak = getPeak(foreHisto, foreSpreadRatio, 0); sb.append("fore:").append(forePeak); if (forePeak.getValue() == 1d) { String msg = "All image pixels are foreground." + " Check binarization parameters"; logger.warn(msg); throw new StepException(msg); } // Background peak backPeak = getPeak(backHisto, backSpreadRatio, 0); if (backPeak.getValue() == 1d) { String msg = "All image pixels are background." + " Check binarization parameters"; logger.warn(msg); throw new StepException(msg); } // Second background peak? secondBackPeak = getPeak(backHisto, backSpreadRatio, 1); if (secondBackPeak != null) { // Check whether we should merge with first foreground peak // Test: Delta between peaks <= line thickness Histogram.Peak<Double> p1 = backPeak.getKey(); Histogram.Peak<Double> p2 = secondBackPeak.getKey(); if (Math.abs(p1.best - p2.best) <= forePeak.getKey().best) { backPeak = new PeakEntry( new Histogram.Peak<>(Math.min(p1.first, p2.first), (p1.best + p2.best) / 2, Math.max(p1.second, p2.second)), (backPeak.getValue() + secondBackPeak.getValue()) / 2); secondBackPeak = null; logger.info("Merged two close background peaks"); } else { // Check whether this second background peak can be an interline // We check that p2 is not too large, compared with p1 if (p2.best > p1.best * constants.maxSecondRatio.getValue()) { logger.info("Second background peak too large {}, ignored", p2.best); secondBackPeak = null; } } } sb.append(" back:").append(backPeak); if (secondBackPeak != null) { sb.append(" secondBack:").append(secondBackPeak); } // Second foreground peak (beam)? if ((forePeak != null) && (backPeak != null)) { // Take most frequent local max for which key (beam thickness) is // larger than twice the mean line thickness and smaller than // mean white gap between staff lines. List<MaxEntry<Integer>> foreMaxima = foreHisto.getLocalMaxima(); double minBeamLineRatio = constants.minBeamLineRatio.getValue(); double minHeight = minBeamLineRatio * forePeak.getKey().best; double maxHeight = backPeak.getKey().best; for (MaxEntry<Integer> max : foreMaxima) { if (max.getKey() >= minHeight && max.getKey() <= maxHeight) { beamEntry = max; sb.append(" beam:").append(beamEntry); break; } } } logger.debug(sb.toString()); } //~ Inner Classes ---------------------------------------------------------- // //-------------// // HistoKeeper // //-------------// /** * This class builds the precise foreground and background run * lengths, it retrieves the various peaks and is able to display a * chart on the related populations if so asked by the user. * It first builds the whole table of foreground vertical runs, which will * be reused in following step (GRID). */ private class HistoKeeper { //~ Instance fields ---------------------------------------------------- private final int[] fore; // (black) foreground runs private final int[] back; // (white) background runs //~ Constructors ------------------------------------------------------- // //-------------// // HistoKeeper // //-------------// /** * Create an instance of histoKeeper. * * @param hMax the maximum possible run length */ public HistoKeeper(int hMax) { // Allocate histogram counters fore = new int[hMax + 2]; back = new int[hMax + 2]; // Useful? Arrays.fill(fore, 0); Arrays.fill(back, 0); } //~ Methods ------------------------------------------------------------ // //-----------// // writePlot // //-----------// public void writePlot() { int upper = (int) Math.min(fore.length, ((backPeak != null) ? ((backPeak.getKey().best * 3) / 2) : 20)); new Plotter("black", fore, foreHisto, foreSpreadRatio, forePeak, null, upper).plot(new Point(0, 0)); new Plotter("white", back, backHisto, backSpreadRatio, backPeak, secondBackPeak, upper) .plot(new Point(20, 20)); } //-----------------// // createHistogram // //-----------------// private Histogram<Integer> createHistogram(int... vals) { Histogram<Integer> histo = new Histogram<>(); for (int i = 0; i < vals.length; i++) { histo.increaseCount(i, vals[i]); } return histo; } //-----------------// // buildHistograms // //-----------------// private void buildHistograms(RunsTable wholeVertTable, int width, int height) { // Upper bounds for run lengths final int maxBack = height / 4; final int maxFore = height / 16; for (int x = 0; x < width; x++) { List<Run> runSeq = wholeVertTable.getSequence(x); // Ordinate of first pixel not yet processed int yLast = 0; for (Run run : runSeq) { int y = run.getStart(); if (y > yLast) { // Process the background run before this run int backLength = y - yLast; if (backLength <= maxBack) { back[backLength]++; } } // Process this foreground run int foreLength = run.getLength(); if (foreLength <= maxFore) { fore[foreLength]++; } yLast = y + foreLength; } // Process a last background run, if any if (yLast < height) { int backLength = height - yLast; if (backLength <= maxBack) { back[backLength]++; } } } if (logger.isDebugEnabled()) { logger.debug("fore values: {}", Arrays.toString(fore)); logger.debug("back values: {}", Arrays.toString(back)); } // Create foreground & background histograms foreHisto = createHistogram(fore); backHisto = createHistogram(back); } } //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- final Constant.Integer minResolution = new Constant.Integer("Pixels", 11, "Minimum resolution, expressed as number of pixels per interline"); final Constant.Ratio quorumRatio = new Constant.Ratio(0.1, "Absolute ratio of total pixels for peak acceptance"); final Constant.Ratio foreSpreadRatio = new Constant.Ratio(0.15, "Relative ratio of best count for foreground spread reading"); final Constant.Ratio backSpreadRatio = new Constant.Ratio(0.3, "Relative ratio of best count for background spread reading"); final Constant.Ratio spreadFactor = new Constant.Ratio(1.0, "Factor applied on line thickness spread"); final Constant.Ratio minBeamLineRatio = new Constant.Ratio(2.5, "Minimum ratio between beam thickness and line thickness"); final Constant.Ratio maxSecondRatio = new Constant.Ratio(2.0, "Maximum ratio between second and first background peak"); final Constant.Boolean printWatch = new Constant.Boolean(false, "Should we print the StopWatch on binarization?"); } //---------// // Plotter // //---------// /** * In charge of building and displaying a chart on provided runs collection */ private class Plotter { //~ Instance fields ---------------------------------------------------- private final String name; private final int[] values; private final Histogram<Integer> histo; private final double spreadRatio; private final PeakEntry<Double> peak; private final PeakEntry<Double> secondPeak; private final int upper; private final XYSeriesCollection dataset = new XYSeriesCollection(); //~ Constructors ------------------------------------------------------- public Plotter(String name, int[] values, Histogram<Integer> histo, double spreadRatio, PeakEntry<Double> peak, PeakEntry<Double> secondPeak, // if any int upper) { this.name = name; this.values = values; this.histo = histo; this.spreadRatio = spreadRatio; this.peak = peak; this.secondPeak = secondPeak; this.upper = upper; } //~ Methods ------------------------------------------------------------ public void plot(Point upperLeft) { // All values, quorum line & spread line plotValues(); plotQuorumLine(); plotSpreadLine("", peak); // Second peak spread line? if (secondPeak != null) { plotSpreadLine("Second", secondPeak); } // Chart JFreeChart chart = ChartFactory.createXYLineChart(sheet.getId() + " (" + name + " runs)", // Title "Lengths " + ((scale != null) ? scale : "*no scale*"), // X-Axis label "Counts", // Y-Axis label dataset, // Dataset PlotOrientation.VERTICAL, // orientation, true, // Show legend false, // Show tool tips false // urls ); // Hosting frame ChartFrame frame = new ChartFrame(sheet.getId() + " - " + name + " runs", chart, true); frame.pack(); frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); frame.setLocation(upperLeft); frame.setVisible(true); } private void plotQuorumLine() { int threshold = histo.getQuorumValue(quorumRatio); String pc = (int) (quorumRatio * 100) + "%"; XYSeries series = new XYSeries("Quorum@" + pc + ":" + threshold); series.add(0, threshold); series.add(upper, threshold); dataset.addSeries(series); } private void plotSpreadLine(String prefix, PeakEntry<Double> peak) { if (peak != null) { int threshold = histo.getQuorumValue(peak.getValue() * spreadRatio); String pc = (int) (spreadRatio * 100) + "%"; XYSeries series = new XYSeries(prefix + "Spread@" + pc + ":" + threshold); series.add((double) peak.getKey().first, threshold); series.add((double) peak.getKey().second, threshold); dataset.addSeries(series); } } private void plotValues() { Integer key = null; Integer secKey = null; if (peak != null) { double mainKey = peak.getKey().best; key = (int) mainKey; } if (secondPeak != null) { double secondKey = secondPeak.getKey().best; secKey = (int) secondKey; } XYSeries series = new XYSeries("Peak:" + key + "(" + (int) (peak.getValue() * 100) + "%)" + ((secondPeak != null) ? (" & " + secKey) : "")); for (int i = 0; i <= upper; i++) { series.add(i, values[i]); } dataset.addSeries(series); } } }