//----------------------------------------------------------------------------//
// //
// F i l a m e n t s F a c t o r y //
// //
//----------------------------------------------------------------------------//
// <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.Nest;
import omr.glyph.facets.BasicAlignment;
import omr.glyph.facets.BasicGlyph;
import omr.glyph.facets.Glyph;
import omr.glyph.facets.GlyphComposition;
import omr.lag.Section;
import omr.lag.Sections;
import omr.math.Line;
import omr.math.PointsCollector;
import omr.run.Orientation;
import omr.sheet.Scale;
import omr.util.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/**
* Class {@code FilamentsFactory} builds filaments (long series of
* sections) out of a collection of sections.
*
* <p>These filaments are meant to represent good candidates for (horizontal)
* staff lines or (vertical) bar lines. The factory aims at a given orientation,
* though the input sections may exhibit mixed orientations.</p>
*
* <p>Internal parameters have default values defined via a ConstantSet. Before
* launching filaments retrieval by {@link #retrieveFilaments}, parameters can
* be modified individually by calling some setXXX() methods.</p>
*
* @author Hervé Bitteur
*/
public class FilamentsFactory
{
//~ Static fields/initializers ---------------------------------------------
/** Specific application parameters */
private static final Constants constants = new Constants();
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(
FilamentsFactory.class);
//~ Instance fields --------------------------------------------------------
/** Related scale */
private final Scale scale;
/** Where filaments are to be stored */
private final Nest nest;
/** Factory orientation */
private final Orientation orientation;
/** Precise constructor for filaments */
private final Constructor<?> filamentConstructor;
private final Object[] scaleArgs;
/** Scale-dependent constants for horizontal stuff */
private final Parameters params;
/** Long filaments found, non sorted */
private final List<Glyph> filaments = new ArrayList<>();
//~ Constructors -----------------------------------------------------------
//------------------//
// FilamentsFactory //
//------------------//
/**
* Create a factory of filaments.
*
* @param scale the related scale
* @param nest the nest to host created filaments
* @param orientation the target orientation
* @param filamentClass precise Filament class to be use for creation
* @throws Exception
*/
public FilamentsFactory (Scale scale,
Nest nest,
Orientation orientation,
Class<? extends Glyph> filamentClass)
throws Exception
{
this.scale = scale;
this.nest = nest;
this.orientation = orientation;
scaleArgs = new Object[]{scale};
filamentConstructor = filamentClass.getConstructor(
new Class<?>[]{Scale.class});
params = new Parameters();
params.initialize();
}
//~ Methods ----------------------------------------------------------------
//------//
// dump //
//------//
public void dump ()
{
params.dump();
}
//--------------//
// isSectionFat //
//--------------//
/**
* Detect if the provided section is a thick one.
* (as seen in the context of the factory orientation)
*
* @param section the section to check
* @return true if fat
*/
public boolean isSectionFat (Section section)
{
if (section.isFat() == null) {
try {
if (section.getMeanThickness(orientation) <= 1) {
section.setFat(false);
return section.isFat();
}
// Check global slimness
if (section.getMeanAspect(orientation) < params.minSectionAspect) {
section.setFat(true);
return section.isFat();
}
// Check thickness
Rectangle bounds = orientation.oriented(section.getBounds());
Line line = orientation.switchRef(
section.getAbsoluteLine());
if (Math.abs(line.getSlope()) < (Math.PI / 4)) {
// Measure mean thickness on each half
int startCoord = bounds.x + (bounds.width / 4);
int startPos = line.yAtX(startCoord);
int stopCoord = bounds.x + ((3 * bounds.width) / 4);
int stopPos = line.yAtX(stopCoord);
// Start side
Rectangle oRoi = new Rectangle(startCoord, startPos, 0, 0);
final int halfWidth = Math.min(
params.probeWidth / 2,
bounds.width / 4);
oRoi.grow(halfWidth, params.maxSectionThickness);
PointsCollector collector = new PointsCollector(
orientation.absolute(oRoi));
section.cumulate(collector);
int startThickness = (int) Math.rint(
(double) collector.getSize() / oRoi.width);
// Stop side
oRoi.translate(stopCoord - startCoord, stopPos - startPos);
collector = new PointsCollector(orientation.absolute(oRoi));
section.cumulate(collector);
int stopThickness = (int) Math.rint(
(double) collector.getSize() / oRoi.width);
section.setFat(
(startThickness > params.maxSectionThickness)
|| (stopThickness > params.maxSectionThickness));
} else {
section.setFat(bounds.height > params.maxSectionThickness);
}
} catch (Exception ex) {
logger.warn("Error in checking fatness of " + section, ex);
section.setFat(true);
}
}
return section.isFat();
}
//-------------------//
// retrieveFilaments //
//-------------------//
/**
* Aggregate the long and thin sections into filaments (glyphs).
*
* @param source the section source for filaments
* @param useExpansion true to expand filaments with short sections left
* over
* @return the collection of retrieved filaments
*/
public List<Glyph> retrieveFilaments (Collection<Section> source,
boolean useExpansion)
{
StopWatch watch = new StopWatch("FilamentsFactory");
try {
// Create a filament for each section long & slim
watch.start("createFilaments");
createFilaments(source);
logger.debug("{} {} filaments created.",
orientation, filaments.size());
// Merge filaments into larger filaments
watch.start("mergeFilaments");
mergeFilaments();
// Expand with short sections left over?
if (useExpansion) {
watch.start("expandFilaments");
expandFilaments(source);
// Merge filaments into larger filaments
watch.start("mergeFilaments #2");
mergeFilaments();
}
} catch (Exception ex) {
logger.warn("FilamentsFactory cannot retrieveFilaments", ex);
} finally {
if (constants.printWatch.getValue()) {
watch.print();
}
}
return filaments;
}
//----------------//
// setMaxCoordGap //
//----------------//
public void setMaxCoordGap (Scale.Fraction frac)
{
params.maxCoordGap = scale.toPixels(frac);
}
//----------------------//
// setMaxExpansionSpace //
//----------------------//
public void setMaxExpansionSpace (Scale.Fraction frac)
{
params.maxExpansionSpace = scale.toPixels(frac);
}
//-------------------------//
// setMaxFilamentThickness //
//-------------------------//
public void setMaxFilamentThickness (Scale.LineFraction lineFrac)
{
params.maxFilamentThickness = scale.toPixels(lineFrac);
}
//-------------------------//
// setMaxFilamentThickness //
//-------------------------//
public void setMaxFilamentThickness (Scale.Fraction frac)
{
params.maxFilamentThickness = scale.toPixels(frac);
}
//----------------//
// setMaxGapSlope //
//----------------//
public void setMaxGapSlope (double value)
{
params.maxGapSlope = value;
}
//-----------------------//
// setMaxInvolvingLength //
//-----------------------//
public void setMaxInvolvingLength (Scale.Fraction frac)
{
params.maxInvolvingLength = scale.toPixels(frac);
}
//-----------------------//
// setMaxOverlapDeltaPos //
//-----------------------//
public void setMaxOverlapDeltaPos (Scale.Fraction frac)
{
params.maxOverlapDeltaPos = scale.toPixels(frac);
}
//-----------------------//
// setMaxOverlapDeltaPos //
//-----------------------//
public void setMaxOverlapDeltaPos (Scale.LineFraction lFrac)
{
params.maxOverlapDeltaPos = scale.toPixels(lFrac);
}
//--------------//
// setMaxPosGap //
//--------------//
public void setMaxPosGap (Scale.LineFraction lineFrac)
{
params.maxPosGap = scale.toPixels(lineFrac);
}
//--------------//
// setMaxPosGap //
//--------------//
public void setMaxPosGap (Scale.Fraction frac)
{
params.maxPosGap = scale.toPixels(frac);
}
//----------------------//
// setMaxPosGapForSlope //
//----------------------//
public void setMaxPosGapForSlope (Scale.Fraction frac)
{
params.maxPosGapForSlope = scale.toPixels(frac);
}
//------------------------//
// setMaxSectionThickness //
//------------------------//
public void setMaxSectionThickness (Scale.LineFraction lineFrac)
{
params.maxSectionThickness = scale.toPixels(lineFrac);
}
//------------------------//
// setMaxSectionThickness //
//------------------------//
public void setMaxSectionThickness (Scale.Fraction frac)
{
params.maxSectionThickness = scale.toPixels(frac);
}
//-------------//
// setMaxSpace //
//-------------//
public void setMaxSpace (Scale.Fraction frac)
{
params.maxSpace = scale.toPixels(frac);
}
//-------------------------//
// setMinCoreSectionLength //
//-------------------------//
public void setMinCoreSectionLength (Scale.Fraction frac)
{
setMinCoreSectionLength(scale.toPixels(frac));
}
//-------------------------//
// setMinCoreSectionLength //
//-------------------------//
public void setMinCoreSectionLength (int value)
{
params.minCoreSectionLength = value;
}
//---------------------//
// setMinSectionAspect //
//---------------------//
public void setMinSectionAspect (double value)
{
params.minSectionAspect = value;
}
//----------//
// canMerge //
//----------//
/**
* Check whether the two provided filaments could be merged.
*
* @param one a filament
* @param two another filament
* @param expanding true when expanding filaments with sections left over
* @return true if test is positive
*/
private boolean canMerge (Glyph one,
Glyph two,
boolean expanding)
{
// For VIP debugging
final boolean areVips = one.isVip() && two.isVip();
String vips = null;
if (areVips) {
vips = one.getId() + "&" + two.getId() + ": "; // BP here!
}
try {
// Start & Stop points for each filament
Point2D oneStart = orientation.oriented(
one.getStartPoint(orientation));
Point2D oneStop = orientation.oriented(
one.getStopPoint(orientation));
Point2D twoStart = orientation.oriented(
two.getStartPoint(orientation));
Point2D twoStop = orientation.oriented(
two.getStopPoint(orientation));
// coord gap?
double overlapStart = Math.max(oneStart.getX(), twoStart.getX());
double overlapStop = Math.min(oneStop.getX(), twoStop.getX());
double coordGap = (overlapStart - overlapStop) - 1;
if (coordGap > params.maxCoordGap) {
if (logger.isDebugEnabled() || areVips) {
logger.info(
"{}Gap too long: {} vs {}",
vips, coordGap, params.maxCoordGap);
}
return false;
}
// pos gap?
if (coordGap < 0) {
// Overlap between the two filaments
// Determine maximum consistent resulting thickness
double maxConsistentThickness = maxConsistentThickness(one);
double maxSpace = expanding ? params.maxExpansionSpace
: params.maxSpace;
// Measure thickness at various coord values of overlap
// Provided that the overlap is long enough
int valNb = (int) Math.min(3, 1 - (coordGap / 10));
for (int iq = 1; iq <= valNb; iq++) {
double midCoord = overlapStart
- ((iq * coordGap) / (valNb + 1));
double onePos = one.getPositionAt(midCoord, orientation);
double twoPos = two.getPositionAt(midCoord, orientation);
double posGap = Math.abs(onePos - twoPos);
if (posGap > params.maxOverlapDeltaPos) {
if (logger.isDebugEnabled() || areVips) {
logger.info(
"{}Delta pos too high for overlap: {} vs {}",
vips, posGap, params.maxOverlapDeltaPos);
}
return false;
}
// Check resulting thickness at middle of overlap
double thickness = Glyphs.getThicknessAt(
midCoord,
orientation,
one,
two);
if (thickness > params.maxFilamentThickness) {
if (logger.isDebugEnabled() || areVips) {
logger.info(
"{}Too thick: {} vs {} {} {}",
vips, (float) thickness,
params.maxFilamentThickness, one, two);
}
return false;
}
// Check thickness consistency
if ((-coordGap <= params.maxInvolvingLength)
&& (thickness > maxConsistentThickness)) {
if (logger.isDebugEnabled() || areVips) {
logger.info(
"{}Non consistent thickness: {} vs {} {} {}",
vips, (float) thickness,
(float) maxConsistentThickness, one, two);
}
return false;
}
// Check space between overlapped filaments
double space = thickness
- (one.getThicknessAt(midCoord, orientation)
+ two.getThicknessAt(midCoord, orientation));
if (space > maxSpace) {
if (logger.isDebugEnabled() || areVips) {
logger.info(
"{}Space too large: {} vs {} {} {}",
vips, (float) space, maxSpace, one, two);
}
return false;
}
}
} else {
// No overlap, it's a true gap
Point2D start;
Point2D stop;
if (oneStart.getX() < twoStart.getX()) {
// one - two
start = oneStop;
stop = twoStart;
} else {
// two - one
start = twoStop;
stop = oneStart;
}
// Compute position gap, taking thickness into account
double oneThickness = one.getWeight() / one.getLength(
orientation);
double twoThickness = two.getWeight() / two.getLength(
orientation);
int posMargin = (int) Math.rint(
Math.max(oneThickness, twoThickness) / 2);
double posGap = Math.abs(stop.getY() - start.getY())
- posMargin;
if (posGap > params.maxPosGap) {
if (logger.isDebugEnabled() || areVips) {
logger.info(
"{}Delta pos too high for gap: {} vs {}",
vips, (float) posGap, params.maxPosGap);
}
return false;
}
// Check slope (relevant only for significant dy)
if (posGap > params.maxPosGapForSlope) {
double gapSlope = posGap / coordGap;
if (gapSlope > params.maxGapSlope) {
if (logger.isDebugEnabled() || areVips) {
logger.info(
"{}Slope too high for gap: {} vs {}",
vips, (float) gapSlope, params.maxGapSlope);
}
return false;
}
}
}
if (logger.isDebugEnabled() || areVips) {
logger.info("{}Compatible!", vips);
}
return true;
} catch (Exception ex) {
// Generally a stick for which some parameters cannot be computed
return false;
}
}
//----------------//
// createFilament //
//----------------//
private Glyph createFilament (Section section)
throws Exception
{
Filament fil = (Filament) filamentConstructor.newInstance(scaleArgs);
fil.addSection(section);
return nest.addGlyph(fil);
}
//-----------------//
// createFilaments //
//-----------------//
/**
* Aggregate long sections into initial filaments.
*/
private void createFilaments (Collection<Section> source)
throws Exception
{
// Sort sections by decreasing length in the desired orientation
List<Section> sections = new ArrayList<>(source);
Collections.sort(
sections,
Sections.getReverseLengthComparator(orientation));
for (Section section : sections) {
// Limit to main sections
if (section.getLength(orientation) < params.minCoreSectionLength) {
if (section.isVip()) {
logger.info("Too short {}", section);
}
continue;
}
if (isSectionFat(section)) {
if (section.isVip()) {
logger.info("Too fat {}", section);
}
continue;
}
Glyph fil = createFilament(section);
filaments.add(fil);
if (logger.isDebugEnabled() || section.isVip() || nest.isVip(fil)) {
logger.info(
"Created {} with {}", fil, section);
if (section.isVip() || nest.isVip(fil)) {
fil.setVip();
}
}
}
logger.debug("createFilaments: {}/{}", filaments.size(), source.size());
}
//-----------------//
// expandFilaments //
//-----------------//
/**
* Expand as much as possible the existing filaments with the
* provided sections.
*
* @param source the source of available sections
* @return the collection of expanded filaments
*/
private List<Glyph> expandFilaments (Collection<Section> source)
{
try {
// Sort sections by first position
List<Section> sections = new ArrayList<>();
for (Section section : source) {
if (!section.isGlyphMember() && !isSectionFat(section)) {
sections.add(section);
}
}
logger.debug("expandFilaments: {}/{}",
sections.size(), source.size());
Collections.sort(sections, Section.posComparator);
List<Glyph> glyphs = new ArrayList<>(sections.size());
for (Section section : sections) {
Glyph glyph = new BasicGlyph(scale.getInterline());
glyph.addSection(
section,
GlyphComposition.Linking.NO_LINK_BACK);
glyph = nest.addGlyph(glyph);
glyphs.add(glyph);
if (section.isVip() || nest.isVip(glyph)) {
logger.info("VIP created {} from {}", glyph, section);
glyph.setVip();
}
}
// List of filaments, sorted by decreasing length
Collections.sort(
filaments,
Glyphs.getReverseLengthComparator(orientation));
// Process each filament on turn
for (Glyph fil : filaments) {
// Build filament fat box
final Rectangle filBounds = orientation.oriented(
fil.getBounds());
filBounds.grow(params.maxCoordGap, params.maxPosGap);
boolean expanding = true;
do {
expanding = false;
for (Iterator<Glyph> it = glyphs.iterator(); it.hasNext();) {
Glyph glyph = it.next();
Rectangle glyphBounds = orientation.oriented(
glyph.getBounds());
if (filBounds.intersects(glyphBounds)) {
// Check more closely
if (canMerge(fil, glyph, true)) {
if (logger.isDebugEnabled()
|| fil.isVip()
|| glyph.isVip()) {
logger.info("Merging {} w/ {}",
fil,
Sections.toString(glyph.getMembers()));
if (glyph.isVip()) {
fil.setVip();
}
}
fil.stealSections(glyph);
it.remove();
expanding = true;
break;
}
} else {
if (fil.isVip() && glyph.isVip()) {
logger.info("No intersection between {} and {}",
fil, glyph);
}
}
}
} while (expanding);
}
} catch (Exception ex) {
logger.warn("FilamentsFactory cannot expandFilaments", ex);
}
return filaments;
}
//------------------------//
// maxConsistentThickness //
//------------------------//
private double maxConsistentThickness (Glyph stick)
{
double mean = stick.getWeight() / (double) stick.getLength(orientation);
if (mean < 2) {
return 2 * constants.maxConsistentRatio.getValue() * mean;
} else {
return constants.maxConsistentRatio.getValue() * mean;
}
}
//----------------//
// mergeFilaments //
//----------------//
/**
* Aggregate single-section filaments into long multi-section
* filaments.
*/
private void mergeFilaments ()
{
// List of filaments, sorted by decreasing length
Collections.sort(
filaments,
Glyphs.getReverseLengthComparator(orientation));
// Browse by decreasing filament length
for (Glyph current : filaments) {
Glyph candidate = current;
// Keep on working while we do have a candidate to check for merge
CandidateLoop:
while (true) {
final Rectangle candidateBounds = orientation.oriented(
candidate.getBounds());
candidateBounds.grow(params.maxCoordGap, params.maxPosGap);
// Check the candidate vs all filaments until current excluded
HeadsLoop:
for (Glyph head : filaments) {
if (head == current) {
break CandidateLoop; // Actual end of sub-list
}
if ((head != candidate) && (head.getPartOf() == null)) {
Rectangle headBounds = orientation.oriented(
head.getBounds());
if (headBounds.intersects(candidateBounds)) {
// Check for a possible merge
if (canMerge(head, candidate, false)) {
if (logger.isDebugEnabled()
|| head.isVip()
|| candidate.isVip()) {
logger.info(
"Merged {} into {}",
candidate, head);
if (candidate.isVip()) {
head.setVip();
}
}
head.stealSections(candidate);
candidate = head; // This is a new candidate
break HeadsLoop;
} else {
// if (head.isVip() || candidate.isVip()) {
// logger.info(
// "Could not merge " + candidate +
// " into " + head);
// }
}
} else {
if (head.isVip() && candidate.isVip()) {
logger.info(
"No intersection between {} and {}",
candidate, head);
}
}
}
}
}
}
// Discard the merged filaments
removeMergedFilaments();
}
//-----------------------//
// removeMergedFilaments //
//-----------------------//
private void removeMergedFilaments ()
{
for (Iterator<Glyph> it = filaments.iterator(); it.hasNext();) {
Glyph fil = it.next();
if (fil.getPartOf() != null) {
it.remove();
}
}
}
//~ Inner Classes ----------------------------------------------------------
//-----------//
// Constants //
//-----------//
private static final class Constants
extends ConstantSet
{
//~ Instance fields ----------------------------------------------------
Constant.Double maxGapSlope = new Constant.Double(
"tangent",
0.5,
"Maximum absolute slope for a gap");
Constant.Boolean printWatch = new Constant.Boolean(
false,
"Should we print out the stop watch?");
Constant.Ratio minSectionAspect = new Constant.Ratio(
3,
"Minimum section aspect (length / thixkness)");
Constant.Ratio maxConsistentRatio = new Constant.Ratio(
1.7,
"Maximum thickness ratio for consistent merge");
//
// Constants specified WRT mean line thickness
// -------------------------------------------
//
Scale.LineFraction maxSectionThickness = new Scale.LineFraction(
1.5,
"Maximum horizontal section thickness WRT mean line height");
Scale.LineFraction maxFilamentThickness = new Scale.LineFraction(
1.5,
"Maximum filament thickness WRT mean line height");
Scale.LineFraction maxPosGap = new Scale.LineFraction(
0.75,
"Maximum delta position for a gap between filaments");
//
// Constants specified WRT mean interline
// --------------------------------------
//
Scale.Fraction minCoreSectionLength = new Scale.Fraction(
1,
"Minimum length for a section to be considered as core");
Scale.Fraction maxOverlapDeltaPos = new Scale.Fraction(
0.5,
"Maximum delta position between two overlapping filaments");
Scale.Fraction maxCoordGap = new Scale.Fraction(
1,
"Maximum delta coordinate for a gap between filaments");
Scale.Fraction maxSpace = new Scale.Fraction(
0.16,
"Maximum space between overlapping filaments");
Scale.Fraction maxExpansionSpace = new Scale.Fraction(
0.02,
"Maximum space when expanding filaments");
Scale.Fraction maxPosGapForSlope = new Scale.Fraction(
0.1,
"Maximum delta Y to check slope for a gap between filaments");
Scale.Fraction maxInvolvingLength = new Scale.Fraction(
2,
"Maximum filament length to apply thickness test");
}
//------------//
// Parameters //
//------------//
/**
* Class {@code Parameters} gathers all scale-dependent parameters.
*/
private class Parameters
{
//~ Instance fields ----------------------------------------------------
/** Probe width */
public int probeWidth;
/** Maximum acceptable thickness for sections */
public int maxSectionThickness;
/** Maximum acceptable thickness for filaments */
public int maxFilamentThickness;
/** Minimum acceptable length for core sections */
public int minCoreSectionLength;
/** Maximum acceptable delta position */
public int maxOverlapDeltaPos;
/** Maximum delta coordinate for real gap */
public int maxCoordGap;
/** Maximum delta position for real gaps */
public int maxPosGap;
/** Maximum space between overlapping filaments */
public int maxSpace;
/** Maximum space for expansion */
public int maxExpansionSpace;
/** Maximum filament length to apply thickness test */
public int maxInvolvingLength;
/** Maximum dy for slope check on real gap */
public int maxPosGapForSlope;
/** Minimum acceptable aspect for sections */
public double minSectionAspect;
/** Maximum slope for real gaps */
public double maxGapSlope;
//~ Methods ------------------------------------------------------------
public void dump ()
{
Main.dumping.dump(this);
}
/**
* Initialize with default values
*/
public void initialize ()
{
setMinCoreSectionLength(constants.minCoreSectionLength);
setMaxSectionThickness(constants.maxSectionThickness);
setMaxFilamentThickness(constants.maxFilamentThickness);
setMaxCoordGap(constants.maxCoordGap);
setMaxPosGap(constants.maxPosGap);
setMaxSpace(constants.maxSpace);
setMaxExpansionSpace(constants.maxExpansionSpace);
setMaxInvolvingLength(constants.maxInvolvingLength);
setMaxPosGapForSlope(constants.maxPosGapForSlope);
setMaxOverlapDeltaPos(constants.maxOverlapDeltaPos);
setMaxGapSlope(constants.maxGapSlope.getValue());
setMinSectionAspect(constants.minSectionAspect.getValue());
probeWidth = scale.toPixels(BasicAlignment.getProbeWidth());
if (logger.isDebugEnabled()) {
dump();
}
}
}
}