//----------------------------------------------------------------------------// // // // S h a p e C h e c k e r // // // //----------------------------------------------------------------------------// // <editor-fold defaultstate="collapsed" desc="hdr"> // // Copyright © Hervé Bitteur and others 2000-2013. 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.glyph; import omr.constant.Constant; import omr.constant.ConstantSet; import static omr.glyph.Shape.*; import static omr.glyph.ShapeSet.*; import omr.glyph.facets.Glyph; import omr.grid.StaffInfo; import omr.run.Orientation; import omr.score.entity.Barline; import omr.score.entity.Clef; import omr.score.entity.Measure; import omr.score.entity.ScoreSystem; import omr.score.entity.Staff; import omr.score.entity.SystemPart; import omr.sheet.Scale; import omr.sheet.Sheet; import omr.sheet.SystemInfo; import omr.util.HorizontalSide; import omr.util.Predicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Point; import java.awt.Rectangle; import java.util.ArrayList; import java.util.Collection; import java.util.EnumMap; import java.util.EnumSet; import java.util.List; /** * Class {@code ShapeChecker} gathers additional specific shape checks, * still working on symbols in isolation from other symbols, meant to * complement the work done by a shape evaluator. * * <p> * Typically, physical shapes (the *_set shape names) must be mapped to * the right logical shapes using proper additional tests.</p> * * <p> * Checks are made on the glyph only, the only knowledge about current glyph * environment being its staff-based pitch position and the attached stems and * ledgers.</p> * * <p> * Checks made in relation with other symbols are not handled here (because * the other symbols may not have been recognized yet). Such more elaborated * checks are the purpose of {@link omr.glyph.pattern.PatternsChecker}.</p> * * @author Hervé Bitteur */ public class ShapeChecker { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(ShapeChecker.class); /** Singleton */ private static ShapeChecker INSTANCE; /** Small dynamics with no 'P' or 'F' */ private static final EnumSet<Shape> SmallDynamics = EnumSet.copyOf( shapesOf( DYNAMICS_CHAR_M, DYNAMICS_CHAR_R, DYNAMICS_CHAR_S, DYNAMICS_CHAR_Z)); /** Medium dynamics with a 'P' (but no 'F') */ private static final EnumSet<Shape> MediumDynamics = EnumSet.copyOf( shapesOf(DYNAMICS_MP, DYNAMICS_P, DYNAMICS_PP, DYNAMICS_PPP)); /** Tall dynamics with an 'F' */ private static final EnumSet<Shape> TallDynamics = EnumSet.copyOf( shapesOf( DYNAMICS_F, DYNAMICS_FF, DYNAMICS_FFF, DYNAMICS_FP, DYNAMICS_FZ, DYNAMICS_MF, DYNAMICS_RF, DYNAMICS_RFZ, DYNAMICS_SF, DYNAMICS_SFFZ, DYNAMICS_SFP, DYNAMICS_SFPP, DYNAMICS_SFZ)); //~ Instance fields -------------------------------------------------------- /** Map of Shape => Sequence of checkers */ private final EnumMap<Shape, Collection<Checker>> checkerMap; /** Checker that can be used on its own. */ private Checker stemChecker; //~ Constructors ----------------------------------------------------------- //--------------// // ShapeChecker // //--------------// private ShapeChecker () { checkerMap = new EnumMap<>(Shape.class); registerChecks(); } //~ Methods ---------------------------------------------------------------- //-------------// // getInstance // //-------------// public static ShapeChecker getInstance () { if (INSTANCE == null) { INSTANCE = new ShapeChecker(); } return INSTANCE; } //-----------// // checkStem // //-----------// /** * Basic check for a stem candidate, using gap to closest staff. * * @param system containing system * @param glyph stem candidate * @return true if OK */ public boolean checkStem (SystemInfo system, Glyph glyph) { return stemChecker.check(system, null, glyph, null); } //----------// // annotate // //----------// /** * Run a series of checks on the provided glyph, based on the * candidate shape, and annotate the evaluation accordingly. * This annotation can even change the shape itself, thus allowing a move * from physical shape (such as WEDGE_set) to proper logical shape * (CRESCENDO or DECRESCENDO). * * @param system the containing system * @param eval the evaluation to populate * @param glyph the glyph to check for a shape * @param features the glyph features */ public void annotate (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { if (!constants.applySpecificCheck.getValue()) { return; } // if (glyph.isVip()) { // logger.info("Checking " + glyph); // } // Collection<Checker> checks = checkerMap.get(eval.shape); if (checks == null) { return; } for (Checker checker : checks) { if (!(checker.check(system, eval, glyph, features))) { if (eval.failure != null) { eval.failure = new Evaluation.Failure( checker.name + ":" + eval.failure); } else { eval.failure = new Evaluation.Failure(checker.name); } return; } } } //-------// // relax // //-------// /** * Take into account the fact that the provided glyph has been * (certainly manually) assigned the provided shape. * So update the tests internals accordingly. * * @param shape the assigned shape * @param glyph the glyph at hand * @param features the glyph features * @param sheet the containing sheet */ public void relax (Shape shape, Glyph glyph, double[] features, Sheet sheet) { Collection<Checker> checks = checkerMap.get(shape); if (checks == null) { return; } for (Checker checker : checks) { checker.relax(shape, glyph, features, sheet); } } //------------// // addChecker // //------------// /** * Add a checker to a series of shapes. * * @param checker the checker to add * @param shapes the shape(s) for which the check applies */ private void addChecker (Checker checker, Shape... shapes) { for (Shape shape : shapes) { Collection<Checker> checks = checkerMap.get(shape); if (checks == null) { checks = new ArrayList<>(); checkerMap.put(shape, checks); } checks.add(checker); } } //------------// // addChecker // //------------// /** * Add a checker to a series of shape ranges. * * @param checker the checker to add * @param shapeRanges the shape range(s) to which the check applies */ private void addChecker (Checker checker, ShapeSet... shapeRanges) { for (ShapeSet range : shapeRanges) { addChecker(checker, range.getShapes().toArray(new Shape[0])); } } //--------------// // correctShape // //--------------// private boolean correctShape (SystemInfo system, Glyph glyph, Evaluation eval, Shape newShape) { if (eval.shape != newShape) { // logger.info( // system.getLogPrefix() + "G#" + glyph.getId() + " " + // eval.shape + " -> " + newShape); eval.shape = newShape; } return true; } //------------// // logLogical // //------------// /** * Meant for debugging the mapping from physical to logical shape. * * @param system related system * @param glyph the glyph at hand * @param eval the physical evaluation * @param newShape the chosen logical shape */ private void logLogical (SystemInfo system, Glyph glyph, Evaluation eval, Shape newShape) { // For debugging only if (eval.grade >= 0.1) { logger.info("{}{} {} weight:{} {} corrected as {}", system.getLogPrefix(), glyph, eval, glyph.getWeight(), glyph.getBounds(), newShape); } } //----------------// // registerChecks // //----------------// /** * Populate the checkers map. */ private void registerChecks () { // // General constraint check on weight, width, height // new Checker("Constraint", allPhysicalShapes) { // @Override // public boolean check (SystemInfo system, // Evaluation eval, // Glyph glyph, // double[] features) // { // if (!constants.applyConstraintsCheck.getValue()) { // return true; // } // // // Apply registered parameters constraints // return GlyphRegression.getInstance() // .constraintsMatched(features, eval); // } // // @Override // public void relax (Shape shape, // Glyph glyph, // double[] features, // Sheet sheet) // { // // Here relax the constraints if so needed // boolean extended = GlyphRegression.getInstance() // .includeSample( // features, // shape); // logger.info( // "Constraints " + (extended ? "extended" : "included") + // " for glyph#" + glyph.getId() + " as " + shape); // // // Record the glyph description to disk // GlyphRepository.getInstance() // .recordOneGlyph(glyph, sheet); // } // }; new Checker("NotWithinWidth", allPhysicalShapes) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // They must be within the abscissa bounds of the system // Except a few shapes Shape shape = eval.shape; if ((shape == BRACKET) || (shape == BRACE) || (shape == TEXT) || (shape == CHARACTER)) { return true; } Rectangle glyphBox = glyph.getBounds(); if (((glyphBox.x + glyphBox.width) < system.getLeft()) || (glyphBox.x > system.getRight())) { return false; } return true; } }; new Checker("MeasureRest", HW_REST_set) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { int pp = (int) Math.rint(2 * glyph.getPitchPosition()); if (pp == -1) { eval.shape = Shape.HALF_REST; return true; } else if (pp == -3) { eval.shape = Shape.WHOLE_REST; return true; } else { eval.failure = new Evaluation.Failure("pitch"); return false; } } }; new Checker("NotWithinStaffHeight", Clefs) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Must be within staff height return Math.abs(glyph.getPitchPosition()) < 4; } }; new Checker("WithinStaffHeight", Dynamics) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Must be outside staff height return Math.abs(glyph.getPitchPosition()) > 4; } }; new Checker("TooFarFromLeftBar", Keys) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // They must be rather close to the left side of the measure ScoreSystem scoreSystem = system.getScoreSystem(); Scale scale = scoreSystem.getScale(); double maxKeyXOffset = scale.toPixels( constants.maxKeyXOffset); Rectangle box = glyph.getBounds(); Point point = box.getLocation(); SystemPart part = scoreSystem.getPartAt(point); Measure measure = part.getMeasureAt(point); if (measure == null) { return true; } Barline insideBar = measure.getInsideBarline(); Staff staff = part.getStaffAt(point); if (staff == null) { return false; } Clef clef = measure.getFirstMeasureClef(staff.getId()); int start = (clef != null) ? (clef.getBox().x + clef.getBox().width) : ((insideBar != null) ? insideBar.getLeftX() : measure.getLeftX()); return (point.x - start) <= maxKeyXOffset; } }; new Checker("CommonCutTime", COMMON_TIME) { private Predicate<Glyph> stemPredicate = new Predicate<Glyph>() { @Override public boolean check (Glyph entity) { return entity.getShape() == Shape.STEM; } }; @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // COMMON_TIME shape is easily confused with CUT_TIME // Check presence of a "pseudo-stem" Rectangle box = glyph.getBounds(); box.grow(-box.width / 4, 0); List<Glyph> neighbors = system.lookupIntersectedGlyphs( box, glyph); if (Glyphs.contains(neighbors, stemPredicate)) { eval.shape = Shape.CUT_TIME; } return true; } }; new Checker("Hook", BEAM_HOOK) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Check we have exactly 1 stem if (glyph.getStemNumber() != 1) { eval.failure = new Evaluation.Failure("stem!=1"); return false; } // Hook slope is not reliable, so this test is disabled // if (!validBeamHookSlope(glyph)) { // eval.failure = new Evaluation.Failure("slope"); // // return false; // } return true; } }; new Checker("Beams", shapesOf(BEAM, BEAM_2, BEAM_3)) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { Integer singleThickness = system.getScoreSystem().getScale(). getMainBeam(); if (singleThickness != null) { // Check we have thickness consistent with the number of // beams (since we know single beam thickness) double meanThickness = glyph.getMeanThickness( Orientation.HORIZONTAL); int nb = (int) Math.rint( meanThickness / singleThickness); switch (nb) { case 1: return correctShape(system, glyph, eval, BEAM); case 2: return correctShape(system, glyph, eval, BEAM_2); case 3: return correctShape(system, glyph, eval, BEAM_3); default: ///logger.warn("Bad beam #" + glyph.getId() + " nb:" + nb); eval.failure = new Evaluation.Failure("beamThickness"); return false; } } else { return true; } } }; // Shapes that require a stem on the left side new Checker( "noLeftStem", shapesOf(FlagSets, shapesOf(Flags.getShapes()))) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { return glyph.getStem(HorizontalSide.LEFT) != null; } }; // Shapes that require a stem nearby new Checker("noStem", StemSymbols) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { return glyph.getStemNumber() >= 1; } }; new Checker("Text", TEXT, CHARACTER) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Check reasonable height (Cannot be too tall when close to staff) double maxHeight = (Math.abs(glyph.getPitchPosition()) >= constants.minTitlePitchPosition. getValue()) ? constants.maxTitleHeight.getValue() : constants.maxLyricsHeight.getValue(); if (glyph.getNormalizedHeight() >= maxHeight) { eval.failure = new Evaluation.Failure("tooHigh"); return false; } // // Check there is no huge horizontal gap between parts // if (hugeGapBetweenParts(glyph)) { // eval.failure = new Evaluation.Failure("gaps"); // // return false; // } return true; } // /** // * Browse the collection of provided glyphs to make sure there // * is no huge horizontal gap included // * @param glyphs the collection of glyphs that compose the text // * candidate // * @param sheet needed for scale of the context // * @return true if gap found // */ // private boolean hugeGapBetweenParts (Glyph compound) // { // if (compound.getParts() // .isEmpty()) { // return false; // } // // // Sort glyphs by abscissa // List<Glyph> glyphs = new ArrayList<Glyph>( // compound.getParts()); // Collections.sort(glyphs, Glyph.abscissaComparator); // // final Scale scale = new Scale(glyphs.get(0).getInterline()); // final int maxGap = scale.toPixels(constants.maxTextGap); // int gapStart = 0; // Glyph prev = null; // // for (Glyph glyph : glyphs) { // Rectangle box = glyph.getBounds(); // // if (prev != null) { // if ((box.x - gapStart) > maxGap) { // if (logger.isDebugEnabled()) { // logger.debug( // "huge gap detected between glyphs #" + // prev.getId() + " & " + glyph.getId()); // } // // return true; // } // } // // prev = glyph; // gapStart = (box.x + box.width) - 1; // } // // return false; // } }; new Checker("FullTimeSig", FullTimes) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { double absPos = Math.abs(glyph.getPitchPosition()); double maxDy = constants.maxTimePitchPositionMargin.getValue(); // A full time shape must be on 0 position if (absPos > maxDy) { eval.failure = new Evaluation.Failure("pitch"); return false; } // Total height for a complete time sig is staff height if (glyph.getNormalizedHeight() > 4.5) { eval.failure = new Evaluation.Failure("tooHigh"); return false; } return true; } }; new Checker("PartialTimeSig", TIME_69_set, PartialTimes) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { double absPos = Math.abs(glyph.getPitchPosition()); double maxDy = constants.maxTimePitchPositionMargin.getValue(); // A partial time shape must be on -2 or +2 positions if (Math.abs(absPos - 2) > maxDy) { eval.failure = new Evaluation.Failure("pitch"); return false; } return true; } }; new Checker("StaffGap", Notes.getShapes(), NoteHeads.getShapes(), Rests.getShapes(), Dynamics.getShapes(), Articulations.getShapes()) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // A note / rest / dynamic cannot be too far from a staff Point center = glyph.getAreaCenter(); StaffInfo staff = system.getStaffAt(center); // Staff may be null when we are modifying system boundaries if (staff == null) { return false; } int gap = staff.getGapTo(glyph); int maxGap = system.getScoreSystem().getScale().toPixels( constants.maxGapToStaff); return gap <= maxGap; } }; stemChecker = new Checker("StaffStemGap", shapesOf(shapesOf(STEM), shapesOf(Beams.getShapes(), Flags.getShapes(), FlagSets))) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // A beam / flag / stem cannot be too far from a staff Point center = glyph.getAreaCenter(); StaffInfo staff = system.getStaffAt(center); // Staff may be null for a very long glyph across systems if (staff == null) { return false; } int gap = staff.getGapTo(glyph); int maxGap = system.getScoreSystem().getScale().toPixels( constants.maxStemGapToStaff); return gap <= maxGap; } }; new Checker("SmallDynamics", SmallDynamics) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Check height return glyph.getNormalizedHeight() <= constants.maxSmallDynamicsHeight. getValue(); } }; new Checker("MediumDynamics", MediumDynamics) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Check height return glyph.getNormalizedHeight() <= constants.maxMediumDynamicsHeight. getValue(); } }; new Checker("TallDynamics", TallDynamics) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Check height return glyph.getNormalizedHeight() <= constants.maxTallDynamicsHeight. getValue(); } }; new Checker("BelowStaff", Pedals) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Pedal marks must be below the staff return glyph.getPitchPosition() > 4; } }; new Checker("Tuplet", Tuplets) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Tuplets cannot be too far from a staff if (Math.abs(glyph.getPitchPosition()) > constants.maxTupletPitchPosition. getValue()) { eval.failure = new Evaluation.Failure("pitch"); return false; } // // Simply check the tuplet character via OCR, if available // // Nota: We must avoid multiple OCR calls on the same glyph // if (Language.getOcr() // .isAvailable()) { // if (glyph.isTransient()) { // glyph = system.registerGlyph(glyph); // } // // BasicContent textInfo = glyph.getTextInfo(); // OcrLine line; // // if (textInfo.getOcrContent() == null) { // String language = system.getScoreSystem() // .getScore() // .getLanguage(); // List<OcrLine> lines = textInfo.recognizeGlyph( // language); // // if ((lines != null) && !lines.isEmpty()) { // line = lines.get(0); // textInfo.setOcrInfo(language, line); // } // } // // line = textInfo.getOcrLine(); // // if (line != null) { // String str = line.value; // Shape shape = eval.shape; // // if (shape == TUPLET_THREE) { // if (str.equals("3")) { // return true; // } // // //eval.shape = CHARACTER; // } // // if (shape == TUPLET_SIX) { // if (str.equals("6")) { // return true; // } // // //eval.shape = CHARACTER; // } // // eval.failure = new Evaluation.Failure("ocr"); // // return false; // } // } return true; } }; new Checker("LongRest", LONG_REST) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Must be centered on pitch position 0 if (Math.abs(glyph.getPitchPosition()) > 0.5) { eval.failure = new Evaluation.Failure("pitch"); return false; } return true; } }; new Checker("Breve", BREVE_REST) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Must be centered on pitch position -1 if (Math.abs(glyph.getPitchPosition() + 1) > 0.5) { eval.failure = new Evaluation.Failure("pitch"); return false; } return true; } }; new Checker("Braces", BRACE, BRACKET) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Must be centered on left of part barline Rectangle box = glyph.getBounds(); Point left = new Point( box.x, box.y + (box.height / 2)); if (left.x > system.getLeft()) { eval.failure = new Evaluation.Failure("notOnLeft"); return false; } // Make sure at least a staff interval is embraced boolean embraced = false; int intervalTop = Integer.MIN_VALUE; for (StaffInfo staff : system.getStaves()) { if (intervalTop != Integer.MIN_VALUE) { int intervalBottom = staff.getFirstLine().yAt(box.x); if ((intervalTop >= box.y) && (intervalBottom <= (box.y + box.height))) { embraced = true; // Ok for this one break; } } intervalTop = staff.getLastLine().yAt(box.x); } if (!embraced) { eval.failure = new Evaluation.Failure( "noStaffEmbraced"); return false; } return true; } }; new Checker("WholeSansLedgers", WHOLE_NOTE) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Check that whole notes are not too far from staves // without ledgers Point point = glyph.getAreaCenter(); StaffInfo staff = system.getStaffAt(point); if (staff == null) { return false; } double pitch = staff.pitchPositionOf(point); if (Math.abs(pitch) <= 6) { return true; } return staff.getClosestLedger(point) != null; } }; new Checker("SystemTop", DAL_SEGNO, DA_CAPO, SEGNO, CODA, BREATH_MARK) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Check that these markers are just above first staff Point point = glyph.getAreaCenter(); StaffInfo staff = system.getStaffAt(point); if (staff != system.getFirstStaff()) { return false; } double pitch = staff.pitchPositionOf(point); return pitch <= -5; } }; new Checker("Fermata_set", FERMATA_set) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Use moment n21 to differentiate between V & ^ // TBD: We could use pitch position as well? double n21 = glyph.getGeometricMoments().getN21(); Shape newShape = (n21 > 0) ? FERMATA : FERMATA_BELOW; ///logLogical(system, glyph, eval, newShape); eval.shape = newShape; return true; } }; new Checker("FLAG_*_set", FlagSets) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { Shape newShape = null; boolean covar = glyph.getGeometricMoments().getN11() > 0; switch (eval.shape) { case FLAG_1_set: newShape = covar ? FLAG_1 : FLAG_1_UP; break; case FLAG_2_set: newShape = covar ? FLAG_2 : FLAG_2_UP; break; case FLAG_3_set: newShape = covar ? FLAG_3 : FLAG_3_UP; break; case FLAG_4_set: newShape = covar ? FLAG_4 : FLAG_4_UP; break; case FLAG_5_set: newShape = covar ? FLAG_5 : FLAG_5_UP; break; } ///logLogical(system, glyph, eval, newShape); eval.shape = newShape; return true; } }; new Checker("TIME_69_set", TIME_69_set) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Use moment n12 to differentiate between <(6) & >(9) double n12 = glyph.getGeometricMoments().getN12(); Shape newShape = (n12 > 0) ? TIME_NINE : TIME_SIX; ///logLogical(system, glyph, eval, newShape); eval.shape = newShape; return true; } }; new Checker("TURN_set", TURN_set) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { Shape newShape; // Use aspect to detect turn_up double aspect = glyph.getAspect(Orientation.VERTICAL); if (aspect > 1) { newShape = TURN_UP; } else { // Use xy covariance boolean covar = glyph.getGeometricMoments().getN11() > 0; newShape = covar ? TURN : INVERTED_TURN; } ///logLogical(system, glyph, eval, newShape); eval.shape = newShape; return true; } }; new Checker("Wedge_set", WEDGE_set) { @Override public boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features) { // Use moment n12 to differentiate between < & > double n12 = glyph.getGeometricMoments().getN12(); Shape newShape = (n12 > 0) ? CRESCENDO : DECRESCENDO; ///logLogical(system, glyph, eval, newShape); eval.shape = newShape; return true; } }; } //~ Inner Classes ---------------------------------------------------------- //---------// // Checker // //---------// /** * A checker runs a specific check for a given glyph with respect to * a collection of candidate shapes. */ private abstract class Checker { //~ Instance fields ---------------------------------------------------- /** Unique name for this check */ public final String name; //~ Constructors ------------------------------------------------------- public Checker (String name, Shape... shapes) { this.name = name; addChecker(this, shapes); } public Checker (String name, Collection<Shape> shapes) { this.name = name; addChecker(this, shapes.toArray(new Shape[0])); } public Checker (String name, Collection<Shape>... shapes) { this.name = name; Collection<Shape> allShapes = new ArrayList<>(); for (Collection<Shape> col : shapes) { allShapes.addAll(col); } addChecker(this, allShapes.toArray(new Shape[allShapes.size()])); } public Checker (String name, ShapeSet... shapeSets) { this.name = name; addChecker(this, shapeSets); } public Checker (String name, Shape shape, Collection<Shape> collection) { this.name = name; List<Shape> all = new ArrayList<>(); all.add(shape); all.addAll(collection); addChecker(this, all.toArray(new Shape[0])); } public Checker (String name, Shape shape) { this.name = name; List<Shape> all = new ArrayList<>(); all.add(shape); addChecker(this, all.toArray(new Shape[0])); } //~ Methods ------------------------------------------------------------ /** * Run the specific test. * * @param system the containing system * @param eval the partially-filled evaluation (eval.shape is an * input/output, eval.grade and eval.failure are outputs) * @param glyph the glyph at hand * @param features the glyph features * @return true if OK, false otherwise */ public abstract boolean check (SystemInfo system, Evaluation eval, Glyph glyph, double[] features); /** * Take into account the fact that the provided glyph has been * (certainly manually) assigned the provided shape. * So update the test internals accordingly. * * @param shape the assigned shape * @param glyph the glyph at hand * @param features the glyph features * @param sheet the containing sheet */ public void relax (Shape shape, Glyph glyph, double[] features, Sheet sheet) { // Void by default } @Override public String toString () { return name; } } //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Constant.Boolean applySpecificCheck = new Constant.Boolean( true, "Should we apply specific checks on shape candidates?"); Constant.Boolean applyConstraintsCheck = new Constant.Boolean( true, "Should we apply constraints checks on shape candidates?"); Scale.Fraction maxTitleHeight = new Scale.Fraction( 4d, "Maximum normalized height for a title text"); Scale.Fraction maxLyricsHeight = new Scale.Fraction( 2.5d, "Maximum normalized height for a lyrics text"); Constant.Double minTitlePitchPosition = new Constant.Double( "PitchPosition", 15d, "Minimum absolute pitch position for a title"); Constant.Double maxTupletPitchPosition = new Constant.Double( "PitchPosition", 15d, "Minimum absolute pitch position for a tuplet"); Constant.Double maxTimePitchPositionMargin = new Constant.Double( "PitchPosition", 1d, "Maximum absolute pitch position margin for a time signature"); Scale.Fraction maxTextGap = new Scale.Fraction( 5.0, "Maximum value for a horizontal gap between glyphs of the same text"); Scale.Fraction maxKeyXOffset = new Scale.Fraction( 2, "Maximum horizontal offset for a key since clef or measure start"); Scale.Fraction maxSmallDynamicsHeight = new Scale.Fraction( 1.5, "Maximum height for small dynamics (no p, no f)"); Scale.Fraction maxMediumDynamicsHeight = new Scale.Fraction( 2, "Maximum height for small dynamics (no p, no f)"); Scale.Fraction maxTallDynamicsHeight = new Scale.Fraction( 2.5, "Maximum height for small dynamics (no p, no f)"); Scale.Fraction maxGapToStaff = new Scale.Fraction( 8, "Maximum vertical gap between a note-like glyph and closest staff"); Scale.Fraction maxStemGapToStaff = new Scale.Fraction( 12, "Maximum vertical gap between a stem and closest staff"); } }