//----------------------------------------------------------------------------// // // // C l u s t e r s R e t r i e v 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.grid; import omr.Main; import omr.constant.Constant; import omr.constant.ConstantSet; import omr.glyph.Glyphs; import omr.glyph.Shape; import omr.glyph.facets.Glyph; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import omr.math.Histogram; import omr.run.Orientation; import static omr.run.Orientation.*; import omr.sheet.Scale; import omr.sheet.Sheet; import omr.sheet.Skew; import omr.util.Wrapper; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; /** * Class {@code ClustersRetriever} performs vertical samplings of the * horizontal filaments in order to detect regular patterns of a * preferred interline value and aggregate the filaments into clusters * of lines. * * @author Hervé Bitteur */ public class ClustersRetriever { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger( ClustersRetriever.class); /** * For comparing Filament instances on their starting point */ private static final Comparator<Filament> startComparator = new Comparator<Filament>() { @Override public int compare (Filament f1, Filament f2) { // Sort on start return Double.compare( f1.getStartPoint(HORIZONTAL).getX(), f2.getStartPoint(HORIZONTAL).getX()); } }; /** * For comparing Filament instances on their stopping point */ private static final Comparator<Filament> stopComparator = new Comparator<Filament>() { @Override public int compare (Filament f1, Filament f2) { // Sort on stop return Double.compare( f1.getStopPoint(HORIZONTAL).getX(), f2.getStopPoint(HORIZONTAL).getX()); } }; //~ Instance fields -------------------------------------------------------- /** Comparator on cluster ordinate */ public Comparator<LineCluster> ordinateComparator = new Comparator<LineCluster>() { @Override public int compare (LineCluster c1, LineCluster c2) { double o1 = ordinateOf(c1); double o2 = ordinateOf(c2); if (o1 < o2) { return -1; } if (o1 > o2) { return +1; } return 0; } }; /** Related sheet */ private final Sheet sheet; /** Related scale */ private final Scale scale; /** Desired interline */ private final int interline; /** Scale-dependent constants */ private final Parameters params; /** Picture width to sample for combs */ private final int pictureWidth; /** Long filaments to process */ private final List<LineFilament> filaments; /** Filaments discarded */ private final List<LineFilament> discardedFilaments = new ArrayList<>(); /** Skew of the sheet */ private final Skew skew; /** A map (colIndex -> vertical list of samples), sorted on colIndex */ private Map<Integer, List<FilamentComb>> colCombs; /** Color used for comb display */ private final Color combColor; /** * The popular size of combs detected for the specified interline * (typically: 4, 5 or 6) */ private int popSize; /** X values per column index */ private int[] colX; /** Collection of clusters */ private final List<LineCluster> clusters = new ArrayList<>(); //~ Constructors ----------------------------------------------------------- //-------------------// // ClustersRetriever // //-------------------// /** * Creates a new ClustersRetriever object, for a given staff * interline. * * @param sheet the sheet to process * @param filaments the current collection of filaments * @param interline the precise interline to be processed * @param combColor color to be used for combs display */ public ClustersRetriever (Sheet sheet, List<LineFilament> filaments, int interline, Color combColor) { this.sheet = sheet; this.filaments = filaments; this.interline = interline; this.combColor = combColor; skew = sheet.getSkew(); pictureWidth = sheet.getWidth(); scale = sheet.getScale(); colCombs = new TreeMap<>(); params = new Parameters(scale); } //~ Methods ---------------------------------------------------------------- //-----------// // buildInfo // //-----------// public List<LineFilament> buildInfo () { // Retrieve all vertical combs gathering filaments retrieveCombs(); // Remember the most popular comb length retrievePopularSize(); // Check relevance //if ((popSize < 4) || (popSize > 6)) { // below jlp modification, "3" popped out for Tremolo example and // caused halt at this value if ((popSize < 3) || (popSize > 6)) { logger.info("{}Giving up spurious line comb size: {}", sheet.getLogPrefix(), popSize); return discardedFilaments; } // Interconnect filaments via the network of combs followCombsNetwork(); // Retrieve clusters retrieveClusters(); logger.info( "{}Retrieved line clusters: {} of size: {} with interline: {}", sheet.getLogPrefix(), clusters.size(), popSize, interline); return discardedFilaments; } //-------------// // getClusters // //-------------// /** * Report the sequence of clusters detected by this retriever using * its provided interline value. * * @return the sequence of interline-based clusters */ public List<LineCluster> getClusters () { return clusters; } //--------------// // getInterline // //--------------// /** * Report the value of the interline this retriever is based upon * * @return the interline value */ public int getInterline () { return interline; } //--------------------// // getStafflineGlyphs // //--------------------// /** * Report the glyphs that are part of actual cluster lines * * @return the collection of glyphs actually used for retained cluster */ Collection<Glyph> getStafflineGlyphs () { List<Glyph> glyphs = new ArrayList<>(); for (LineCluster cluster : clusters) { for (FilamentLine line : cluster.getLines()) { glyphs.add(line.fil); } } return glyphs; } //-------------// // renderItems // //-------------// /** * Render the vertical combs of filaments * * @param g graphics context */ void renderItems (Graphics2D g) { Color oldColor = g.getColor(); g.setColor(combColor); for (Entry<Integer, List<FilamentComb>> entry : colCombs.entrySet()) { int col = entry.getKey(); int x = colX[col]; for (FilamentComb comb : entry.getValue()) { g.draw( new Line2D.Double( x, comb.getY(0), x, comb.getY(comb.getCount() - 1))); } } g.setColor(oldColor); } //-----------// // bestMatch // //-----------// /** * Find the best match between provided sequences. * (which may contain null values when related data is not available) * * @param one first sequence * @param two second sequence * @param bestDelta output: best delta between the two sequences * @return the best distance found */ private double bestMatch (Double[] one, Double[] two, Wrapper<Integer> bestDelta) { final int deltaMax = one.length - 1; final int deltaMin = -deltaMax; double bestDist = Double.MAX_VALUE; bestDelta.value = null; for (int delta = deltaMin; delta <= deltaMax; delta++) { int distSum = 0; int count = 0; for (int oneIdx = 0; oneIdx < one.length; oneIdx++) { int twoIdx = oneIdx + delta; if ((twoIdx >= 0) && (twoIdx < two.length)) { Double oneVal = one[oneIdx]; Double twoVal = two[twoIdx]; if ((oneVal != null) && (twoVal != null)) { count++; distSum += Math.abs(twoVal - oneVal); } } } if (count > 0) { double dist = (double) distSum / count; if (dist < bestDist) { bestDist = dist; bestDelta.value = delta; } } } return bestDist; } //----------// // canMerge // //----------// /** * Check for merge possibility between two clusters * * @param one first cluster * @param two second cluster * @param deltaPos output: the delta in positions between these clusters * if the test has succeeded * @return true if successful */ private boolean canMerge (LineCluster one, LineCluster two, Wrapper<Integer> deltaPos) { final Rectangle oneBox = one.getBounds(); final Rectangle twoBox = two.getBounds(); final int oneLeft = oneBox.x; final int oneRight = (oneBox.x + oneBox.width) - 1; final int twoLeft = twoBox.x; final int twoRight = (twoBox.x + twoBox.width) - 1; final int minRight = Math.min(oneRight, twoRight); final int maxLeft = Math.max(oneLeft, twoLeft); final int gap = maxLeft - minRight; double dist; logger.debug("gap:{}", gap); if (gap <= 0) { // Overlap: use middle of common part final int xMid = (maxLeft + minRight) / 2; final double slope = sheet.getSkew().getSlope(); dist = bestMatch( ordinatesOf( one.getPointsAt(xMid, params.maxExpandDx, interline, slope)), ordinatesOf( two.getPointsAt(xMid, params.maxExpandDx, interline, slope)), deltaPos); } else if (gap > params.maxMergeDx) { logger.debug("Gap too wide between {} & {}", one, two); return false; } else { // True gap: use proper edges if (oneLeft < twoLeft) { // Case one --- two dist = bestMatch( ordinatesOf(one.getStops()), ordinatesOf(two.getStarts()), deltaPos); } else { // Case two --- one dist = bestMatch( ordinatesOf(one.getStarts()), ordinatesOf(two.getStops()), deltaPos); } } // Check best distance logger.debug("canMerge dist: {} one:{} two:{}", dist, one, two); return dist <= params.maxMergeDy; } //-------------------------// // computeAcceptableLength // //-------------------------// private double computeAcceptableLength () { // Determine minimum true length for valid clusters List<Integer> lengths = new ArrayList<>(); for (LineCluster cluster : clusters) { lengths.add(cluster.getTrueLength()); } Collections.sort(lengths); int medianLength = lengths.get(lengths.size() / 2); double minLength = medianLength * constants.minClusterLengthRatio. getValue(); logger.debug("medianLength: {} minLength: {}", medianLength, minLength); return minLength; } //------------------// // connectAncestors // //------------------// private void connectAncestors (LineFilament one, LineFilament two) { LineFilament oneAnc = (LineFilament) one.getAncestor(); LineFilament twoAnc = (LineFilament) two.getAncestor(); if (oneAnc != twoAnc) { if (oneAnc.getLength(Orientation.HORIZONTAL) >= twoAnc.getLength( Orientation.HORIZONTAL)) { ///logger.info("Inclusion " + twoAnc + " into " + oneAnc); oneAnc.include(twoAnc); oneAnc.getCombs().putAll(twoAnc.getCombs()); } else { ///logger.info("Inclusion " + oneAnc + " into " + twoAnc); twoAnc.include(oneAnc); twoAnc.getCombs().putAll(oneAnc.getCombs()); } } } //----------------// // createClusters // //----------------// private void createClusters () { Collections.sort( filaments, Glyphs.getReverseLengthComparator(Orientation.HORIZONTAL)); for (LineFilament fil : filaments) { fil = (LineFilament) fil.getAncestor(); if ((fil.getCluster() == null) && !fil.getCombs().isEmpty()) { LineCluster cluster = new LineCluster(interline, fil); clusters.add(cluster); } } removeMergedClusters(); } //----------------------------// // destroyNonStandardClusters // //----------------------------// private void destroyNonStandardClusters () { for (Iterator<LineCluster> it = clusters.iterator(); it.hasNext();) { LineCluster cluster = it.next(); if (cluster.getSize() != popSize) { logger.debug("Destroying non standard {}", cluster); cluster.destroy(); it.remove(); } } } //------------------------------// // discardNonClusteredFilaments // //------------------------------// private void discardNonClusteredFilaments () { for (Iterator<LineFilament> it = filaments.iterator(); it.hasNext();) { LineFilament fil = it.next(); if (fil.getCluster() == null) { it.remove(); discardedFilaments.add(fil); } else { fil.setShape(Shape.STAFF_LINE); } } } //--------------// // dumpClusters // //--------------// private void dumpClusters () { for (LineCluster cluster : clusters) { logger.info("{} {}", cluster.getCenter(), cluster.toString()); } } //---------------// // expandCluster // //---------------// /** * Try to expand the provided cluster with filaments taken out of * the provided sorted collection of isolated filaments * * @param cluster the cluster to work on * @param fils the (properly sorted) collection of filaments */ private void expandCluster (LineCluster cluster, List<LineFilament> fils) { final double slope = sheet.getSkew().getSlope(); Rectangle clusterBox = null; for (LineFilament fil : fils) { fil = (LineFilament) fil.getAncestor(); if (fil.getCluster() != null) { continue; } // For VIP debugging final boolean areVips = cluster.isVip() && fil.isVip(); String vips = null; if (areVips) { vips = "F" + fil.getId() + "&C" + cluster.getId() + ": "; // BP here! } if (clusterBox == null) { clusterBox = cluster.getBounds(); clusterBox.grow(params.clusterXMargin, params.clusterYMargin); } Rectangle filBox = fil.getBounds(); Point middle = new Point(); middle.x = filBox.x + (filBox.width / 2); middle.y = (int) Math.rint(fil.getPositionAt(middle.x, HORIZONTAL)); if (clusterBox.contains(middle)) { // Check if this filament matches a cluster line List<Point2D> points = cluster.getPointsAt( middle.x, params.maxExpandDx, interline, slope); for (Point2D point : points) { // Check vertical distance, if point is available if (point == null) { continue; } double dy = Math.abs(middle.y - point.getY()); if (dy <= params.maxExpandDy) { int index = points.indexOf(point); if (cluster.includeFilamentByIndex(fil, index)) { if (logger.isDebugEnabled() || fil.isVip() || cluster.isVip()) { logger.info( "Aggregated F{} to C{} at index {}", fil.getId(), cluster.getId(), index); if (fil.isVip()) { cluster.setVip(); } } clusterBox = null; // Invalidate cluster box break; } } else { if (areVips) { logger.info("{}dy={} vs {}", vips, dy, params.maxExpandDy); } } } } else { if (areVips) { logger.info("{}No box intersection", vips); } } } } //----------------// // expandClusters // //----------------// /** * Aggregate non-clustered filaments to close clusters when * appropriate. */ private void expandClusters () { List<LineFilament> startFils = new ArrayList<>(filaments); Collections.sort(startFils, startComparator); List<LineFilament> stopFils = new ArrayList<>(startFils); Collections.sort(stopFils, stopComparator); // Browse clusters, starting with the longest ones Collections.sort(clusters, LineCluster.reverseLengthComparator); for (LineCluster cluster : clusters) { logger.debug("Expanding {}", cluster); // Expanding on left side expandCluster(cluster, stopFils); // Expanding on right side expandCluster(cluster, startFils); } } //--------------------// // followCombsNetwork // //--------------------// /** * Use the network of combs and filaments to interconnect filaments * via common combs. */ private void followCombsNetwork () { logger.debug("Following combs network"); for (LineFilament fil : filaments) { Map<Integer, FilamentComb> combs = fil.getCombs(); // Sequence of lines around the filament, indexed by relative pos Map<Integer, LineFilament> lines = new TreeMap<>(); // Loop on all combs this filament is involved in for (FilamentComb comb : combs.values()) { int posPivot = comb.getIndex(fil); for (int pos = 0; pos < comb.getCount(); pos++) { int line = pos - posPivot; if (line != 0) { LineFilament f = lines.get(line); if (f != null) { connectAncestors(f, comb.getFilament(pos)); } else { lines.put(line, comb.getFilament(pos)); } } } } } removeMergedFilaments(); } //-------------------// // mergeClusterPairs // //-------------------// /** * Merge clusters horizontally or destroy short clusters. */ private void mergeClusterPairs () { // Sort clusters according to their ordinate in page Collections.sort(clusters, ordinateComparator); double minLength = computeAcceptableLength(); WholeLoop: for (int idx = 0; idx < clusters.size();) { LineCluster cluster = clusters.get(idx); Point2D dskCenter = skew.deskewed(cluster.getCenter()); double yMax = dskCenter.getY() + params.maxMergeCenterDy; for (LineCluster cl : clusters.subList(idx + 1, clusters.size())) { // Check dy if (skew.deskewed(cl.getCenter()).getY() > yMax) { break; } // Merge logger.info("Pairing clusters C{} & C{}", cluster.getId(), cl.getId()); cluster.mergeWith(cl, 0); clusters.remove(cl); continue WholeLoop; // Recheck at same index } // Short isolated? if (cluster.getTrueLength() < minLength) { logger.info("Destroying spurious {}", cluster); clusters.remove(cluster); } else { idx++; // Move forward } } removeMergedFilaments(); } //---------------// // mergeClusters // //---------------// /** * Merge compatible clusters as much as possible. */ private void mergeClusters () { // Sort clusters according to their ordinate in page Collections.sort(clusters, ordinateComparator); for (LineCluster current : clusters) { LineCluster candidate = current; // Keep on working while we do have a candidate to check for merge CandidateLoop: while (true) { Wrapper<Integer> deltaPos = new Wrapper<>(); Rectangle candidateBox = candidate.getBounds(); candidateBox.grow(params.clusterXMargin, params.clusterYMargin); // Check the candidate vs all clusters until current excluded for (LineCluster head : clusters) { if (head == current) { break CandidateLoop; // Actual end of sub list } if ((head == candidate) || (head.getParent() != null)) { continue; } // Check rough proximity Rectangle headBox = head.getBounds(); if (headBox.intersects(candidateBox)) { // Try a merge if (canMerge(head, candidate, deltaPos)) { logger.debug("Merging {} with {} delta:{}", candidate, head, deltaPos.value); // Do the merge candidate.mergeWith(head, deltaPos.value); break; } } } } } removeMergedClusters(); removeMergedFilaments(); } //------------// // ordinateOf // //------------// /** * Report the orthogonal distance of the provided point * to the sheet top edge tilted with global slope. */ private Double ordinateOf (Point2D point) { if (point != null) { return sheet.getSkew().deskewed(point).getY(); } else { return null; } } //------------// // ordinateOf // //------------// /** * Report the orthogonal distance of the cluster center * to the sheet top edge tilted with global slope. */ private double ordinateOf (LineCluster cluster) { return ordinateOf(cluster.getCenter()); } //-------------// // ordinatesOf // //-------------// private Double[] ordinatesOf (Collection<Point2D> points) { Double[] ys = new Double[points.size()]; int index = 0; for (Point2D p : points) { ys[index++] = ordinateOf(p); } return ys; } //----------------------// // removeMergedClusters // //----------------------// private void removeMergedClusters () { for (Iterator<LineCluster> it = clusters.iterator(); it.hasNext();) { LineCluster cluster = it.next(); if (cluster.getParent() != null) { it.remove(); } } } //-----------------------// // removeMergedFilaments // //-----------------------// private void removeMergedFilaments () { for (Iterator<LineFilament> it = filaments.iterator(); it.hasNext();) { LineFilament fil = it.next(); if (fil.getPartOf() != null) { it.remove(); } } } //------------------// // retrieveClusters // //------------------// /** * Connect filaments via the combs they are involved in, * and come up with clusters of lines. */ private void retrieveClusters () { // Create clusters recursively out of filements createClusters(); // Aggregate filaments left over when possible (first) expandClusters(); // Merge clusters mergeClusters(); // Trim clusters with too many lines trimClusters(); // Discard non standard clusters destroyNonStandardClusters(); // Merge clusters horizontally mergeClusterPairs(); // Aggregate filaments left over when possible (second) expandClusters(); // Discard non-clustered filaments discardNonClusteredFilaments(); removeMergedFilaments(); // Debug if (logger.isDebugEnabled()) { dumpClusters(); } } //---------------// // retrieveCombs // //---------------// /** * Detect regular patterns of (staff) lines. * Use vertical sampling on regularly-spaced abscissae */ private void retrieveCombs () { // /** Minimum acceptable delta y */ // int dMin = (int) Math.floor( // interline * (1 - constants.maxJitter.getValue())); // // /** Maximum acceptable delta y */ // int dMax = (int) Math.ceil( // interline * (1 + constants.maxJitter.getValue())); /** Minimum acceptable delta y */ int dMin = (int) Math.floor(scale.getMinInterline()); /** Maximum acceptable delta y */ int dMax = (int) Math.ceil(scale.getMaxInterline()); /** Number of vertical samples to collect */ int sampleCount = -1 + (int) Math.rint( (double) pictureWidth / params.samplingDx); /** Exact columns abscissae */ colX = new int[sampleCount + 1]; /** Precise x interval */ double samplingDx = (double) pictureWidth / (sampleCount + 1); for (int col = 1; col <= sampleCount; col++) { final List<FilamentComb> colList = new ArrayList<>(); colCombs.put(col, colList); final int x = (int) Math.rint(samplingDx * col); colX[col] = x; // Retrieve Filaments with ordinate at x, sorted by increasing y List<FilY> filys = retrieveFilamentsAtX(x); // Second, check y deltas to detect combs FilamentComb comb = null; FilY prevFily = null; for (FilY fily : filys) { if (prevFily != null) { int dy = (int) Math.rint(fily.y - prevFily.y); if ((dy >= dMin) && (dy <= dMax)) { if (comb == null) { // Start of a new comb comb = new FilamentComb(col); colList.add(comb); comb.append(prevFily.filament, prevFily.y); if (prevFily.filament.isVip()) { logger.info("Created {} with {}", comb, prevFily.filament); } } // Extend comb comb.append(fily.filament, fily.y); if (fily.filament.isVip()) { logger.info("Appended {} to {}", fily.filament, comb); } } else { // No comb active comb = null; } } prevFily = fily; } } } //----------------------// // retrieveFilamentsAtX // //----------------------// /** * For a given abscissa, retrieve the filaments that are intersected * by vertical x, and sort them according to their ordinate at x. * * @param x the desired abscissa * @return the sorted list of structures (Fil + Y), perhaps empty */ private List<FilY> retrieveFilamentsAtX (double x) { List<FilY> list = new ArrayList<>(); for (LineFilament fil : filaments) { if ((x >= fil.getStartPoint(HORIZONTAL).getX()) && (x <= fil.getStopPoint(HORIZONTAL).getX())) { list.add(new FilY(fil, fil.getPositionAt(x, HORIZONTAL))); } } Collections.sort(list); return list; } //---------------------// // retrievePopularSize // //---------------------// /** * Retrieve the most popular size (line count) among all combs. */ private void retrievePopularSize () { // Build histogram of combs lengths Histogram<Integer> histo = new Histogram<>(); for (List<FilamentComb> list : colCombs.values()) { for (FilamentComb comb : list) { histo.increaseCount(comb.getCount(), comb.getCount()); } } // Use the most popular length // Should be 4 for bass tab, 5 for standard notation, 6 for guitar tab popSize = histo.getMaxBucket(); logger.debug("{}Popular line comb: {} histo:{}", sheet.getLogPrefix(), popSize, histo.dataString()); } //--------------// // trimClusters // //--------------// private void trimClusters () { Collections.sort(clusters, ordinateComparator); // Trim clusters with too many lines for (Iterator<LineCluster> it = clusters.iterator(); it.hasNext();) { LineCluster cluster = it.next(); cluster.trim(popSize); } } //~ Inner Classes ---------------------------------------------------------- //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Scale.Fraction samplingDx = new Scale.Fraction( 1, "Typical delta X between two vertical samplings"); Scale.Fraction maxExpandDx = new Scale.Fraction( 2, "Maximum dx to aggregate a filament to a cluster"); Scale.Fraction maxExpandDy = new Scale.Fraction( 0.175, "Maximum dy to aggregate a filament to a cluster"); Scale.Fraction maxMergeDx = new Scale.Fraction( 10, "Maximum dx to merge two clusters"); Scale.Fraction maxMergeDy = new Scale.Fraction( 0.4, "Maximum dy to merge two clusters"); Scale.Fraction maxMergeCenterDy = new Scale.Fraction( 1.0, "Maximum center dy to merge two clusters"); Scale.Fraction clusterXMargin = new Scale.Fraction( 4, "Rough margin around cluster abscissa"); Scale.Fraction clusterYMargin = new Scale.Fraction( 2, "Rough margin around cluster ordinate"); Constant.Ratio maxJitter = new Constant.Ratio( 0.1, "Maximum gap from standard comb dy"); Constant.Ratio minClusterLengthRatio = new Constant.Ratio( 0.3, "Minimum cluster length (as ratio of median length)"); } //------// // FilY // //------// /** * Class meant to define an ordering relationship between filaments, * knowing their ordinate at a common abscissa value. */ private static class FilY implements Comparable<FilY> { //~ Instance fields ---------------------------------------------------- final LineFilament filament; final double y; //~ Constructors ------------------------------------------------------- public FilY (LineFilament filament, double y) { this.filament = filament; this.y = y; } //~ Methods ------------------------------------------------------------ @Override public int compareTo (FilY that) { return Double.compare(this.y, that.y); } @Override public String toString () { return "{F" + filament.getId() + " y:" + y + "}"; } } //------------// // Parameters // //------------// /** * Class {@code Parameters} gathers all constants related to * horizontal frames. */ private static class Parameters { //~ Instance fields ---------------------------------------------------- final int samplingDx; final int maxExpandDx; final int maxExpandDy; final int maxMergeDx; final int maxMergeDy; final int maxMergeCenterDy; final int clusterXMargin; final int clusterYMargin; //~ Constructors ------------------------------------------------------- /** * Creates a new Parameters object. * * @param scale the scaling factor */ public Parameters (Scale scale) { samplingDx = scale.toPixels(constants.samplingDx); maxExpandDx = scale.toPixels(constants.maxExpandDx); maxExpandDy = scale.toPixels(constants.maxExpandDy); maxMergeDx = scale.toPixels(constants.maxMergeDx); maxMergeDy = scale.toPixels(constants.maxMergeDy); maxMergeCenterDy = scale.toPixels(constants.maxMergeCenterDy); clusterXMargin = scale.toPixels(constants.clusterXMargin); clusterYMargin = scale.toPixels(constants.clusterYMargin); if (logger.isDebugEnabled()) { Main.dumping.dump(this); } } } }