//----------------------------------------------------------------------------//
// //
// B o r d e r 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.sheet;
import omr.constant.Constant;
import omr.constant.ConstantSet;
import omr.glyph.Glyphs;
import omr.glyph.Shape;
import omr.glyph.facets.Glyph;
import omr.grid.Filament;
import omr.grid.FilamentLine;
import omr.grid.LineInfo;
import omr.math.NaturalSpline;
import static omr.run.Orientation.*;
import omr.util.BrokenLine;
import omr.util.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* Class {@code BorderBuilder} implements a smart approach
* to define the border between two systems.
*
* <p>Strategy for glyph assignment: <ol>
* <li>Identify boxes of all glyphs intersected by the intersystem gutter.</li>
* <li>Elaborate box-based continuous limit of both staves.</li>
* <li>Use the (not too small) glyphs from yellow zone as free boxes seeds.</li>
* <li>Enlarge free boxes to come up with horizontal blobs.</li>
* <li>Incrementally:
* <ol>
* <li>"Grow" free boxes in vertical directions (dy = 1)</li>
* <li>Re-evaluate intersections (with other free boxes & limit boxes)</li>
* <li>Intersection with a staff limit assigns the initial free box to it</li>
* <li>Exit when no free box is left</li>
* </ol></ol></p>
*
* <p>Strategy for border definition: <ol>
* <li>Use ordinate middle between the two (enlarged) box-based limits.</li>
* <li>Remove all unneeded intermediate points.</li>
* </ol></p>
*
*
* @author Hervé Bitteur
*/
public class BorderBuilder
{
//~ Static fields/initializers ---------------------------------------------
/** Specific application parameters */
private static final Constants constants = new Constants();
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(BorderBuilder.class);
//~ Instance fields --------------------------------------------------------
//
/** Related sheet. */
private final Sheet sheet;
/** System above. */
private final SystemInfo prevSystem;
/** System below. */
private final SystemInfo system;
// GlyphRect-based limits
private Limit topLimit;
private Limit botLimit;
/** Free blobs. */
private List<GlyphRect> blobs = new ArrayList<>();
/** Has the user been warned about a closed border?. */
private boolean userWarned;
// Scale-dependent parameters
private final double flatness;
private final int minGlyphWeight;
private final int xMargin;
private final int yMargin;
//~ Constructors -----------------------------------------------------------
//
//---------------//
// BorderBuilder //
//---------------//
/**
* Creates a new BorderBuilder object.
*
* @param sheet the related sheet
* @param prevSystem system on north side
* @param system system on south side
*/
public BorderBuilder (Sheet sheet,
SystemInfo prevSystem,
SystemInfo system)
{
this.sheet = sheet;
this.prevSystem = prevSystem;
this.system = system;
Scale scale = sheet.getScale();
flatness = scale.toPixelsDouble(constants.lineFlatness);
minGlyphWeight = scale.toPixels(constants.minGlyphWeight);
xMargin = scale.toPixels(constants.xMargin);
yMargin = scale.toPixels(constants.yMargin);
}
//~ Methods ----------------------------------------------------------------
//
//-------------//
// buildBorder //
//-------------//
public BrokenLine buildBorder ()
{
// First, retrieve glyphs intersected by intersystem rectangle
LineInfo topLine = prevSystem.getLastStaff().getLastLine();
LineInfo botLine = system.getFirstStaff().getFirstLine();
Rectangle box = topLine.getBounds();
box.add(botLine.getBounds());
final Rectangle interBox = box;
Set<Glyph> glyphs = sheet.getNest().lookupIntersectedGlyphs(interBox);
// Remove small glyphs and the staff lines themselves
// Also remove glyphs that embrace the whole intersystem
Glyphs.purge(
glyphs,
new Predicate<Glyph>()
{
@Override
public boolean check (Glyph glyph)
{
// Purge staff lines
if (glyph.getShape() == Shape.STAFF_LINE) {
return true;
}
// Purge abnormally tall glyphs
Rectangle glyphBox = glyph.getBounds();
if (glyphBox.y <= interBox.y
&& glyphBox.y + glyphBox.height >= interBox.y + interBox.height) {
return true;
}
// Purge too light glyphs
if (glyph.getWeight() < minGlyphWeight) {
return true;
}
return false;
}
});
// Split the set between staff limits and free glyphs in the middle
topLimit = buildLimit(topLine, glyphs, -1);
botLimit = buildLimit(botLine, glyphs, +1);
logger.debug("topLimit: {}", topLimit);
logger.debug("botLimit: {}", botLimit);
// Aggregate free glyphs into fewer blobs
buildFreeBlobs(glyphs);
// Assign each free blob to proper limit
assignFreeBlobs();
// Build a raw border out of the two limits
BrokenLine rawBorder = getRawBorder();
if (constants.useSmoothBorders.isSet()) {
// Return the smooth border
return getSmoothBorder(rawBorder);
} else {
return rawBorder;
}
}
//----------//
// idString //
//----------//
public String idString ()
{
StringBuilder sb = new StringBuilder();
sb.append("Border between systems S#").append(prevSystem.getId())
.append(" & S#").append(system.getId());
return sb.toString();
}
//-----------------//
// assignFreeBlobs //
//-----------------//
/**
* Iterate on growing free blobs, until they get assigned to a limit.
*/
private void assignFreeBlobs ()
{
int delta = system.getTop() - prevSystem.getBottom();
for (int i = 1; i < delta; i++) {
logger.debug("i:{}", i);
// Thicken free blobs and revaluate position WRT limits
int index = -1;
for (Iterator<GlyphRect> it = blobs.iterator(); it.hasNext();) {
Rectangle blob = it.next();
index++;
Rectangle rect = new Rectangle(blob);
rect.grow(0, i);
if (topLimit.intersects(rect)) {
topLimit.add(blob);
logger.debug("topLimit <- {}", blob);
it.remove();
} else if (botLimit.intersects(rect)) {
botLimit.add(blob);
logger.debug("botLimit <- {}", blob);
it.remove();
} else {
// Fusion with another blob?
if (index < (blobs.size() - 1)) {
for (Rectangle b : blobs.subList(
index + 1,
blobs.size())) {
if (b.intersects(rect)) {
logger.debug("{} + {}", b, blob);
b.add(blob);
it.remove();
break;
}
}
}
}
}
if (blobs.isEmpty()) {
break;
}
}
}
//----------------//
// buildFreeBlobs //
//----------------//
/**
* Aggregate the free glyphs into a reduced number of horizontal
* rectangles
*
* @param glyphs the free glyphs
*/
private void buildFreeBlobs (Collection<Glyph> glyphs)
{
for (Glyph glyph : glyphs) {
Rectangle rect = glyph.getBounds();
rect.grow(xMargin, yMargin);
boolean aggregated = false;
// Check if we can aggregate to an existing blob
for (GlyphRect blob : blobs) {
if (rect.intersects(blob)) {
blob.add(glyph);
aggregated = true;
break;
}
}
if (!aggregated) {
// Create a brand new blob
blobs.add(new GlyphRect(glyph));
}
}
if (logger.isDebugEnabled()) {
for (Rectangle blob : blobs) {
logger.debug("free: {}", blob);
}
}
}
//------------//
// buildLimit //
//------------//
/**
* Pickup the glyphs intersected by the provided line, and purge the
* provided collection of these intersecting glyphs, as well as the
* non intersecting glyphs which are located in the 'dir' direction
* with respect to the line.
*
* @param lineInfo the line to intersect
* @param glyphs the initial collection of glyphs
* @param dir the direction in which glyphs are removed
* @return the limit made of glyphs on the line
*/
private Limit buildLimit (LineInfo lineInfo,
Collection<Glyph> glyphs,
int dir)
{
List<Glyph> lineGlyphs = new ArrayList<>();
FilamentLine filamentLine = (FilamentLine) lineInfo;
Filament fil = filamentLine.getFilament();
NaturalSpline line = (NaturalSpline) fil.getLine();
for (Iterator<Glyph> it = glyphs.iterator(); it.hasNext();) {
Glyph glyph = it.next();
if (line.intersects(glyph.getBounds(), flatness)) {
lineGlyphs.add(glyph);
it.remove();
} else {
// Check glyph position WRT line
Point center = glyph.getAreaCenter();
int y = filamentLine.yAt(center.x);
if (((center.y - y) * dir) > 0) {
it.remove();
}
}
}
// Now build the line-based limit
Limit limit = new Limit();
int x1 = 0;
for (Glyph glyph : lineGlyphs) {
Rectangle contour = glyph.getBounds();
if (contour.x > x1) {
// We need to insert an artificial box, based on line segment
int y1 = (int) Math.rint(fil.positionAt(x1, HORIZONTAL));
int y2 = (int) Math.rint(fil.positionAt(contour.x, HORIZONTAL));
limit.boxes.add(new LineRect(x1, y1, contour.x, y2, dir));
}
// Insert the current glyph box
limit.boxes.add(new GlyphRect(glyph));
x1 = contour.x + contour.width;
}
// Complete the limit
int y1 = (int) Math.rint(fil.positionAt(x1, HORIZONTAL));
int y2 = (int) Math.rint(fil.positionAt(sheet.getWidth(), HORIZONTAL));
limit.boxes.add(new LineRect(x1, y1, sheet.getWidth(), y2, dir));
return limit;
}
//--------------//
// getRawBorder //
//--------------//
/**
* Build a broken line as the "middle" between top & bottom limits
*
* @return the (raw) border
*/
private BrokenLine getRawBorder ()
{
// Smoothens the border, but may lead to impossible borders
topLimit.grow(xMargin, 0);
botLimit.grow(xMargin, 0);
//
BrokenLine line = new BrokenLine();
int yPrev = -1;
int xPrev = -1;
for (int x = 0, xMax = sheet.getWidth(); x <= xMax; x++) {
int top = topLimit.getY(x, -1);
int bot = botLimit.getY(x, +1);
int y = (top + bot) / 2;
if (top > bot && !userWarned) {
logger.warn("{}{} got closed at x:{} y:{}",
sheet.getLogPrefix(), idString(), x, y);
userWarned = true;
}
if (x == xMax) {
// Very last point
line.addPoint(new Point(x, y));
} else if (y != yPrev) {
if (x > (xPrev + 1)) {
line.addPoint(new Point(x - 1, yPrev));
}
line.addPoint(new Point(x, y));
xPrev = x;
yPrev = y;
}
}
logger.debug("Raw border: {}", line);
return line;
}
//-----------------//
// getSmoothBorder //
//-----------------//
/**
* Smoothen the raw border as much as possible
*
* @param line the initial (raw) border
* @return the smooth border
*/
private BrokenLine getSmoothBorder (BrokenLine line)
{
// 1/Start with left point
// 2/Try to skip the following points until a limit is intersected
// 3/Backup to previous point and keep it
// 4/Goto 2/
// Complete with right side
int lastIndex = 0;
Removal:
while (true) {
Point lastPoint = line.getPoint(lastIndex);
for (int index = lastIndex + 1; index < line.size(); index++) {
Point pt = line.getPoint(index);
if (topLimit.intersects(lastPoint, pt)
|| botLimit.intersects(lastPoint, pt)) {
// Step back
for (int i = lastIndex + 1, iBreak = index - 1; i < iBreak;
i++) {
Point p = line.getPoint(lastIndex + 1);
line.removePoint(p);
logger.debug("Removed {}", p);
}
lastIndex++;
continue Removal;
}
}
break;
}
for (int i = lastIndex + 1, iBreak = line.size() - 1; i < iBreak;
i++) {
Point p = line.getPoint(lastIndex + 1);
line.removePoint(p);
}
logger.debug("{}Smart S{}-S{} system border: {}",
sheet.getLogPrefix(), prevSystem.getId(), system.getId(), line);
return line;
}
//~ Inner Classes ----------------------------------------------------------
//-----------//
// Constants //
//-----------//
private static final class Constants
extends ConstantSet
{
Scale.Fraction lineFlatness = new Scale.Fraction(
0.5,
"Maximum flattening distance");
Scale.Fraction xMargin = new Scale.Fraction(
2,
"Inter blob horizontal margin");
Scale.Fraction yMargin = new Scale.Fraction(
0,
"Inter blob vertical margin");
Scale.AreaFraction minGlyphWeight = new Scale.AreaFraction(
0.1,
"Minimum weight for free glyph");
Constant.Boolean useSmoothBorders = new Constant.Boolean(
true,
"Should we use smooth inter-system borders?");
}
//-----------//
// GlyphRect //
//-----------//
/**
* A standard rectangle, which keeps track of its building glyphs.
*/
private static class GlyphRect
extends Rectangle
{
//~ Instance fields ----------------------------------------------------
/** Related glyphs (just for debug) */
final List<Glyph> glyphs = new ArrayList<>();
//~ Constructors -------------------------------------------------------
public GlyphRect (Glyph glyph)
{
super(glyph.getBounds());
glyphs.add(glyph);
}
//~ Methods ------------------------------------------------------------
public void add (Glyph glyph)
{
add(glyph.getBounds());
glyphs.add(glyph);
}
public void add (GlyphRect that)
{
super.add(that);
glyphs.addAll(that.glyphs);
}
@Override
public String toString ()
{
return Glyphs.toString("B", glyphs);
}
}
//-------//
// Limit //
//-------//
/**
* Handles the limit of a system as a continuous sequence of
* rectangles made of intersected glyphs and staff line segments.
*/
private static class Limit
{
//~ Instance fields ----------------------------------------------------
/** Horizontal sequence of boxes */
List<Rectangle> boxes = new ArrayList<>();
//~ Methods ------------------------------------------------------------
public Rectangle getBounds ()
{
Rectangle bounds = null;
for (Rectangle rect : boxes) {
if (bounds == null) {
bounds = new Rectangle(rect);
} else {
bounds.add(rect);
}
}
return bounds;
}
public int getY (int x,
int dir)
{
Integer bestY = null;
for (Rectangle r : boxes) {
if ((x >= r.x) && (x <= (r.x + r.width))) {
int y = (dir < 0) ? (r.y + r.height) : r.y;
if (bestY == null) {
bestY = y;
} else {
if (((y - bestY) * dir) < 0) {
bestY = y;
}
}
}
}
return bestY;
}
@Override
public String toString ()
{
StringBuilder sb = new StringBuilder("{Limit [");
for (Rectangle rect : boxes) {
if (rect instanceof LineRect) {
sb.append("-").append(rect.height).append("-");
} else {
sb.append(rect);
}
}
sb.append("] bounds:").append(getBounds());
sb.append("}");
return sb.toString();
}
private void add (Rectangle blob)
{
boxes.add(blob);
}
private void grow (int h,
int v)
{
for (Rectangle rect : boxes) {
rect.grow(h, v);
}
}
private boolean intersects (Rectangle rectangle)
{
Rectangle prevRect = null;
for (Rectangle rect : boxes) {
if (rect.intersects(rectangle)) {
return true;
}
prevRect = rect;
}
return false;
}
private boolean intersects (Point p1,
Point p2)
{
for (Rectangle rect : boxes) {
if (rect.intersectsLine(p1.x, p1.y, p2.x, p2.y)) {
logger.debug("{} intersects from {} to {}",
rect, p1, p2);
return true;
}
}
return false;
}
}
//----------//
// LineRect //
//----------//
/**
* A Rectangle built from a segment of rather horizontal staff line,
* and ensured to be non-empty to allow intersection computation.
*/
private static class LineRect
extends Rectangle
{
//~ Constructors -------------------------------------------------------
public LineRect (int x1,
int y1,
int x2,
int y2,
int dir)
{
super(x1, Math.min(y1, y2), x2 - x1, Math.abs(y2 - y1));
// Make sure this (line-based) rectangle is non empty
if (y2 == y1) {
if (dir < 0) {
y--;
}
height++;
}
}
}
}