//----------------------------------------------------------------------------// // // // S l u r I n s p e c t o 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.pattern; import omr.constant.Constant; import omr.constant.ConstantSet; import omr.glyph.CompoundBuilder; import omr.glyph.Evaluation; import omr.glyph.Grades; import omr.glyph.Shape; import omr.glyph.ShapeSet; import omr.glyph.facets.BasicGlyph; import omr.glyph.facets.Glyph; import omr.grid.StaffInfo; import omr.lag.Section; import omr.lag.Sections; import omr.math.Barycenter; import omr.math.Circle; import omr.math.PointsCollector; import static omr.run.Orientation.*; import omr.sheet.Scale; import omr.sheet.SystemInfo; import omr.util.HorizontalSide; import static omr.util.HorizontalSide.*; import omr.util.Wrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Rectangle; import java.awt.geom.CubicCurve2D; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; import java.util.Iterator; import java.util.List; /** * Class {@code SlurInspector} encapsulates physical processing * dedicated to inspection at system level of glyphs with SLUR shape. * * @author Hervé Bitteur */ public class SlurInspector extends GlyphPattern { //~ Static fields/initializers --------------------------------------------- /** * TODO: * - extendSlur() should extend glyph by glyph * - extendSlurSection() & collectMemberSections() should be factorized */ /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(SlurInspector.class); /** Shapes suitable for extensions. */ private static final EnumSet<Shape> extShapes = EnumSet.copyOf( ShapeSet.shapesOf( ShapeSet.Dots, ShapeSet.shapesOf(Shape.CLUTTER, Shape.CHARACTER, Shape.LEDGER, Shape.TENUTO))); //~ Instance fields -------------------------------------------------------- // /** Compound adapter to extend slurs */ private final SlurCompoundAdapter adapter; /** Scale-dependent parameters. */ private final Parameters params; //~ Constructors ----------------------------------------------------------- //---------------// // SlurInspector // //---------------// /** * Creates a new SlurInspector object. * * @param system The dedicated system */ public SlurInspector (SystemInfo system) { super("Slur", system); adapter = new SlurCompoundAdapter(system); params = new Parameters(system.getSheet().getScale()); } //~ Methods ---------------------------------------------------------------- //---------------// // computeCircle // //---------------// /** * Compute the circle which best approximates the pixels of a given * collection of sections. * We use a rather simple approach, based on 3 defining points (slur ending * points, plus a middle point) which gives good results. * If resulting distance is too high (and if slur width is large enough), * we fall back using plain fitting on all sections points. * * @param sections the collection of sections to fit the circle upon * @return the best circle possible */ public Circle computeCircle (Collection<? extends Section> sections) { final Rectangle box = Sections.getBounds(sections); // Cumulate points from sections PointsCollector collector = new PointsCollector(box); for (Section section : sections) { section.cumulate(collector); } int[] intXX = collector.getXValues(); int[] intYY = collector.getYValues(); double[] xx = new double[collector.getSize()]; double[] yy = new double[collector.getSize()]; for (int i = 0; i < xx.length; i++) { xx[i] = intXX[i]; yy[i] = intYY[i]; } // We force 3 defining points Point2D left = getSlurPointNearX(box.x, sections, box); Point2D right = getSlurPointNearX(box.x + box.width, sections, box); Point2D middle = getSlurPointNearX( box.x + (box.width / 2), sections, box); // Adjust middle abscissa according to slur orientation double slope = (right.getY() - left.getY()) / (right.getX() - left.getX()); Point2D inter = new Point2D.Double( middle.getX(), left.getY() + ((middle.getX() - left.getX()) * slope)); double dy = middle.getY() - inter.getY(); double dx = -dy * (slope / (1 + slope * slope)); middle = getSlurPointNearX( box.x + (box.width / 2) + (int) Math.rint(dx), sections, box); Circle circle = new Circle(left, middle, right, xx, yy); // Switch to points fitting, if needed if ((circle.getDistance() > params.maxCircleDistance)) { logger.debug("Using total fit for slur {}", box); circle = new Circle(xx, yy); } return circle; } //-----------// // getCircle // //-----------// /** * Report the circle which best approximates the pixels of a given * glyph. * * @param glyph The glyph to fit the circle on * @return The best circle possible */ public Circle getCircle (Glyph glyph) { Circle circle = glyph.getCircle(); if (circle == null) { circle = computeCircle(glyph.getMembers()); glyph.setCircle(circle); } return circle; } //------------// // runPattern // //------------// /** * Check all the slur glyphs in the given system, and try to * correct the invalid ones if any. * * @return the number of invalid slurs that are fixed * * <p><b>Synopsis:</b> * <pre> * + extendSlur() // attempt to get to a valid larger slur * + extendSlurSections() * + isValid() * + trimSlur() // attempt to get to a valid smaller slur * + collectMemberSections() * + detectIsolatedSections() * + buildFinalSlur() * </pre> */ @Override public int runPattern () { // Make a list of all slur glyphs to be checked in this system // (So as to free the system glyphs list for on-the-fly modifications) List<Glyph> slurs = new ArrayList<>(); int modifs = 0; for (Glyph glyph : system.getGlyphs()) { if (glyph.getShape() == Shape.SLUR) { if (glyph.isManualShape()) { glyph.addAttachment("^", getCircle(glyph).getCurve()); } else { slurs.add(glyph); } } } // First pass to extend existing slurs List<Glyph> toAdd = new ArrayList<>(); for (Iterator<Glyph> it = slurs.iterator(); it.hasNext();) { Glyph slur = it.next(); // Skip slurs just been 'merged' with another one if (!slur.isActive()) { continue; } // Extend this slur as much as possible try { Glyph largerSlur = extendSlur(slur); if (largerSlur != null) { toAdd.add(largerSlur); it.remove(); modifs++; } } catch (NoSlurCurveException ex) { if (logger.isDebugEnabled()) { logger.info("{}Abnormal curve slur#{}", system.getSheet().getLogPrefix(), slur.getId()); } slur.setShape(null); it.remove(); modifs++; } catch (Exception ex) { logger.warn("Error in extending slur#" + slur.getId(), ex); } } slurs.addAll(toAdd); // Second pass to check each slur validity for (Glyph slur : slurs) { // Skip slurs just been 'merged' with another one if (!slur.isActive()) { continue; } // Check slur validity if (!isValid(slur)) { // Extension has already been tried to no avail // So, just try to trim the slur down try { if (trimSlur(slur) == null) { slur.setShape(null); } } catch (Exception ex) { logger.warn( "Error in trimming slur#" + slur.getId(), ex); } modifs++; } else if (logger.isDebugEnabled()) { logger.debug("Valid slur {}", slur.getId()); slur.addAttachment("^", getCircle(slur).getCurve()); } } return modifs; } //----------// // trimSlur // //----------// /** * For large glyphs, we suspect a slur with a stuck object, * so the strategy is to rebuild the true Slur portions from the * underlying sections. * * @param oldSlur the spurious slur * @return the extracted slur glyph, if any */ public Glyph trimSlur (Glyph oldSlur) { /** * Sections are first ordered by decreasing weight and * continuously tested via the distance to the best * approximating circle. * Sections whose weight is under a given threshold are appended to the * slur only if the resulting circle distance gets lower. * * The "good" sections are put into the "kept" collection. * Sections left over are put into the "left" collection in order to be * used to rebuild the stuck object(s). */ if (oldSlur.isVip() || logger.isDebugEnabled()) { logger.info("Trimming slur {}", oldSlur.idString()); } // Get a COPY of the member list, sorted by decreasing weight */ List<Section> members = new ArrayList<>(oldSlur.getMembers()); Collections.sort(members, Section.reverseWeightComparator); // Find the suitable seed Wrapper<Double> seedDist = new Wrapper<>(); Section seedSection = findSeedSection(members, seedDist); // If no significant section has been found, just give up if (seedSection == null) { if (oldSlur.getShape() == Shape.SLUR) { oldSlur.setShape(null); } return null; } // Sections collected (including seedSection) List<Section> collected = collectMemberSections( members, seedSection, seedDist.value); // Sections left over List<Section> left = new ArrayList<>(members); left.removeAll(collected); // Sections too far from the other ones List<Section> isolated = detectIsolatedSections(seedSection, collected); collected.removeAll(isolated); left.addAll(isolated); if (!collected.isEmpty()) { Glyph newSlur = null; try { // Make sure we do have a suitable slur newSlur = buildFinalSlur(collected); if (newSlur != null) { if (oldSlur.isVip() || logger.isDebugEnabled()) { logger.info("Trimmed slur #{} as smaller #{}", oldSlur.getId(), newSlur.getId()); } } else { if (oldSlur.isVip() || logger.isDebugEnabled()) { logger.info("Giving up slur #{} w/ {}", oldSlur.getId(), collected); } left.addAll(collected); } return newSlur; } catch (Exception ex) { left.addAll(collected); return null; } finally { // Remove former oldSlur glyph if (oldSlur != newSlur) { oldSlur.setShape(null); // Free the sections left over (useful???) for (Section section : left) { section.setGlyph(null); } } } } else { logger.warn("{} No section left when trimming slur #{}", system.getScoreSystem().getContextString(), oldSlur.getId()); return null; } } //----------------// // buildFinalSlur // //----------------// /** * Try to build a valid slur from a collection of sections. * * @param sections the slur sections * @return the valid slur if any, null otherwise */ private Glyph buildFinalSlur (List<Section> sections) { if (null == getInvalidity(sections, null)) { // Build new slur glyph with sections kept Glyph newGlyph = new BasicGlyph(params.interline); for (Section section : sections) { newGlyph.addSection(section, Glyph.Linking.LINK_BACK); } // Beware, the newGlyph may now belong to a different system SystemInfo newSystem = system.getSheet().getSystemOf(newGlyph); // Check whether SLUR is not forbidden for this glyph newGlyph = newSystem.registerGlyph(newGlyph); if (newGlyph.isShapeForbidden(Shape.SLUR)) { return null; } newGlyph = newSystem.addGlyph(newGlyph); newGlyph.setShape(Shape.SLUR); newGlyph.addAttachment("^", getCircle(newGlyph).getCurve()); return newGlyph; } else { return null; } } //-----------------------// // collectMemberSections // //-----------------------// /** * From the provided members, find all sections well located * on the slur circle, including the seed section. * We start from the best seed section, then grow incrementally with * compatible sections, continuously checking distance to resulting circle. * * @param members the glyph sections * @param seedSection the starting seed section * @param lastDistance the fitting distance to current circle * @return the list of sections collected (including seed section) */ private List<Section> collectMemberSections (List<Section> members, Section seedSection, double lastDistance) { List<Section> collected = new ArrayList<>(); // We impose the seed collected.add(seedSection); for (Section section : members) { section.setProcessed(false); } // Let's grow the seed incrementally as much as possible Rectangle slurBox = seedSection.getBounds(); seedSection.setProcessed(true); boolean growing = true; while (growing) { growing = false; for (Section section : members) { if (section.isProcessed()) { continue; } // Need connection Rectangle sctBox = section.getBounds(); sctBox.grow(1, 1); if (!sctBox.intersects(slurBox)) { continue; } logger.debug("Trying {}", section); // Try a circle List<Section> config = new ArrayList<>(collected); config.add(section); try { Circle circle = computeCircle(config); double distance = circle.getDistance(); logger.debug("dist={}", distance); if (distance <= extendedDistance(lastDistance)) { collected.add(section); lastDistance = distance; section.setProcessed(true); slurBox.add(section.getBounds()); growing = true; logger.debug("Keep {}", section); } else { logger.debug("Discard {}", section); } } catch (Exception ex) { logger.debug("{} w/ {}", ex.getMessage(), section); } } } return collected; } //------------------------// // detectIsolatedSections // //------------------------// /** * Detect any section which is too far from the other ones. * * @param seedSection the initial seed section * @param collected the sections collected, including seed section * @return the collection of isolated sections found */ private List<Section> detectIsolatedSections (Section seedSection, List<Section> collected) { final List<Section> isolated = new ArrayList<>(collected); final Rectangle slurBox = seedSection.getBounds(); boolean makingProgress; do { makingProgress = false; for (Iterator<Section> it = isolated.iterator(); it.hasNext();) { Section section = it.next(); Rectangle sectBox = section.getBounds(); sectBox.grow(params.slurBoxDx, params.slurBoxDy); if (sectBox.intersects(slurBox)) { slurBox.add(sectBox); it.remove(); makingProgress = true; } } } while (makingProgress); return isolated; } //------------// // extendSlur // //------------// /** * Try to build a compound glyph with compatible neighboring * glyphs, and test the validity of the resulting slur. * * @param root the slur glyph to extend * @return the extended slur glyph if any, or null. A non-null glyph * is returned IFF we have found a slur which is both larger than * the initial slur and valid. */ private Glyph extendSlur (Glyph root) { // The best compound obtained so far Glyph bestSlur = null; // Loop on extensions, left then right sides for (HorizontalSide side : HorizontalSide.values()) { // Extend as far as possible on the desired side adapter.setSide(side); SideLoop: while (true) { if (root.isVip() || logger.isDebugEnabled()) { logger.info("Trying to {} extend slur #{}", side, root.getId()); } // Look at neighboring glyphs (TODO: should be incremental?) Glyph compound = system.buildCompound( root, true, // include seed system.getGlyphs(), adapter); if (compound != null) { if (root.isVip() || logger.isDebugEnabled()) { logger.info("Slur #{} {} extended as #{}", root.getId(), side, compound.getId()); if (root.isVip()) { compound.setVip(); } } bestSlur = compound; root = compound; } else { // Look at neighboring sections Glyph sectSlur = extendSlurSections(root, side); if (sectSlur != null) { if (root.isVip() || logger.isDebugEnabled()) { logger.info("sectSlur: {}", sectSlur); } bestSlur = sectSlur; } break SideLoop; // We are through on this side } } } return bestSlur; } //--------------------// // extendSlurSections // //--------------------// /** * Try to extend the provided slur with neighboring sections on * the provided side. * Starting from the slur seed, we incrementally aggregate compatible * sections, sorted according to their distance to slur ending point. * The process is stopped at the first failed attempt. * * @param root the slur glyph to extend * @return the extended slur glyph if any, or null. A non-null glyph * is returned IFF we have found a slur which is both larger than the * initial slur and valid. */ private Glyph extendSlurSections (Glyph root, HorizontalSide side) { // The best compound obtained so far Glyph bestSlur = null; List<Section> sections = new ArrayList<>(); sections.addAll(system.getHorizontalSections()); sections.addAll(system.getVerticalSections()); for (Section section : sections) { Glyph glyph = section.getGlyph(); // Discard manual sections if ((glyph != null) && glyph.isManualShape()) { section.setProcessed(true); } else { section.setProcessed(false); } } for (Section section : root.getMembers()) { section.setProcessed(true); } // Initial conditions adapter.setSide(side); // Loop on extensions boolean growing = true; while (growing) { growing = false; if (root.isVip() || logger.isDebugEnabled()) { logger.info("Trying to section-extend slur #{}", root.getId()); } // Process that slur, looking at neighboring sections if (adapter.setSeed(root) == null) { logger.warn("Null reference box"); } // Retrieve good neighbors among the suitable sections List<Section> neighbors = new ArrayList<>(); for (Section section : sections) { if (section.isVip()) { logger.debug("Section {}", section); } if (!section.isProcessed()) { if (adapter.isSectionClose(section) && adapter.isSectionSuitable(section)) { neighbors.add(section); section.setProcessed(true); } } } // Let's try neighbors incrementally if (!neighbors.isEmpty()) { // Sort neighbors according to their distance from slur ending Collections.sort(neighbors, adapter.sectionComparator); // Sections effectively added List<Section> added = new ArrayList<>(); for (Section section : neighbors) { added.add(section); // slur config = seed sections + added sections List<Section> config = new ArrayList<>(added); config.addAll(root.getMembers()); boolean sectionOk = false; double distance = computeCircle(config).getDistance(); logger.debug("dist={}", distance); if (distance <= adapter.extendedDistance()) { Glyph compound = system.buildTransientGlyph(config); if (adapter.isCompoundValid(compound)) { // Assign and insert into system & nest environments compound = system.addGlyph(compound); compound.setEvaluation( adapter.getChosenEvaluation()); if (root.isVip() || logger.isDebugEnabled()) { logger.info( "Slur #{} extended as #{} with {}", root.getId(), compound.getId(), Sections.toString(added)); if (root.isVip()) { compound.setVip(); } } bestSlur = compound; root = compound; adapter.setSeed(root); growing = true; sectionOk = true; } } if (!sectionOk) { if (root.isVip() || logger.isDebugEnabled()) { logger.info("Slur #{} excluding section#{}", root.getId(), section); } break; } } } else { return bestSlur; } } return bestSlur; } //-----------------// // findSeedSection // //-----------------// /** * Find the best seed, which is chosen as the section with best * circle distance among the sections whose weight is significant. * * @param sortedMembers the candidate sections, by decreasing weight * @param seedDist (output) the distance measured for chosen seed * @return the suitable seed, perhaps null */ private Section findSeedSection (List<Section> sortedMembers, Wrapper<Double> seedDist) { Section seedSection = null; seedDist.value = Double.MAX_VALUE; for (Section seed : sortedMembers) { // Check minimum weight int weight = seed.getWeight(); if (weight < params.minChunkWeight) { break; // Since sections are sorted } // Check meanthickness double thickness = Math.min( seed.getMeanThickness(VERTICAL), seed.getMeanThickness(HORIZONTAL)); if (thickness > params.maxChunkThickness) { continue; } Circle circle = computeCircle(Arrays.asList(seed)); double dist = circle.getDistance(); if ((dist <= params.maxCircleDistance) && (dist < seedDist.value)) { seedDist.value = dist; seedSection = seed; } } if (logger.isDebugEnabled()) { if (seedSection == null) { logger.debug("No suitable seed section found"); } else { logger.debug("Seed section is {} dist:{}", seedSection, seedDist.value); } } return seedSection; } //---------------// // getInvalidity // //---------------// /** * Check validity of a collection of sections as a slur. * * @param sections the provided sections * @param resulting circle if already known * @return null if OK, otherwise the cause of invalidity */ private Object getInvalidity (Collection<Section> sections, Circle circle) { if (circle == null) { circle = computeCircle(sections); } // Check distance to circle double dist = circle.getDistance(); if (dist > params.maxCircleDistance) { return "distance " + (float) dist + " vs " + params.maxCircleDistance; } // Check curve is computable if (circle.getCurve() == null) { return "no curve"; } // Check radius double radius = circle.getRadius(); if (radius < params.minCircleRadius) { return "small radius " + (float) radius + " vs " + params.minCircleRadius; } if (radius > params.maxCircleRadius) { return "large radius " + (float) radius + " vs " + params.maxCircleRadius; } // // Check curve bounds are rather close to slur box // Rectangle curveBox = circle.getCurve() // .getBounds(); // // double heightRatio = (double) curveBox.height / contourBox.height; // // if (heightRatio > constants.maxHeightRatio.getValue()) { // if (logger.isDebugEnabled()) { // logger.info( // "Too high ratio: " + (float) heightRatio + // " for curve box " + curveBox); // } // // return false; // } return null; } //-------------------// // getSlurPointNearX // //-------------------// /** * Retrieve the best slur point near the provided abscissa. * * @param x the provided abscissa * @param sections the slur sections * @param box the slur bounding box * @return the best approximating point */ private Point2D getSlurPointNearX (int x, Collection<? extends Section> sections, Rectangle box) { Rectangle roi = new Rectangle(x, box.y, 0, box.height); Barycenter bary; do { bary = new Barycenter(); roi.grow(1, 0); for (Section section : sections) { section.cumulate(bary, roi); } } while (bary.getWeight() == 0); return new Point2D.Double(bary.getX(), bary.getY()); } //---------// // isValid // //---------// /** * Check validity of a glyph as a slur. * * @param glyph the glyph to check * @return true if valid */ private boolean isValid (Glyph slur) { // Make sure we are not trying to reassign a blacklisted shape if (slur.isShapeForbidden(Shape.SLUR)) { return false; } Object cause = getInvalidity(slur.getMembers(), slur.getCircle()); if (slur.isVip()) { if (cause != null) { logger.info("Invalid slur #{} : {}", slur.getId(), cause); } else { logger.info("Valid slur #{}", slur.getId()); } } return cause == null; } //------------------// // extendedDistance // //------------------// /** * Report the maximum extended circle distance, knowing the * circle distance of the current slur. * * @param lastDistance current slur fitting distance * @return extended maximum distance */ private double extendedDistance (double lastDistance) { double ratio = constants.distanceExtensionRatio.getValue(); return lastDistance + ratio * (params.maxCircleDistance - lastDistance); } //~ Inner Classes ---------------------------------------------------------- // //----------------------// // NoSlurCurveException // //----------------------// /** * Used to signal an abnormal "slur" glyph, for which the curve * cannot be computed or is degenerated to a straight line. */ private static class NoSlurCurveException extends RuntimeException { } //---------------------// // SlurCompoundAdapter // //---------------------// /** * CompoundAdapter meant to process the extension of a slur. */ private class SlurCompoundAdapter extends CompoundBuilder.AbstractAdapter { //~ Instance fields ---------------------------------------------------- // Underlying slur curve protected CubicCurve2D curve; // Current fitting distance protected double distance; // Current extension side protected HorizontalSide side; // Current slur ending point protected Point2D endPt; /** To sort sections according to the distance to slur end */ public Comparator<Section> sectionComparator = new Comparator<Section>() { @Override public int compare (Section s1, Section s2) { // We use distance from section to adapter end point return Double.compare(toEndSq(s1), toEndSq(s2)); } }; //~ Constructors ------------------------------------------------------- public SlurCompoundAdapter (SystemInfo system) { // Note: minGrade value (0d) is irrelevant, since compound validity // will be checked against specific slur characteristics rather // than evaluation grade. super(system, 0d); } //~ Methods ------------------------------------------------------------ /** * Compute the extension box on the provided side. * * @return the extension box * @see #setSide */ @Override public Rectangle computeReferenceBox () { Rectangle sBox = seed.getBounds(); // Seed box boolean isShort = sBox.width <= params.minSlurWidth; Point2D cp; // Related control point if (isShort) { // For short glyphs, circle/curve are not reliable // so we use approximating line instead. endPt = (side == LEFT) ? seed.getStartPoint(HORIZONTAL) : seed.getStopPoint(HORIZONTAL); cp = (side == LEFT) ? seed.getStopPoint(HORIZONTAL) : seed.getStartPoint(HORIZONTAL); } else { endPt = (side == LEFT) ? curve.getP1() : curve.getP2(); cp = (side == LEFT) ? curve.getCtrlP1() : curve.getCtrlP2(); } // Exact ending point (?) Rectangle roi = (side == LEFT) ? new Rectangle(sBox.x, sBox.y, 1, sBox.height) : new Rectangle((sBox.x + sBox.width) - 1, sBox.y, 1, sBox.height); Point2D ep = seed.getRectangleCentroid(roi); if (ep != null) { if (side == RIGHT) { ep.setLocation(ep.getX() + 1, ep.getY()); } } else { ep = endPt; // Better than nothing } StaffInfo staff = system.getStaffAt(endPt); if (staff == null) { // Weird case, where the slur crosses system boundaries Point2D otherEnd = (side == LEFT) ? curve.getP2() : curve.getP1(); staff = system.getStaffAt(otherEnd); } // Is the slur end touching a staff line? final double pitch = staff.pitchPositionOf(endPt); final int intPitch = (int) Math.rint(pitch); double target; if ((Math.abs(intPitch) <= 4) && ((intPitch % 2) == 0)) { // TODO: beware of vertical double slope = (ep.getY() - cp.getY()) / (ep.getX() - cp.getX()); if (Math.abs(slope) <= params.maxTangentSlope) { // This end touches a staff line, with horizontal tangent target = params.targetLineTangentHypot; } else { // This end touches a staff line not horizontally target = params.targetLineHypot; } } else { // No staff line is involved, use smaller margins target = params.targetHypot; } Point2D cp2pt = new Point2D.Double( endPt.getX() - cp.getX(), endPt.getY() - cp.getY()); double hypot = Math.hypot(cp2pt.getX(), cp2pt.getY()); double lambda = target / hypot; Point2D ext = new Point2D.Double( ep.getX() + (lambda * cp2pt.getX()), ep.getY() + (lambda * cp2pt.getY())); Rectangle rect = new Rectangle( (int) Math.rint(Math.min(ext.getX(), ep.getX())), (int) Math.rint(Math.min(ext.getY(), ep.getY())), (int) Math.rint(Math.abs(ext.getX() - ep.getX())), (int) Math.rint(Math.abs(ext.getY() - ep.getY()))); // Ensure minimum box height if (rect.height < params.minExtensionHeight) { rect.grow( 0, 1 + (int) Math.rint( (params.minExtensionHeight - rect.height) / 2.0)); } seed.addAttachment(((side == LEFT) ? "e^" : "^e"), rect); return rect; } @Override public boolean isCandidateSuitable (Glyph glyph) { if (!glyph.isActive()) { return false; // Safer } // Check mean thickness double thickness = Math.min( glyph.getMeanThickness(VERTICAL), glyph.getMeanThickness(HORIZONTAL)); if (thickness > params.maxChunkThickness) { return false; } // Check minimum weight if (glyph.getWeight() < params.minExtensionWeight) { return false; } // Check shape if (!glyph.isKnown()) { return true; } Shape shape = glyph.getShape(); if ((shape == Shape.SLUR) && !glyph.isManualShape()) { return true; } return (!glyph.isManualShape() && extShapes.contains(shape)) || (glyph.getGrade() <= Grades.compoundPartMaxGrade); } @Override public boolean isCompoundValid (Glyph compound) { if (isValid(compound)) { // Is distance still OK? double compoundDistance = getCircle(compound).getDistance(); if (compoundDistance <= extendedDistance()) { chosenEvaluation = new Evaluation( Shape.SLUR, Evaluation.ALGORITHM); return true; } else { logger.debug("{} Degrading distance {} vs {}", seed, compoundDistance, extendedDistance()); } } return false; } public boolean isSectionClose (Section section) { return box.intersects(section.getBounds()); } public boolean isSectionSuitable (Section section) { Glyph glyph = section.getGlyph(); if ((glyph != null) && !glyph.isActive()) { return false; // Safer } // Check meanthickness double thickness = Math.min( section.getMeanThickness(VERTICAL), section.getMeanThickness(HORIZONTAL)); if (thickness > params.maxChunkThickness) { return false; } if ((glyph == null) || !glyph.isKnown()) { // Check section weight if (section.getWeight() >= params.minExtensionWeight) { return true; } else { return false; } } Shape shape = glyph.getShape(); if (ShapeSet.Barlines.contains(shape) || (shape == Shape.SLUR)) { return false; } if (glyph.isManualShape()) { return false; } // Check shape grade if (glyph.getGrade() > Grades.compoundPartMaxGrade) { return false; } return true; } @Override public Rectangle setSeed (Glyph seed) { box = null; // Side-effect: compute underlying curve Circle circle = getCircle(seed); if (circle.getRadius().isInfinite()) { throw new NoSlurCurveException(); } curve = circle.getCurve(); if (curve == null) { throw new NoSlurCurveException(); } else { seed.addAttachment("^", curve); } // Side-effect: compute current circle distance distance = circle.getDistance(); return super.setSeed(seed); } /** * Remember the desired extension side. * * @param side the desired side */ public void setSide (HorizontalSide side) { this.side = side; } /** * Report the maximum extended circle distance. * * @return extended maximum distance */ private double extendedDistance () { return SlurInspector.this.extendedDistance(distance); } /** * Report the (square) distance from the slur ending point to * the provided section, according to the current side. * * @param section the provided section * @return the square distance */ private double toEndSq (Section section) { Rectangle b = section.getBounds(); if (side == LEFT) { // Use box right vertical return new Line2D.Double( b.x + b.width, b.y, b.x + b.width, b.y + b.height).ptSegDistSq(endPt); } else { // Use box left vertical return new Line2D.Double(b.x, b.y, b.x, b.y + b.height). ptSegDistSq(endPt); } } } //------------// // Parameters // //------------// private static class Parameters { //~ Instance fields ---------------------------------------------------- final int interline; final int minChunkWeight; final int minExtensionWeight; final double maxChunkThickness; final int slurBoxDx; final int slurBoxDy; final int targetHypot; final int targetLineHypot; final int targetLineTangentHypot; final int minSlurWidth; final int minExtensionHeight; final double maxCircleDistance; final double minCircleRadius; final double maxCircleRadius; final double maxTangentSlope; //~ Constructors ------------------------------------------------------- public Parameters (Scale scale) { interline = scale.getInterline(); minChunkWeight = scale.toPixels(constants.minChunkWeight); minExtensionWeight = scale.toPixels(constants.minExtensionWeight); maxChunkThickness = scale.toPixels(constants.maxChunkThickness); slurBoxDx = scale.toPixels(constants.slurBoxDx); slurBoxDy = scale.toPixels(constants.slurBoxDy); targetHypot = scale.toPixels(constants.slurBoxHypot); targetLineHypot = scale.toPixels(constants.slurLineBoxHypot); targetLineTangentHypot = scale.toPixels( constants.slurLineTangentBoxHypot); minSlurWidth = scale.toPixels(constants.minSlurWidth); minExtensionHeight = scale.toPixels(constants.minExtensionHeight); maxCircleDistance = scale.toPixelsDouble(constants.maxCircleDistance); minCircleRadius = scale.toPixels(constants.minCircleRadius); maxCircleRadius = scale.toPixels(constants.maxCircleRadius); maxTangentSlope = constants.maxTangentSlope.getValue(); } } //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Scale.Fraction maxCircleDistance = new Scale.Fraction( 0.006, "Maximum distance to approximating circle for a slur"); Scale.Fraction minCircleRadius = new Scale.Fraction( 0.7, "Minimum circle radius for a slur"); Scale.Fraction maxCircleRadius = new Scale.Fraction( 100, "Maximum circle radius for a slur"); Scale.AreaFraction minChunkWeight = new Scale.AreaFraction( 0.3, "Minimum weight of a chunk to be part of slur computation"); Scale.AreaFraction minExtensionWeight = new Scale.AreaFraction( 0.01, "Minimum weight of a glyph to be considered for slur extension"); Scale.Fraction slurBoxDx = new Scale.Fraction( 0.7, "Extension abscissa when looking for slur compound"); Scale.Fraction slurBoxDy = new Scale.Fraction( 0.4, "Extension ordinate when looking for slur compound"); Scale.Fraction slurBoxHypot = new Scale.Fraction( 0.9, "Extension length when looking for line-free slur compound"); Scale.Fraction slurLineBoxHypot = new Scale.Fraction( 1.5, "Extension length when looking for line-touching slur compound"); Scale.Fraction slurLineTangentBoxHypot = new Scale.Fraction( 3.0, "Extension length when looking for line-tangent slur compound"); Scale.Fraction minSlurWidth = new Scale.Fraction( 2, "Minimum width to use curve rather than line for extension"); Scale.LineFraction minExtensionHeight = new Scale.LineFraction( 2, "Minimum height for extension box, specified as LineFraction"); Scale.LineFraction maxChunkThickness = new Scale.LineFraction( 2, "Maximum mean thickness of a chunk to be part of slur computation"); Constant.Ratio maxHeightRatio = new Constant.Ratio( 2.0, "Maximum height ratio between curve height and glyph height"); Constant.Double maxTangentSlope = new Constant.Double( "tangent", 0.05, "Maximum slope for staff line tangent"); Constant.Ratio distanceExtensionRatio = new Constant.Ratio( 0.67, "Acceptable distance extension to maximum limit"); } }