//----------------------------------------------------------------------------//
// //
// G l y p h R e g r e s s i o n //
// //
//----------------------------------------------------------------------------//
// <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.glyph;
import omr.Main;
import omr.constant.Constant;
import omr.constant.ConstantSet;
import omr.glyph.facets.Glyph;
import omr.math.LinearEvaluator;
import omr.math.LinearEvaluator.Sample;
import org.jdesktop.application.Application.ExitListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.EventObject;
import java.util.List;
import javax.xml.bind.JAXBException;
/**
* Class {@code GlyphRegression} is a glyph evaluator that encapsulates
* a {@link LinearEvaluator} working on glyph parameters.
*
* @author Hervé Bitteur
*/
public class GlyphRegression
extends AbstractEvaluationEngine
{
//~ Static fields/initializers ---------------------------------------------
/** Specific application parameters */
private static final Constants constants = new Constants();
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(
GlyphRegression.class);
/** LinearEvaluator backup file name */
private static final String BACKUP_FILE_NAME = "linear-evaluator.xml";
/** The singleton */
private static volatile GlyphRegression INSTANCE;
//~ Instance fields --------------------------------------------------------
/** The encapsulated linear evaluator */
private LinearEvaluator engine;
/** The constraints (minimum, maximum) per shape & per parameter */
private EnumMap<Shape, Range[]> constraintMap = new EnumMap<>(
Shape.class);
//~ Constructors -----------------------------------------------------------
//-----------------//
// GlyphRegression //
//-----------------//
/**
* Private constructor
*/
private GlyphRegression ()
{
// Unmarshal from backup data
engine = (LinearEvaluator) unmarshal();
if (engine == null) {
// Get a brand new one (not trained)
logger.info("Creating a brand new {}", getName());
engine = new LinearEvaluator(ShapeDescription.getParameterLabels());
} else {
defineConstraints();
// debug
// for (Shape shape : ShapeSet.allPhysicalShapes) {
// dumpOneShapeConstraints(shape);
// }
}
// Listen to application exit
if (Main.getGui() != null) {
Main.getGui().addExitListener(
new ExitListener()
{
@Override
public boolean canExit (EventObject eo)
{
return true;
}
@Override
public void willExit (EventObject eo)
{
if (engine.isDataModified()) {
marshal();
}
}
});
}
}
//~ Methods ----------------------------------------------------------------
// //--------------------//
// // constraintsMatched //
// //--------------------//
// /**
// * Check that all the (non-disabled) constraints matched between a
// * given glyph and a shape
// * @param params the glyph features
// * @param eval the evaluation context to update
// * @return true if matched, false otherwise
// */
// public boolean constraintsMatched (double[] params,
// Evaluation eval)
// {
// String failed = firstMisMatched(params, eval.shape);
//
// if (failed != null) {
// eval.failure = new Evaluation.Failure(failed);
//
// return false;
// } else {
// return true;
// }
// }
//--------------//
// isCompatible //
//--------------//
@Override
protected final boolean isCompatible (Object obj)
{
if (obj instanceof LinearEvaluator) {
LinearEvaluator anEngine = (LinearEvaluator) obj;
// Check parameters names, they must be identical
if (!Arrays.equals(
anEngine.getParameterNames(),
ShapeDescription.getParameterLabels())) {
if (logger.isDebugEnabled()) {
logger.debug("Engine parameters: {}",
Arrays.toString(anEngine.getParameterNames()));
logger.debug("Shape parameters: {}",
Arrays.toString(ShapeDescription.getParameterLabels()));
}
return false;
}
// Check categories names. Order is not relevant
// Engine categories must be a subset of physical shapes
String[] categories = anEngine.getCategoryNames();
String[] shapes = ShapeSet.getPhysicalShapeNames();
String[] sortedShapes = Arrays.copyOf(shapes, shapes.length);
List<String> extraNames = new ArrayList<>(Arrays.asList(categories));
extraNames.removeAll(Arrays.asList(sortedShapes));
if (!extraNames.isEmpty()) {
if (logger.isDebugEnabled()) {
Arrays.sort(categories);
logger.debug("Engine categories: {}",
Arrays.toString(categories));
Arrays.sort(sortedShapes);
logger.debug("Physical shapes: {}",
Arrays.toString(sortedShapes));
logger.debug("Extra names found in {}: {}",
getName(), extraNames);
}
return false;
} else {
return true;
}
} else {
return false;
}
}
//------//
// dump //
//------//
@Override
public void dump ()
{
engine.dump();
}
//--------------//
// dumpDistance //
//--------------//
/**
* Print out the "distance" information between a given glyph and a
* shape. It's a sort of debug information.
*
* @param glyph the glyph at hand
* @param shape the shape to measure distance from
*/
public void dumpDistance (Glyph glyph,
Shape shape)
{
// shapeDescs[shape.ordinal()].dumpDistance(glyph);
}
//-------------------------//
// dumpOneShapeConstraints //
//-------------------------//
public void dumpOneShapeConstraints (Shape shape)
{
StringBuilder sb = new StringBuilder();
sb.append(String.format("Constraints for %s: ", shape));
String[] labels = ShapeDescription.getParameterLabels();
Range[] ranges = constraintMap.get(shape);
if (ranges == null) {
sb.append(String.format("none%n"));
} else {
sb.append(String.format("%n"));
for (int p = 0; p < ShapeDescription.length(); p++) {
StringBuilder sbp = new StringBuilder();
Range range = ranges[p];
if (range != null) {
Double min = range.min;
if (min != null) {
sbp.append(" min=").append(min);
}
Double max = range.max;
if (max != null) {
sbp.append(" max=").append(max);
}
}
if (sbp.length() > 0) {
sb.append(String.format(" %s:%s%n", labels[p], sbp));
}
}
}
logger.info(sb.toString());
}
//-----------//
// getEngine //
//-----------//
/**
* @return the engine
*/
public LinearEvaluator getEngine ()
{
return engine;
}
//-------------//
// getInstance //
//-------------//
/**
* Provide access to the single instance of GlyphRegression for the
* application
*
* @return the GlyphRegression instance
*/
public static GlyphRegression getInstance ()
{
if (INSTANCE == null) {
synchronized (GlyphRegression.class) {
if (INSTANCE == null) {
INSTANCE = new GlyphRegression();
}
}
}
return INSTANCE;
}
//------------//
// getMaximum //
//------------//
/**
* Get the constraint test on maximum for a parameter of the
* provided shape.
*
* @param paramIndex the impacted parameter
* @param shape the targeted shape
* @return the current maximum value (null if test is disabled)
*/
public Double getMaximum (int paramIndex,
Shape shape)
{
Range[] ranges = constraintMap.get(shape);
if (ranges == null) {
return null;
}
Range range = ranges[paramIndex];
return (range != null) ? range.max : null;
}
//------------//
// getMinimum //
//------------//
/**
* Get the constraint test on minimum for a parameter of the
* provided shape.
*
* @param paramIndex the impacted parameter
* @param shape the targeted shape
* @return the current minimum value (null if test is disabled)
*/
public Double getMinimum (int paramIndex,
Shape shape)
{
Range[] ranges = constraintMap.get(shape);
if (ranges == null) {
return null;
}
Range range = ranges[paramIndex];
return (range != null) ? range.min : null;
}
//---------//
// getName //
//---------//
@Override
public final String getName ()
{
return "Linear Evaluator";
}
//---------------//
// includeSample //
//---------------//
/**
* Take into account the observed parameters for the provided shape,
* and relax the related constraints if needed.
*
* @param params the observed input parameters
* @param shape the provided shape
* @return true if constraints have been extended
*/
public boolean includeSample (double[] params,
Shape shape)
{
// Include this observation
boolean extended = engine.includeSample(params, shape.toString());
if (extended) {
// Update extended constraints for the shape
defineOneShapeConstraints(shape);
}
return extended;
}
//-----------------//
// measureDistance //
//-----------------//
/**
* Measure the "distance" information between a given glyph and a
* shape.
*
* @param glyph the glyph at hand
* @param shape the shape to measure distance from
* @return the measured distance
*/
public double measureDistance (Glyph glyph,
Shape shape)
{
return engine.categoryDistance(
ShapeDescription.features(glyph),
shape.toString());
}
//-----------------//
// measureDistance //
//-----------------//
/**
* Measure the "distance" information between a given glyph and a
* shape.
*
* @param ins the input parameters
* @param shape the shape to measure distance from
* @return the measured distance
*/
public double measureDistance (double[] ins,
Shape shape)
{
return engine.categoryDistance(ins, shape.toString());
}
//-----------------//
// measureDistance //
//-----------------//
/**
* Measure the "distance" information between two glyphs.
*
* @param one the first glyph
* @param two the second glyph
* @return the measured distance
*/
public double measureDistance (Glyph one,
Glyph two)
{
return measureDistance(one, ShapeDescription.features(two));
}
//-----------------//
// measureDistance //
//-----------------//
/**
* Measure the "distance" information between a glyph and an array
* of parameters (generally fed from another glyph).
*
* @param glyph the given glyph
* @param ins the array (size = paramCount) of parameters
* @return the measured distance
*/
public double measureDistance (Glyph glyph,
double[] ins)
{
return engine.patternDistance(ShapeDescription.features(glyph), ins);
}
//------------//
// setMaximum //
//------------//
/**
* Set the constraint test on maximum for a parameter of the
* provided shape.
*
* @param paramIndex the impacted parameter
* @param shape the targeted shape
* @param val the new maximum value (null for disabling the test)
*/
public void setMaximum (int paramIndex,
Shape shape,
Double val)
{
doGetRange(paramIndex, shape).max = val;
}
//------------//
// setMinimum //
//------------//
/**
* Set the constraint test on minimum for a parameter of the
* provided shape.
*
* @param paramIndex the impacted parameter
* @param shape the targeted shape
* @param val the new minimum value (null for disabling the test)
*/
public void setMinimum (int paramIndex,
Shape shape,
Double val)
{
doGetRange(paramIndex, shape).min = val;
}
//-------//
// train //
//-------//
/**
* Launch the training of the evaluator.
*
* @param base the collection of glyphs used for training
* @param monitor a monitoring entity
* @param mode incremental or scratch mode
*/
@Override
public void train (Collection<Glyph> base,
Monitor monitor,
StartingMode mode)
{
if (base.isEmpty()) {
logger.warn("No glyph to retrain Regression Evaluator");
return;
}
// Prepare the collection of samples
Collection<Sample> samples = new ArrayList<>();
for (Glyph glyph : base) {
try {
Shape shape = glyph.getShape().getPhysicalShape();
Sample sample = new Sample(
shape.toString(),
ShapeDescription.features(glyph));
samples.add(sample);
} catch (Exception ex) {
logger.warn(
"Weird glyph shape: " + glyph.getShape() + " file="
+ GlyphRepository.getInstance().getGlyphName(glyph),
ex);
}
}
// Do the training
engine.train(samples);
// Save to disk
marshal();
}
//-------------//
// getFileName //
//-------------//
@Override
protected String getFileName ()
{
return BACKUP_FILE_NAME;
}
//-------------------//
// getRawEvaluations //
//-------------------//
@Override
protected Evaluation[] getRawEvaluations (Glyph glyph)
{
// If too small, it's just NOISE
if (!isBigEnough(glyph)) {
return noiseEvaluations;
} else {
double[] ins = ShapeDescription.features(glyph);
Evaluation[] evals = new Evaluation[shapeCount];
Shape[] values = Shape.values();
for (int s = 0; s < shapeCount; s++) {
Shape shape = values[s];
evals[s] = new Evaluation(
shape,
1d / measureDistance(ins, shape));
}
// Order the evals from best to worst
Arrays.sort(evals);
return evals;
}
}
//---------//
// marshal //
//---------//
@Override
protected void marshal (OutputStream os)
throws FileNotFoundException, IOException, JAXBException
{
engine.marshal(os);
}
//-----------//
// unmarshal //
//-----------//
@Override
protected LinearEvaluator unmarshal (InputStream is)
throws JAXBException
{
return LinearEvaluator.unmarshal(is);
}
//-------------------//
// defineConstraints //
//-------------------//
/**
* Here we customize the linear evaluator to our specific needs,
* by removing some constraint checks and relaxing others.
*/
private void defineConstraints ()
{
for (Shape shape : ShapeSet.allPhysicalShapes) {
defineOneShapeConstraints(shape);
}
}
//---------------------------//
// defineOneShapeConstraints //
//---------------------------//
/**
* Here we customize the constraints to our specific needs, by
* removing some constraint checks and relaxing others.
*
* @param shape the shape at hand
*/
private void defineOneShapeConstraints (Shape shape)
{
// // First, use LinearEvaluator observed constraints
// for (int p = 0; p < ShapeDescription.length(); p++) {
// setMinimum(p, shape, engine.getMinimum(p, shape.name()));
// setMaximum(p, shape, engine.getMaximum(p, shape.name()));
// }
//
// // Second, relax some constraints
// // Add some margin around constraints
// double minFactor = constants.factorForMinima.getValue();
// double maxFactor = constants.factorForMaxima.getValue();
//
// for (String label : Arrays.asList("weight", "width", "height")) {
// int p = ShapeDescription.getParameterIndex(label);
// Double val = getMinimum(p, shape);
//
// if (val != null) {
// if (val > 0) {
// setMinimum(p, shape, val * minFactor);
// } else {
// setMinimum(p, shape, val * maxFactor);
// }
// }
//
// val = getMaximum(p, shape);
//
// if (val != null) {
// if (val > 0) {
// setMaximum(p, shape, val * maxFactor);
// } else {
// setMaximum(p, shape, val * minFactor);
// }
// }
// }
//
// // Disable some selected features
// for (String label : Arrays.asList(
// "ledger",
// "n11",
// "n20",
// "n02",
// "n30",
// "n21",
// "n12",
// "n03",
// "aspect")) {
// int p = ShapeDescription.getParameterIndex(label);
// setMinimum(p, shape, null);
// setMaximum(p, shape, null);
// }
//
// // Keep "stemNb" exactly as it is, with no margin
//
// // Third, remove some constraints
// switch (shape) {
// case TEXT :
// disableMaximum(TEXT, "weight");
// disableMaximum(TEXT, "width");
//
// break;
//
// case BRACE :
// disableMaximum(BRACE, "weight");
// disableMaximum(BRACE, "height");
//
// break;
//
// case BRACKET :
// disableMaximum(BRACKET, "weight");
// disableMaximum(BRACKET, "height");
//
// break;
//
// case BEAM :
// disableMaximum(BEAM, "weight");
// disableMaximum(BEAM, "width");
// disableMaximum(BEAM, "height");
//
// break;
//
// case BEAM_2 :
// disableMaximum(BEAM_2, "weight");
// disableMaximum(BEAM_2, "width");
// disableMaximum(BEAM_2, "height");
//
// break;
//
// case BEAM_3 :
// disableMaximum(BEAM_3, "weight");
// disableMaximum(BEAM_3, "width");
// disableMaximum(BEAM_3, "height");
//
// break;
//
// case SLUR :
// disableMaximum(SLUR, "weight");
// disableMaximum(SLUR, "width");
// disableMaximum(SLUR, "height");
//
// break;
//
// case ARPEGGIATO :
// disableMaximum(ARPEGGIATO, "weight");
// disableMaximum(ARPEGGIATO, "height");
//
// break;
//
// case CRESCENDO :
// disableMaximum(CRESCENDO, "weight");
// disableMaximum(CRESCENDO, "width");
// disableMaximum(CRESCENDO, "height");
//
// break;
//
// case DECRESCENDO :
// disableMaximum(DECRESCENDO, "weight");
// disableMaximum(DECRESCENDO, "width");
// disableMaximum(DECRESCENDO, "height");
//
// break;
//
// default :
// }
}
//----------------//
// disableMaximum //
//----------------//
private void disableMaximum (Shape shape,
String paramLabel)
{
setMaximum(ShapeDescription.getParameterIndex(paramLabel), shape, null);
}
//----------------//
// disableMinimum //
//----------------//
private void disableMinimum (Shape shape,
String paramLabel)
{
setMinimum(ShapeDescription.getParameterIndex(paramLabel), shape, null);
}
//------------//
// doGetRange //
//------------//
/**
* Retrieve (and create if necessary) the range entity that
* corresponds to parameter of paramIndex for the provided shape.
*
* @param paramIndex parameter reference
* @param shape provided shape
* @return the desired range entity
*/
private Range doGetRange (int paramIndex,
Shape shape)
{
Range[] ranges = constraintMap.get(shape);
if (ranges == null) {
ranges = new Range[ShapeDescription.length()];
constraintMap.put(shape, ranges);
}
Range range = ranges[paramIndex];
if (range == null) {
ranges[paramIndex] = range = new Range();
}
return range;
}
//-----------------//
// firstMisMatched //
//-----------------//
/**
* Perform a basic check on max / min bounds, if any, for each
* parameter value of the provided pattern.
*
* @param pattern the collection of parameters to check with respect to
* targeted shape
* @param shape the targeted shape
* @return the name of the first failing check, null otherwise
*/
private String firstMisMatched (double[] pattern,
Shape shape)
{
String[] labels = ShapeDescription.getParameterLabels();
Range[] ranges = constraintMap.get(shape);
if (ranges == null) {
return null; // No test => OK
}
for (int p = 0; p < ShapeDescription.length(); p++) {
String label = labels[p];
double val = pattern[p];
Range range = ranges[p];
if (range == null) {
continue;
}
Double min = range.min;
if ((min != null) && (val < min)) {
logger.debug("{} failed on minimum for {} {} < {}",
shape, label, val, min);
return label + ".min";
}
Double max = range.max;
if ((max != null) && (val > max)) {
logger.debug("{} failed on maximum for {} {} > {}",
shape, label, val, max);
return label + ".max";
}
}
// Everything is OK
return null;
}
//~ Inner Classes ----------------------------------------------------------
//-------//
// Range //
//-------//
public static class Range
{
//~ Instance fields ----------------------------------------------------
/** Constraint on minimum, if any */
Double min;
/** Constraint on maximum, if any */
Double max;
}
//-----------//
// Constants //
//-----------//
private static final class Constants
extends ConstantSet
{
//~ Instance fields ----------------------------------------------------
Constant.Double factorForMinima = new Constant.Double(
"factor",
0.7,
"Factor applied to all minimum constraints");
Constant.Double factorForMaxima = new Constant.Double(
"factor",
1.3,
"Factor applied to all maximum constraints");
}
}