//----------------------------------------------------------------------------//
// //
// F i l a m e n t A l i g n m e n t //
// //
//----------------------------------------------------------------------------//
// <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.constant.Constant;
import omr.constant.ConstantSet;
import omr.glyph.facets.BasicAlignment;
import omr.glyph.facets.Glyph;
import omr.glyph.facets.GlyphComposition.Linking;
import omr.lag.Section;
import omr.math.LineUtil;
import omr.math.NaturalSpline;
import omr.math.Population;
import omr.run.Orientation;
import omr.sheet.Scale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Class {@code FilamentAlignment} is a GlyphAlignment meant for a
* Filament instance, where the underlying Line is actually not a
* straight line, but a NaturalSpline.
*
* @author Hervé Bitteur
*/
public class FilamentAlignment
extends BasicAlignment
{
//~ Static fields/initializers ---------------------------------------------
/** Specific application parameters */
private static final Constants constants = new Constants();
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(
FilamentAlignment.class);
//~ Instance fields --------------------------------------------------------
/** Absolute defining points */
protected List<Point2D> points;
/** Mean distance from a straight line */
protected Double meanDistance;
//~ Constructors -----------------------------------------------------------
//-------------------//
// FilamentAlignment //
//-------------------//
/**
* Creates a new FilamentAlignment object.
*/
public FilamentAlignment (Glyph glyph)
{
super(glyph);
}
//~ Methods ----------------------------------------------------------------
//--------//
// dumpOf //
//--------//
@Override
public String dumpOf ()
{
StringBuilder sb = new StringBuilder();
sb.append(super.dumpOf());
sb.append(String.format(" meanRadius:%.3f%n", getMeanCurvature()));
return sb.toString();
}
//---------//
// getLine //
//---------//
@Override
public NaturalSpline getLine ()
{
return (NaturalSpline) super.getLine();
}
//------------------//
// getMeanCurvature //
//------------------//
/**
* Report the average radius of curvature along all segments of
* the curve.
* This is not a global radius, but rather a way to mesure how straight
* the curve is.
*
* @return the average of radius measurements along all curve segments
*/
public double getMeanCurvature ()
{
Point2D prevPoint = null;
Line2D prevBisector = null;
Line2D bisector = null;
Population curvatures = new Population();
for (Point2D point : points) {
if (prevPoint != null) {
bisector = LineUtil.bisector(
new Line2D.Double(prevPoint, point));
}
if (prevBisector != null) {
Point2D inter = LineUtil.intersection(
prevBisector.getP1(),
prevBisector.getP2(),
bisector.getP1(),
bisector.getP2());
double radius = Math.hypot(
inter.getX() - point.getX(),
inter.getY() - point.getY());
curvatures.includeValue(1 / radius);
}
prevBisector = bisector;
prevPoint = point;
}
if (curvatures.getCardinality() > 0) {
return 1 / curvatures.getMeanValue();
} else {
return 0;
}
}
//-----------------//
// getMeanDistance //
//-----------------//
@Override
public double getMeanDistance ()
{
if (line == null) {
computeLine();
}
if (meanDistance == null) {
Line2D straight = new Line2D.Double(startPoint, stopPoint);
double totalDistSq = 0;
int pointCount = points.size() - 2; // Only intermediate points!
for (int i = 1, iMax = pointCount; i <= iMax; i++) {
totalDistSq += straight.ptLineDistSq(points.get(i));
}
if (pointCount > 0) {
meanDistance = Math.sqrt(totalDistSq / pointCount);
}
}
return (meanDistance != null) ? meanDistance : 0;
}
//---------------//
// getPositionAt //
//---------------//
@Override
public double getPositionAt (double coord,
Orientation orientation)
{
if (line == null) {
computeLine();
}
if (orientation == Orientation.HORIZONTAL) {
if ((coord < startPoint.getX()) || (coord > stopPoint.getX())) {
double sl = (stopPoint.getY() - startPoint.getY()) / (stopPoint.
getX()
- startPoint.
getX());
return startPoint.getY() + (sl * (coord - startPoint.getX()));
} else {
return line.yAtX(coord);
}
} else {
if ((coord < startPoint.getY()) || (coord > stopPoint.getY())) {
double sl = (stopPoint.getX() - startPoint.getX()) / (stopPoint.
getY()
- startPoint.
getY());
return startPoint.getX() + (sl * (coord - startPoint.getY()));
} else {
return line.xAtY(coord);
}
}
}
//-----------------//
// invalidateCache //
//-----------------//
@Override
public void invalidateCache ()
{
super.invalidateCache();
points = null;
meanDistance = null;
}
//-----------------//
// polishCurvature //
//-----------------//
/**
* Polish the filament by looking at local curvatures and removing
* sections when necessary.
* <p>If the local point is next to the first or last point of the curve,
* then the point to modify is likely to be this first or last point.
* In the other cases, the local point itself is modified.
*/
public void polishCurvature ()
{
boolean modified = false;
do {
modified = false;
final List<Line2D> bisectors = getBisectors();
// Compute radius values (using same index as points)
final List<Double> radii = new ArrayList<>();
radii.add(null); // To skip index 0 for which we have no value (???)
for (int i = 1, iBreak = points.size() - 1; i < iBreak; i++) {
radii.add(getRadius(i, bisectors));
}
// Check smallest radius
Integer idx = null;
double minRadius = Integer.MAX_VALUE;
for (int i = 1, iBreak = points.size() - 1; i < iBreak; i++) {
double radius = radii.get(i);
if (minRadius > radius) {
minRadius = radius;
idx = i;
}
}
double rad = minRadius / glyph.getInterline();
if (rad < constants.minRadius.getValue()) {
if (logger.isDebugEnabled() || glyph.isVip()) {
logger.info("Polishing F#{} minRad: {} seq:{} {}",
glyph.getId(), (float) rad, idx, points.get(idx));
}
// Adjust the removable point for first & last points
if (idx == 1) {
idx--;
} else if (idx == (points.size() - 2)) {
idx++;
}
// Lookup corresponding section(s)
Scale scale = new Scale(glyph.getInterline());
int probeWidth = scale.toPixels(
BasicAlignment.getProbeWidth());
Orientation orientation = getRoughOrientation();
final Point2D point = points.get(idx);
Point2D orientedPt = orientation.oriented(
points.get(idx));
Rectangle2D rect = new Rectangle2D.Double(
orientedPt.getX() - (probeWidth / 2),
orientedPt.getY() - (probeWidth / 2),
probeWidth,
probeWidth);
List<Section> found = new ArrayList<>();
for (Section section : glyph.getMembers()) {
if (rect.intersects(section.getOrientedBounds())) {
found.add(section);
}
}
if (found.size() > 1) {
// Pick up the section closest to the point
Collections.sort(
found,
new Comparator<Section>()
{
@Override
public int compare (Section s1,
Section s2)
{
return Double.compare(
point.distance(s1.getCentroid()),
point.distance(s2.getCentroid()));
}
});
}
Section section = found.isEmpty() ? null : found.get(0);
if (section != null) {
logger.debug("Removed section#{} from {} F{}",
section.getId(), orientation, glyph.getId());
glyph.removeSection(section, Linking.LINK_BACK);
modified = true;
}
}
} while (modified);
}
//------------//
// renderLine //
//------------//
@Override
public void renderLine (Graphics2D g)
{
if (!glyph.getBounds().intersects(g.getClipBounds())) {
return;
}
// The curved line itself
if (line != null) {
g.draw((NaturalSpline) line);
}
// Then the absolute defining points?
if (constants.showFilamentPoints.isSet() && (points != null)) {
// Point radius
double r = glyph.getInterline() * constants.filamentPointSize.
getValue();
Ellipse2D ellipse = new Ellipse2D.Double();
for (Point2D p : points) {
ellipse.setFrame(p.getX() - r, p.getY() - r, 2 * r, 2 * r);
g.fill(ellipse);
}
}
}
//---------//
// slopeAt //
//---------//
public double slopeAt (double coord,
Orientation orientation)
{
if (line == null) {
computeLine();
}
if (orientation == Orientation.HORIZONTAL) {
return getLine().yDerivativeAtX(coord);
} else {
return getLine().xDerivativeAtY(coord);
}
}
//-------------//
// computeLine //
//-------------//
/**
* Compute cached data: curve, startPoint, stopPoint, slope.
* Curve goes from startPoint to stopPoint through intermediate points
* regularly spaced
*/
@Override
protected void computeLine ()
{
try {
Scale scale = new Scale(glyph.getInterline());
/** Width of window to retrieve pixels */
int probeWidth = scale.toPixels(BasicAlignment.getProbeWidth());
/** Typical length of curve segments */
double typicalLength = scale.toPixels(constants.segmentLength);
// We need a rough orientation right now
Orientation orientation = getRoughOrientation();
Point2D orientedStart = (startPoint == null) ? null
: orientation.oriented(startPoint);
Point2D orientedStop = (stopPoint == null) ? null
: orientation.oriented(stopPoint);
Rectangle oBounds = orientation.oriented(glyph.getBounds());
double oStart = (orientedStart != null) ? orientedStart.getX()
: oBounds.x;
double oStop = (orientedStop != null) ? orientedStop.getX()
: (oBounds.x + (oBounds.width - 1));
double length = oStop - oStart + 1;
Rectangle oProbe = new Rectangle(oBounds);
oProbe.x = (int) Math.ceil(oStart);
oProbe.width = probeWidth;
// Determine the number of segments and their precise length
int segCount = (int) Math.rint(length / typicalLength);
double segLength = length / segCount;
List<Point2D> newPoints = new ArrayList<>(segCount + 1);
// First point
if (startPoint == null) {
Point2D p = orientation.oriented(
getRectangleCentroid(orientation.absolute(oProbe)));
startPoint = orientation.absolute(
new Point2D.Double(oStart, p.getY()));
}
newPoints.add(startPoint);
// Intermediate points (perhaps none)
for (int i = 1; i < segCount; i++) {
oProbe.x = (int) Math.rint(oStart + (i * segLength));
Point2D pt = getRectangleCentroid(orientation.absolute(oProbe));
// If, unfortunately, we are in a filament hole, just skip it
if (pt != null) {
newPoints.add(pt);
}
}
// Last point
if (stopPoint == null) {
oProbe.x = (int) Math.floor(oStop - oProbe.width + 1);
Point2D p = orientation.oriented(
getRectangleCentroid(orientation.absolute(oProbe)));
stopPoint = orientation.absolute(
new Point2D.Double(oStop, p.getY()));
}
newPoints.add(stopPoint);
// Interpolate the best spline through the provided points
line = NaturalSpline.interpolate(
newPoints.toArray(new Point2D[newPoints.size()]));
// Remember points (atomically)
this.points = newPoints;
// Cache global slope
getSlope();
} catch (Exception ex) {
logger.warn("Filament cannot computeData", ex);
}
}
//-----------//
// findPoint //
//-----------//
protected Point2D findPoint (int coord,
Orientation orientation,
int margin)
{
Point2D best = null;
double bestDeltacoord = Integer.MAX_VALUE;
for (Point2D p : points) {
double dc = Math.abs(
coord
- ((orientation == Orientation.HORIZONTAL) ? p.getX() : p.
getY()));
if ((dc <= margin) && (dc < bestDeltacoord)) {
bestDeltacoord = dc;
best = p;
}
}
return best;
}
//--------------//
// getBisectors //
//--------------//
/**
* Report bisectors of inter-points segments.
*
* @return sequence of bisectors, such that bisectors[i] is bisector of
* segment (i -> i+1)
*/
private List<Line2D> getBisectors ()
{
if (line == null) {
computeLine();
}
List<Line2D> bisectors = new ArrayList<>();
for (int i = 0; i < (points.size() - 1); i++) {
bisectors.add(
LineUtil.bisector(
new Line2D.Double(points.get(i), points.get(i + 1))));
}
return bisectors;
}
//-----------//
// getRadius //
//-----------//
/**
* Report radius computed at point with index 'i'.
* <p>TODO: This a simplistic way for computing radius, based on insection
* of the two adjacent bisectors.
* There may be other ways, such as using the following property:
* sin angle a / length of segment a = 1 / (2 * radius)
*
* @param i the index of desired point
* @param bisectors the sequence of bisectors
* @return the value of radius of curvature
*/
private double getRadius (int i,
List<Line2D> bisectors)
{
Line2D prevBisector = bisectors.get(i - 1);
Point2D point = points.get(i);
Line2D nextBisector = bisectors.get(i);
Point2D inter = LineUtil.intersection(
prevBisector.getP1(),
prevBisector.getP2(),
nextBisector.getP1(),
nextBisector.getP2());
return Math.hypot(
inter.getX() - point.getX(),
inter.getY() - point.getY());
}
//---------------------//
// getRoughOrientation //
//---------------------//
private Orientation getRoughOrientation ()
{
Rectangle box = glyph.getBounds();
return (box.height > box.width) ? Orientation.VERTICAL
: Orientation.HORIZONTAL;
}
//~ Inner Classes ----------------------------------------------------------
//-----------//
// Constants //
//-----------//
private static final class Constants
extends ConstantSet
{
//~ Instance fields ----------------------------------------------------
final Scale.Fraction segmentLength = new Scale.Fraction(
2,
"Typical length between filament curve intermediate points");
Constant.Boolean showFilamentPoints = new Constant.Boolean(
false,
"Should we display filament points?");
Scale.Fraction filamentPointSize = new Scale.Fraction(
0.05,
"Size of displayed filament points");
Scale.Fraction minRadius = new Scale.Fraction(
12,
"Minimum acceptable radius of curvature");
}
}