//----------------------------------------------------------------------------// // // // A l t e r P a t t e r n // // // //----------------------------------------------------------------------------// // <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.pattern; import omr.constant.ConstantSet; import omr.glyph.CompoundBuilder; import omr.glyph.Evaluation; import omr.glyph.Glyphs; import omr.glyph.Shape; import omr.glyph.ShapeSet; import omr.glyph.facets.Glyph; import omr.sheet.Scale; import omr.sheet.SystemInfo; import omr.util.HorizontalSide; import omr.util.Vip; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Rectangle; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.SortedSet; /** * Class {@code AlterPattern} implements a pattern for alteration * glyphs which have been "oversegmented" into stem(s) + other stuff. * <p>This applies for sharp, natural and flat signs. * We use the fact that the stem(s) are rather short and, for the case of sharp * and natural, very close to each other. * * @author Hervé Bitteur */ public class AlterPattern extends GlyphPattern { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(AlterPattern.class); //~ Instance fields -------------------------------------------------------- // // Scale-dependent constants for alter verification private final int maxCloseStemDx; private final int minCloseStemOverlap; private final int maxAlterStemLength; private final int maxNaturalOverlap; private final int flatHeadWidth; private final int flatHeadHeight; // Adapters private final PairAdapter sharpAdapter; private final PairAdapter naturalAdapter; /** Collection of (short) stems, sorted by abscissa. */ private SortedSet<Glyph> stems; //~ Constructors ----------------------------------------------------------- /** * Creates a new AlterPattern object. */ public AlterPattern (SystemInfo system) { super("Alter", system); maxCloseStemDx = scale.toPixels(constants.maxCloseStemDx); minCloseStemOverlap = scale.toPixels(constants.minCloseStemOverlap); maxAlterStemLength = scale.toPixels(constants.maxAlterStemLength); maxNaturalOverlap = scale.toPixels(constants.maxNaturalOverlap); flatHeadWidth = scale.toPixels(constants.flatHeadWidth); flatHeadHeight = scale.toPixels(constants.flatHeadHeight); sharpAdapter = new SharpAdapter(system); naturalAdapter = new NaturalAdapter(system); } //~ Methods ---------------------------------------------------------------- //------------// // runPattern // //------------// /** * Check the neighborhood of all short stems. * * @return the number of cases fixed */ @Override public int runPattern () { int successNb = 0; // Success counter // Sorted short stems stems = retrieveShortStems(); // Look for close stems (sharps & naturals) successNb += checkCloseStems(); // Look for isolated stems (flats) successNb += checkSingleStems(); // Impacted neighbors checkFormerStems(); return successNb; } //-----------------// // checkCloseStems // //-----------------// /** * Verify the case of stems very close to each other since they * may result from oversegmentation of sharp or natural signs. * * @return the number of cases fixed */ private int checkCloseStems () { int nb = 0; for (Glyph glyph : stems) { if (!glyph.isStem()) { continue; // Already consumed } // Retrieve interesting stem pairs in the neighborhood List<StemPair> pairs = getNeighboringPairs(glyph); // Inspect pairs by increasing distance for (StemPair pair : pairs) { if (!pair.left.isStem() || !pair.right.isStem()) { continue; // This pair is no longer relevant } if (pair.isVip()) { logger.info("{} Alter pair: {}", glyph.idString(), pair); } // "hide" the stems to not perturb evaluation pair.left.setShape(null); pair.right.setShape(null); PairAdapter adapter; if (pair.overlap <= maxNaturalOverlap) { logger.debug("NATURAL sign?"); adapter = naturalAdapter; } else { logger.debug("SHARP sign?"); adapter = sharpAdapter; } // Prepare the adapter with proper stem boxes adapter.setStemBoxes(pair.left.getBounds(), pair.right.getBounds()); Glyph compound = system.buildCompound( pair.left, true, system.getGlyphs(), adapter); if (compound != null) { nb++; logger.debug("{}Compound #{} rebuilt as {}", system.getLogPrefix(), compound.getId(), compound.getShape()); } else { // Restore stem shapes pair.left.setShape(Shape.STEM); pair.right.setShape(Shape.STEM); } } } return nb; } //------------------// // checkFormerStems // //------------------// /** * Look for glyphs whose shape was dependent on former stems, * and call their shape into question again. */ private void checkFormerStems () { SortedSet<Glyph> impacted = Glyphs.sortedSet(); for (Glyph glyph : system.getGlyphs()) { if (!glyph.isActive()) { continue; } for (HorizontalSide side : HorizontalSide.values()) { // Retrieve "deassigned" stem if any Glyph stem = glyph.getStem(side); if ((stem != null) && (stem.getShape() != Shape.STEM)) { impacted.add(glyph); } } } if (logger.isDebugEnabled()) { logger.debug( Glyphs.toString("Impacted alteration neighbors", impacted)); } for (Glyph glyph : impacted) { Shape shape = glyph.getShape(); if (ShapeSet.StemSymbols.contains(shape)) { // Trigger a reevaluation (w/o forbidding the current shape) glyph.setShape(null); glyph.allowShape(shape); } // Re-compute glyph features system.computeGlyphFeatures(glyph); } } //------------------// // checkSingleStems // //------------------// /** * Verify the case of isolated short stems since they may result * from oversegmentation of flat signs. * * @return the number of cases fixed */ private int checkSingleStems () { int nb = 0; FlatAdapter flatAdapter = new FlatAdapter(system); for (Glyph glyph : stems) { if (!glyph.isStem()) { continue; } // If stem already has notehead or flag/beam, skip it Set<Glyph> goods = new HashSet<>(); Set<Glyph> bads = new HashSet<>(); glyph.getSymbolsBefore(StemPattern.reliableStemSymbols, goods, bads); glyph.getSymbolsAfter(StemPattern.reliableStemSymbols, goods, bads); if (!goods.isEmpty()) { logger.debug("Skipping good stem {}", glyph); continue; } // "hide" the stems temporarily to not perturb evaluation glyph.setShape(null); Glyph compound = system.buildCompound( glyph, true, system.getGlyphs(), flatAdapter); if (compound != null) { nb++; logger.debug("{}Compound #{} rebuilt as {}", system.getLogPrefix(), compound.getId(), compound.getShape()); } else { // Restore stem shape glyph.setShape(Shape.STEM); } } return nb; } //--------------------// // retrieveShortStems // //--------------------// /** * Retrieve the collection of all stems in the system, * ordered naturally by their abscissa. * * @return the set of short stems */ private SortedSet<Glyph> retrieveShortStems () { final SortedSet<Glyph> shortStems = Glyphs.sortedSet(); for (Glyph glyph : system.getGlyphs()) { if (glyph.isStem() && glyph.isActive()) { // Check stem length if (glyph.getBounds().height <= maxAlterStemLength) { shortStems.add(glyph); } } } return shortStems; } //---------------------// // getNeighboringPairs // //---------------------// /** * Retrieve all pairs of stems, transitively close to the provided * seed, to pickup the most promising pair for natural/sharp alter. * * @param seed the stem seed * @return the collection of stems pairs */ private List<StemPair> getNeighboringPairs (Glyph seed) { List<StemPair> pairs = new ArrayList<>(); // First, come up with candidate stems SortedSet<Glyph> neighbors = Glyphs.sortedSet(Arrays.asList(seed)); Rectangle box = seed.getBounds(); for (Glyph glyph : stems) { if (glyph != seed && glyph.isStem()) { Rectangle glyphBox = glyph.getBounds(); glyphBox.grow(maxCloseStemDx, 0); if (box.intersects(glyphBox)) { neighbors.add(glyph); box.add(glyph.getBounds()); } else if (glyphBox.x > box.x + box.width) { break; } } } // Second, evaluate pairs and keep only the possible ones for (Glyph left : neighbors) { final Rectangle leftBox = left.getBounds(); final int leftX = leftBox.x + (leftBox.width / 2); for (Glyph other : neighbors.tailSet(left)) { if ((other == left)) { continue; } // Check horizontal distance final Rectangle rightBox = other.getBounds(); final int rightX = rightBox.x + (rightBox.width / 2); if (rightX - leftX > maxCloseStemDx) { continue; } // Check vertical overlap final int commonTop = Math.max(leftBox.y, rightBox.y); final int commonBot = Math.min( leftBox.y + leftBox.height, rightBox.y + rightBox.height); final int overlap = commonBot - commonTop; if (overlap < minCloseStemOverlap) { continue; } // Evaluate compatibility double deltaLength = Math.abs(leftBox.height - rightBox.height); double deltaRatio = deltaLength / Math.max(leftBox.height, rightBox.height); pairs.add(new StemPair(left, other, overlap, deltaRatio)); } } Collections.sort(pairs); return pairs; } //~ Inner Classes ---------------------------------------------------------- //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Evaluation.Grade alterMinGrade = new Evaluation.Grade( 0.3, "Minimum grade for sharp/natural sign verification"); Evaluation.Grade flatMinGrade = new Evaluation.Grade( 20d, "Minimum grade for flat sign verification"); Scale.Fraction maxCloseStemDx = new Scale.Fraction( 0.7d, "Maximum horizontal distance for close stems"); Scale.Fraction maxAlterStemLength = new Scale.Fraction( 3.5, "Maximum length for pseudo-stem(s) in alteration sign"); Scale.Fraction maxNaturalOverlap = new Scale.Fraction( 2.0d, "Maximum vertical overlap for natural stems"); Scale.Fraction minCloseStemOverlap = new Scale.Fraction( 0.5d, "Minimum vertical overlap for close stems"); Scale.Fraction flatHeadHeight = new Scale.Fraction( 1d, "Typical height of flat head"); Scale.Fraction flatHeadWidth = new Scale.Fraction( 0.5d, "Typical width of flat head"); } //-------------// // FlatAdapter // //-------------// /** * Compound adapter meant to build flats. */ private class FlatAdapter extends CompoundBuilder.TopShapeAdapter { //~ Constructors ------------------------------------------------------- public FlatAdapter (SystemInfo system) { super( system, constants.flatMinGrade.getValue(), EnumSet.of(Shape.FLAT)); } //~ Methods ------------------------------------------------------------ @Override public Rectangle computeReferenceBox () { final Rectangle stemBox = seed.getBounds(); return new Rectangle( stemBox.x, (stemBox.y + stemBox.height) - flatHeadHeight, flatHeadWidth, flatHeadHeight); } @Override public boolean isCandidateSuitable (Glyph glyph) { if (glyph.isManualShape()) { return false; } Shape shape = glyph.getShape(); return !ShapeSet.StemSymbols.contains(shape) || shape == Shape.BEAM_HOOK; } } //----------------// // NaturalAdapter // //----------------// /** * Compound adapter meant to build naturals. */ private class NaturalAdapter extends PairAdapter { //~ Constructors ------------------------------------------------------- public NaturalAdapter (SystemInfo system) { super(system, EnumSet.of(Shape.NATURAL)); } //~ Methods ------------------------------------------------------------ @Override public Rectangle computeReferenceBox () { Rectangle newBox = getStemsBox(); newBox.grow(maxCloseStemDx / 4, minCloseStemOverlap / 2); return newBox; } } //-------------// // PairAdapter // //-------------// /** * Abstract compound adapter meant to build sharps or naturals * from a pair of close stems. */ private abstract class PairAdapter extends CompoundBuilder.TopShapeAdapter { //~ Instance fields ---------------------------------------------------- protected Rectangle leftBox; protected Rectangle rightBox; //~ Constructors ------------------------------------------------------- public PairAdapter (SystemInfo system, EnumSet<Shape> shapes) { super(system, constants.alterMinGrade.getValue(), shapes); } //~ Methods ------------------------------------------------------------ @Override public boolean isCandidateClose (Glyph glyph) { // We use containment instead of intersection return box.contains(glyph.getBounds()); } @Override public boolean isCandidateSuitable (Glyph glyph) { return !glyph.isManualShape(); } public void setStemBoxes (Rectangle leftBox, Rectangle rightBox) { this.leftBox = leftBox; this.rightBox = rightBox; } protected Rectangle getStemsBox () { if ((leftBox == null) || (rightBox == null)) { throw new NullPointerException("Stem boxes have not been set"); } Rectangle box = new Rectangle(leftBox); box.add(rightBox); return box; } } //--------------// // SharpAdapter // //--------------// /** * Compound adapter meant to build sharps. */ private class SharpAdapter extends PairAdapter { //~ Constructors ------------------------------------------------------- public SharpAdapter (SystemInfo system) { super(system, EnumSet.of(Shape.SHARP)); } //~ Methods ------------------------------------------------------------ @Override public Rectangle computeReferenceBox () { Rectangle newBox = getStemsBox(); newBox.grow(maxCloseStemDx / 2, minCloseStemOverlap / 2); return newBox; } } //----------// // StemPair // //----------// /** * Data about a possible pair of stems for a sharp/natural alter. */ private class StemPair implements Comparable<StemPair>, Vip { /** Stem on left side. */ final Glyph left; /** Stem on right side. */ final Glyph right; /** Vertical overlap. */ final int overlap; /** Info about pair "distance" (to pick the best pair). */ final double distance; boolean vip = false; public StemPair (Glyph left, Glyph right, int overlap, double distance) { this.left = left; this.right = right; this.overlap = overlap; this.distance = distance; if (left.isVip() || right.isVip()) { setVip(); } } /** To sort pairs. */ @Override public int compareTo (StemPair that) { return Double.compare(this.distance, that.distance); } @Override public String toString () { StringBuilder sb = new StringBuilder("{Stems"); sb.append(" #").append(left.getId()); sb.append(" #").append(right.getId()); sb.append(" over:").append(overlap); sb.append(" dist:").append((float) distance); sb.append("}"); return sb.toString(); } @Override public final boolean isVip () { return vip; } @Override public final void setVip () { this.vip = true; } } }