//----------------------------------------------------------------------------//
// //
// S t i c k s B u i l d 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.stick;
import omr.Main;
import omr.check.FailureResult;
import omr.check.SuccessResult;
import omr.constant.Constant;
import omr.constant.ConstantSet;
import omr.glyph.Glyphs;
import omr.glyph.Nest;
import omr.glyph.facets.BasicGlyph;
import omr.glyph.facets.Glyph;
import omr.lag.BasicSection;
import omr.lag.Section;
import omr.lag.Sections;
import omr.run.Orientation;
import omr.sheet.Scale;
import static omr.stick.SectionRole.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Class {@code SticksBuilder} introduces the scanning of a source of
* sections, to retrieve sticks.
*
* <p> The same algorithms are used for all kinds of sticks with only one
* difference, depending on length of sticks we are looking for, since long
* alignments (this applies to staff lines only) can exhibit not very straight
* lines as opposed to bar lines, stems or ledgers.
*
* <ul> <li> <b>Horizontal sticks</b> can be (chunks of) staff lines, alternate
* ending, or ledgers. </li>
*
* <li> <b>Vertical sticks</b> can be bar lines, or stems. </li> </ul> </p>
*
* @author Hervé Bitteur
*/
public class SticksBuilder
{
//~ Static fields/initializers ---------------------------------------------
/** Specific application parameters */
private static final Constants constants = new Constants();
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(SticksBuilder.class);
/** Unique identifier for debugging */
private static int globalId = 0;
/** A too small stick */
private static final FailureResult TOO_SMALL = new FailureResult(
"SticksBuilder-TooSmall");
/** A stick whose slope is not correct */
private static final FailureResult NOT_STRAIGHT = new FailureResult(
"SticksBuilder-NotStraight");
/** A stick correctly assigned */
private static final SuccessResult ASSIGNED = new SuccessResult(
"SticksBuilder-Assigned");
/** For comparing sections, according to the thickening relevance */
private static final Comparator<Section> thickeningComparator = new Comparator<Section>()
{
@Override
public int compare (Section s1,
Section s2)
{
// Criteria #1 is layer number (small layer numbers
// first) Plus section thickness for non layer zero
// sections
int layerDiff;
StickRelation r1 = s1.getRelation();
StickRelation r2 = s2.getRelation();
if ((r1.layer == 0) || (r2.layer == 0)) {
layerDiff = r1.layer - r2.layer;
} else {
layerDiff = (r1.layer + s1.getRunCount())
- (r2.layer + s2.getRunCount());
}
if (layerDiff != 0) {
return layerDiff;
} else {
// Criteria #2 is section length (large lengths
// first)
return s2.getMaxRunLength() - s1.getMaxRunLength();
}
}
};
//~ Instance fields --------------------------------------------------------
/** Desired orientation of sticks */
protected final Orientation orientation;
/** The related scale */
protected final Scale scale;
/** The containing nest */
protected final Nest nest;
/** Class parameters */
protected final Parameters params;
/** The source adapter to retrieve sections from */
protected final SectionsSource source;
/** A flag used to trigger processing specific to very long (and not totally
* straight) alignments */
private final boolean longAlignment;
/** The <b>sorted</b> collection of sticks found in this area */
protected List<Glyph> sticks = new ArrayList<>();
/** Sections which are potential future members */
private List<Section> candidates = new ArrayList<>();
/** Sections recognized as members of sticks */
private List<Section> members = new ArrayList<>();
/** Used to flag sections already visited wrt a given stick */
private Map<Section, Glyph> visited;
/** Instance data for the area */
private int id = ++globalId;
//~ Constructors -----------------------------------------------------------
/**
* Creates a new SticksBuilder object.
*
* @param orientation general orientation of desired sticks
* @param scale the related scale
* @param nest the nest which hosts the glyphs
* @param source An adaptor to get access to participating sections
* @param longAlignment specific flag to indicate long filament retrieval
*/
public SticksBuilder (Orientation orientation,
Scale scale,
Nest nest,
SectionsSource source,
boolean longAlignment)
{
// Cache computing parameters
this.orientation = orientation;
this.scale = scale;
this.nest = nest;
this.source = source;
this.longAlignment = longAlignment;
// Default parameters values
params = new Parameters();
params.initialize();
}
//~ Methods ----------------------------------------------------------------
//------------//
// canConnect //
//------------//
public boolean canConnect (Glyph one,
Glyph two,
int maxDeltaCoord,
int maxDeltaPos)
{
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));
if (Math.abs(oneStop.getX() - twoStart.getX()) <= maxDeltaCoord) {
// Case: this ... that
if (Math.abs(twoStart.getY() - oneStop.getY()) <= maxDeltaPos) {
return true;
}
}
if (Math.abs(twoStop.getX() - oneStart.getX()) <= maxDeltaCoord) {
// Case: that ... this
if (Math.abs(twoStop.getY() - oneStart.getY()) <= maxDeltaPos) {
return true;
}
}
return false;
}
//--------------//
// createSticks //
//--------------//
/**
* Perform the initialization of the newly allocated instance. This
* consists in scanning the given area, using the source provided. The
* resulting sticks are available through the getSticks method.
*
* @param preCandidates (maybe null) a list of predefined candidate sections
*/
public void createSticks (List<Section> preCandidates)
{
// Use a brand new glyph map
visited = new HashMap<>();
// Do we have pre-candidates to start with ?
if (preCandidates != null) {
candidates = preCandidates;
} else {
// Browse through our sections, to collect the long core sections
List<Section> target;
if (longAlignment) {
// For long alignments (staff lines), we use an intermediate
// list of candidates
target = candidates;
} else {
// For short alignment (such as stems and others), we fill
// members directly
target = members;
}
while (source.hasNext()) {
Section section = source.next();
// By vertue of the Source adaptor, all provided sections are
// entirely within the stick area. So tests for core sections
// are not already taken, thickness and length, that's all.
if (!section.isGlyphMember()
&& (section.getRunCount() <= params.maxThickness)
&& (section.getMaxRunLength() >= params.minCoreLength)) {
// OK, this section is candidate as core member of stick set
mark(section, target, SectionRole.CORE, 0, 0);
}
}
}
if (longAlignment) {
// Get rid of candidates too far from the staff line core
thickenAlignmentCore(candidates, members);
}
logger.debug("{}{}",
members.size(), Sections.toString(" Core sections", members));
// Collect candidate sections around the core ones
for (Section section : members) {
collectNeighborsOf(section, 1, -1, true); // Before the core section
collectNeighborsOf(section, 1, +1, true); // After the core section
}
// Purge some internal sections
///purgeInternals(candidates);
//
if (longAlignment) {
// Thicken the core sections with the candidates, still paying
// attention to the resulting thickness of the sticks.
thickenAlignmentCore(candidates, members);
///purgeInternals(members);
} else {
members.addAll(candidates);
}
logger.debug("{}{}",
members.size(), Sections.toString(" total sections", members));
// Aggregate member sections into as few sticks as possible.
// This creates instances in sticks list.
aggregateMemberSections();
// Compute stick lines and check the resulting slope. This may result in
// discarded sticks.
for (Iterator<Glyph> it = sticks.iterator(); it.hasNext();) {
Glyph stick = it.next();
// Glyph size (at least 3 points)
if (stick.getWeight() < 3) {
stick.setResult(TOO_SMALL);
for (Section section : stick.getMembers()) {
logger.debug("Discarding too small stick {}", section);
discard(section);
}
it.remove();
} else {
// Glyph slope must be close to expected value
double stickSlope = (orientation == Orientation.VERTICAL)
? stick.getInvertedSlope() : stick.getSlope();
if (Math.abs(stickSlope - params.expectedSlope) > params.slopeMargin) {
stick.setResult(NOT_STRAIGHT);
for (Section section : stick.getMembers()) {
logger.debug("Discarding not straight stick {}", section);
discard(section);
}
it.remove();
}
}
}
// if (logger.isDebugEnabled()) {
// dump(true);
// }
}
//------//
// dump //
//------//
/**
* A debugging routine, which dumps any pertinent info to standard output.
*
* @param withContent flag to specify that sticks are to be dumped with
* their content
*/
public void dump (boolean withContent)
{
params.dump();
System.out.println("StickArea#" + id + " size=" + sticks.size());
for (Glyph stick : sticks) {
System.out.println(stick.dumpOf());
}
}
//-----------//
// getSticks //
//-----------//
/**
* Returns the collection of sticks found in this area
*
* @return the sticks found
*/
public List<Glyph> getSticks ()
{
return sticks;
}
//-------------//
// isDiscarded //
//-------------//
/**
* Checks whether a given section has been discarded
*
* @param section the section to check
*
* @return true if actually discarded
*/
public static boolean isDiscarded (Section section)
{
StickRelation relation = section.getRelation();
return (relation != null) && (relation.role == DISCARDED);
}
//-------//
// reset //
//-------//
/**
* Used to reset the ids of stick areas (for debugging mainly)
*/
public static void reset ()
{
globalId = 0;
}
//----------------//
// retrieveSticks //
//----------------//
/**
* Perform the sticks retrieval (creation & merge) based on the parameters
* defined for this area
*
* @return the sticks retrieved
*/
public List<Glyph> retrieveSticks ()
{
// Retrieve the stick(s)
createSticks(null);
// Merge aligned verticals
merge();
// Sort sticks found
Collections.sort(sticks, Glyph.byId);
logger.debug("End of scanning area, found {} stick(s): {}",
sticks.size(), Glyphs.toString(sticks));
return sticks;
}
//------------------//
// setExpectedSlope //
//------------------//
public void setExpectedSlope (double value)
{
params.expectedSlope = value;
}
//-----------------//
// setMaxAdjacency //
//-----------------//
public void setMaxAdjacency (double value)
{
params.maxAdjacency = value;
}
//------------------//
// setMaxDeltaCoord //
//------------------//
public void setMaxDeltaCoord (Scale.Fraction frac)
{
params.maxDeltaCoord = scale.toPixels(frac);
}
//----------------//
// setMaxDeltaPos //
//----------------//
public void setMaxDeltaPos (Scale.Fraction frac)
{
params.maxDeltaPos = scale.toPixels(frac);
}
//-----------------//
// setMaxThickness //
//-----------------//
public void setMaxThickness (Scale.Fraction frac)
{
params.maxThickness = scale.toPixels(frac);
}
//-----------------//
// setMaxThickness //
//-----------------//
public void setMaxThickness (Scale.LineFraction lFrac)
{
params.maxThickness = scale.toPixels(lFrac);
}
//------------------//
// setMinCoreLength //
//------------------//
public void setMinCoreLength (Scale.Fraction frac)
{
setMinCoreLength(scale.toPixels(frac));
}
//------------------//
// setMinCoreLength //
//------------------//
public void setMinCoreLength (int value)
{
params.minCoreLength = value;
}
//---------------------//
// setMinSectionAspect //
//---------------------//
public void setMinSectionAspect (double value)
{
params.minSectionAspect = value;
}
//----------------//
// setSlopeMargin //
//----------------//
public void setSlopeMargin (double value)
{
params.slopeMargin = value;
}
//-------//
// merge //
//-------//
/**
* Merge all sticks found in the area, provided they can be considered
* extensions of one another, according to the current proximity parameters
*/
protected void merge ()
{
final long startTime = System.currentTimeMillis();
// Sort on stick mid position first
Collections.sort(
sticks,
new Comparator<Glyph>()
{
@Override
public int compare (Glyph s1,
Glyph s2)
{
return s1.getMidPos(orientation)
- s2.getMidPos(orientation);
}
});
// Then use position to narrow the tests
List<Glyph> removals = new ArrayList<>();
int index = -1;
for (Glyph stick : sticks) {
index++;
List<Glyph> tail = sticks.subList(index + 1, sticks.size());
boolean merging = true;
while (merging) {
Rectangle stickBounds = orientation.oriented(
stick.getBounds());
stickBounds.grow(params.maxDeltaCoord, params.maxDeltaPos);
Rectangle stickBox = orientation.absolute(stickBounds);
// final int maxPos = stick.getMidPos() + (20 * maxDeltaPos);
merging = false;
for (Iterator<Glyph> it = tail.iterator(); it.hasNext();) {
Glyph other = it.next();
// Check other has not been removed yet
if (removals.contains(other)) {
continue;
}
if (other.getBounds().intersects(stickBox)) {
if (canConnect(
stick,
other,
params.maxDeltaCoord,
params.maxDeltaPos)) {
int oldId = stick.getId();
stick.stealSections(other);
stick = nest.addGlyph(stick);
if (logger.isDebugEnabled()
&& (stick.getId() != oldId)) {
logger.debug("Merged sticks #{} & #{} => #{}",
oldId, other.getId(), stick.getId());
}
removals.add(other);
merging = true;
break;
}
}
}
}
}
sticks.removeAll(removals);
logger.debug("merged {} sticks in {} ms",
removals.size(), System.currentTimeMillis() - startTime);
}
//-----------//
// aggregate //
//-----------//
/**
* Try to aggregate this section (and its neighbors) to the stick
*
* @param section the section to aggregate
* @param stick the stick to which section is to be aggregated
*/
private void aggregate (Section section,
Glyph stick)
{
if (visited.get(section) != stick) {
visited.put(section, stick);
if (section.isAggregable()) {
// Check that resulting stick thickness would still be OK
if (isClose(
stick.getMembers(),
section,
params.maxThickness + 1)) {
stick.addSection(section, Glyph.Linking.LINK_BACK);
// Aggregate recursively other sections
for (Section source : section.getSources()) {
aggregate(source, stick);
}
for (Section target : section.getTargets()) {
aggregate(target, stick);
}
}
}
}
}
//-------------------------//
// aggregateMemberSections //
//-------------------------//
/**
* Aggregate member sections into sticks. We start with no stick at all,
* then consider each member section on its turn. If the section is not
* aggregated it begins a new stick. Otherwise we try to aggregate the
* section to one of the sticks identified so far.
*/
private void aggregateMemberSections ()
{
final int interline = scale.getInterline();
for (Section section : members) {
if (section.isAggregable()) {
Glyph stick = new BasicGlyph(interline);
stick.setResult(ASSIGNED); // Needed to flag the stick
aggregate(section, stick);
stick = nest.addGlyph(stick);
stick.setResult(ASSIGNED); // In case we are reusing a glyph
sticks.add(stick);
}
}
}
//--------------------//
// collectNeighborsOf //
//--------------------//
/**
* Look for neighbors of this section, in the given direction. If a
* neighboring sections qualifies, it is assigned the provided layer level.
*
* @param section the section from which neighbors are considered
* @param layer the current layer number of the section, regarding the
* stick
* @param direction the direction to look into, which is coded as
* <b>+1</b> for looking at outgoing edges, and
* <b>-1</b> for looking at incoming edges.
*/
private void collectNeighborsOf (Section section,
int layer,
int direction,
boolean internalAllowed)
{
if (direction > 0) {
for (Section target : section.getTargets()) {
collectSection(target, layer, direction, internalAllowed);
}
} else {
for (Section source : section.getSources()) {
collectSection(source, layer, direction, internalAllowed);
}
}
}
//----------------//
// collectSection //
//----------------//
/**
* Try to collect this section as a candidate.
*
* @param section the condidate section
* @param direction which direction we are moving to
* @param internalAllowed false if we are reaching the stick border
*/
private void collectSection (Section section,
int layer,
int direction,
boolean internalAllowed)
{
// We consider only free (not yet assigned) sections, which are
// located within the given area.
if (!isFree(section) || !source.isInArea(section)) {
return;
}
// If the section being checked is already too thick compared with the
// stick we are looking at, then we discard the section.
if (isTooThick(section)) {
// Mark the section, so that we don't retry the same one via another
// path
mark(section, null, TOO_THICK, layer, direction);
return;
}
if (longAlignment) {
// We don't collect sections that are too far from the center of
// member sections, since this would result in a too thick line.
if (!isClose(members, section, params.maxThickness + 1)) {
mark(section, null, TOO_FAR, layer, direction);
return;
}
}
// Include only sections that are slim enough
if ((section.getRunCount() > 1)
&& (section.getAspect(orientation) < params.minSectionAspect)) {
mark(section, null, TOO_FAT, layer, direction);
return;
}
// Check that section is adjacent to open space
final double adjacency = (direction > 0) ? section.getLastAdjacency()
: section.getFirstAdjacency();
if (adjacency <= params.maxAdjacency) {
mark(section, candidates, PERIPHERAL, layer, direction);
///collectNeighborsOf(section, layer + 1, direction, false);
collectNeighborsOf(section, layer - 1, -direction, true);
} else {
if (internalAllowed) {
mark(section, candidates, INTERNAL, layer, direction);
collectNeighborsOf(section, layer + 1, direction, false);
} else {
mark(section, null, TOO_FAR, layer, direction);
}
}
}
//---------//
// discard //
//---------//
/**
* Flag a section (using its related data) as discarded.
*
* @param section the section to discard
*/
private void discard (Section section)
{
StickRelation relation = section.getRelation();
if ((relation != null) && relation.isCandidate()) {
relation.role = DISCARDED;
section.setGlyph(null);
}
}
//---------//
// isClose //
//---------//
/**
* Check that the section would not thicken too much the stick being built,
* and whose members are passed as parameter
*
* @param members the members to compute distance to
* @param section the section to check
* @param maxThickness the maximum resulting stick thickness
*
* @return true if OK
*/
private boolean isClose (Collection<Section> members,
Section section,
int maxThickness)
{
// Just to speed up
final int start = section.getStartCoord();
final int stop = section.getStopCoord();
final int firstPos = section.getFirstPos();
final int lastPos = section.getLastPos();
// Check real stick thickness so far
for (Section sct : members) {
// Check overlap in abscissa with section at hand
if (Math.max(start, sct.getStartCoord()) <= Math.min(
stop,
sct.getStopCoord())) {
// Check global thickness
int thick;
if (sct.getFirstPos() > firstPos) {
thick = sct.getLastPos() - firstPos + 1;
} else {
thick = lastPos - sct.getFirstPos() + 1;
}
if (thick > maxThickness) {
logger.debug("Too thick real line ({}) {}", thick, section);
return false;
}
}
}
return true;
}
//--------//
// isFree //
//--------//
private boolean isFree (Section section)
{
StickRelation relation = section.getRelation();
return (relation == null) || (relation.role == null);
}
//------------//
// isTooThick //
//------------//
/**
* Check whether the given section is too thick (thicker than the allowed
* maxThickness).
*
* @param section the section to check
*
* @return true if section is too thick
*/
private boolean isTooThick (Section section)
{
return section.getRunCount() > params.maxThickness;
}
//------//
// mark //
//------//
/**
* Mark the section with the given flag, and insert it in the provided list
* if any
*
* @param section the section to mark
* @param list the list (if any) to add the section to
* @param role section role
* @param layer section layer in the stick
* @param direction section direction in the stick
*/
private void mark (Section section,
List<Section> list,
SectionRole role,
int layer,
int direction)
{
((BasicSection) section).setParams(role, layer, direction);
if (list != null) {
list.add(section);
}
}
//----------------------//
// thickenAlignmentCore //
//----------------------//
/**
* This routine is used only in the special case of long alignment, meaning
* staff line detection. The routine uses and then clears the candidate
* list. The sections that successfully pass the tests are added to the
* members list.
*
* @param ins input list of candidates (consumed)
* @param outs output list of members (appended)
*/
private void thickenAlignmentCore (List<Section> ins,
List<Section> outs)
{
// Sort ins according to their relevance
Collections.sort(ins, thickeningComparator);
// Using the priority order, let's thicken the stick core
for (Section section : ins) {
if (!isDiscarded(section)) {
if (isClose(outs, section, params.maxThickness)) {
// OK, it is now a member of the stick
outs.add(section);
} else {
// Get rid of this one
discard(section);
}
}
}
// Get rid of the ins list
ins.clear();
}
//~ Inner Classes ----------------------------------------------------------
//------------//
// Parameters //
//------------//
/**
* Class {@code Parameters} gathers all parameters for SticksBuilder
*/
protected class Parameters
{
//~ Instance fields ----------------------------------------------------
/** Expected (oriented) slope for sticks */
public double expectedSlope;
/** Margin around expected slope */
public double slopeMargin;
/** Minimum value for length of core sections */
public int minCoreLength;
/** Maximum value for adjacency */
public double maxAdjacency;
/** Maximum thickness value for sticks */
public int maxThickness;
/** Minimum aspect (length / thickness) for a section */
public double minSectionAspect;
/** Maximum gap in coordinate when merging sticks */
public int maxDeltaCoord;
/** Maximum gap in position when merging sticks */
public int maxDeltaPos;
//~ Methods ------------------------------------------------------------
public void dump ()
{
Main.dumping.dump(this);
}
/**
* Initialize with default values
*/
public void initialize ()
{
setExpectedSlope(0d);
setSlopeMargin(constants.slopeMargin.getValue());
setMinCoreLength(constants.coreSectionLength);
setMaxAdjacency(constants.maxAdjacency.getValue());
setMaxThickness(constants.maxThickness);
setMinSectionAspect(constants.minSectionAspect.getValue());
setMaxDeltaCoord(constants.maxDeltaCoord);
setMaxDeltaPos(constants.maxDeltaPos);
if (logger.isDebugEnabled()) {
dump();
}
}
}
//-----------//
// Constants //
//-----------//
private static final class Constants
extends ConstantSet
{
//~ Instance fields ----------------------------------------------------
Constant.Double slopeMargin = new Constant.Double(
"tangent",
0.04d,
"Maximum slope value for a stick");
Scale.Fraction coreSectionLength = new Scale.Fraction(
1.5, // 2.0
"Minimum length of a section to be processed");
Constant.Ratio maxAdjacency = new Constant.Ratio(
0.8d,
"Maximum adjacency ratio to be a true vertical line");
Scale.Fraction maxThickness = new Scale.Fraction(
0.3,
"Maximum thickness for resulting stick");
Constant.Ratio minSectionAspect = new Constant.Ratio(
6.7d,
"Minimum value for section aspect (length / thickness)");
Scale.Fraction maxDeltaCoord = new Scale.Fraction(
0.175,
"Maximum difference of ordinates when merging two sticks");
Scale.Fraction maxDeltaPos = new Scale.Fraction(
0.1,
"Maximum difference of abscissa when merging two sticks");
Constant.Angle maxSlope = new Constant.Angle(
0.04d,
"Maximum slope value for a stick to be vertical");
}
}