//----------------------------------------------------------------------------//
// //
// A d a p t i v e F i l t 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.run;
import omr.constant.Constant;
import omr.constant.ConstantSet;
import omr.math.Population;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
/**
* Class {@code AdaptiveFilter} is an abstract implementation of
* {@code PixelFilter} which provides foreground information based on
* mean value and standard deviation in pixel neighborhood.
*
* <p>See work of Sauvola et al.<a
* href="http://www.mediateam.oulu.fi/publications/pdf/24.p">
* here</a>.
*
* <p>The mean value and the standard deviation value are provided thanks to
* underlying integrals {@link Tile} instances.
* The precise tile size and behavior is the responsibility of subclasses of
* this class.
*
* <p> See work of Shafait et al. <a
* href="http://www.dfki.uni-kl.de/~shafait/papers/Shafait-efficient-binarization-SPIE08.pdf">
* here</a>.
*
* <pre>
* 0---------------------------------------------+---------------+
* | | |
* | | |
* | | |
* | a| b|
* +---------------------------------------------+---------------+
* | | |
* | | |
* | | |
* | | |
* | | |
* | c| d|
* +---------------------------------------------+---------------+
* </pre>
* Key table features:
* <ul>
* <li>Assumption: The integral of any rectangle with origin at (0,0) is stored
* in the bottom right cell of the rectangle.</li>
*
* <li>As a consequence the integral of any rectangle, whatever its origin,
* can be simply computed as:
* <code>a + d - b - c</code>
* </li>
*
* <li>In particular if lower right rectangle is reduced to a single cell, then
* <code>d = pixel_value + top + left - topLeft</code><br/>
* This property is used to incrementally populate the table.</li>
* </ul>
*
* @author ryo/twitter @xiaot_Tag
* @author Hervé Bitteur
*/
public class AdaptiveFilter
extends SourceWrapper
implements PixelFilter
{
//~ Static fields/initializers ---------------------------------------------
/** Specific application parameters */
private static final Constants constants = new Constants();
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(
AdaptiveFilter.class);
//~ Instance fields --------------------------------------------------------
//
/** Default value for (half of) window size. */
protected final int HALF_WINDOW_SIZE = constants.halfWindowSize.getValue();
/** Coefficient of mean value. */
protected final double MEAN_COEFF;
/** Coefficient of standard deviation. */
protected final double STD_DEV_COEFF;
/** Table for integrals of plain values. */
protected Tile tile;
/** Table for integrals of squared values. */
protected Tile sqrTile;
//~ Constructors -----------------------------------------------------------
//
//----------------//
// AdaptiveFilter //
//----------------//
/**
* Create an adaptive wrapper on a pixel source.
*
* @param source the underlying source of raw pixels
*/
public AdaptiveFilter (PixelSource source,
double meanCoeff,
double stdDevCoeff)
{
super(source);
this.MEAN_COEFF = meanCoeff;
this.STD_DEV_COEFF = stdDevCoeff;
}
//~ Methods ----------------------------------------------------------------
//------------//
// getContext //
//------------//
@Override
public Context getContext (int x,
int y)
{
final int imageWidth = source.getWidth();
final int imageHeight = source.getHeight();
int xMin = Math.max(0, x - HALF_WINDOW_SIZE);
int xMax = Math.min(imageWidth - 1, x + HALF_WINDOW_SIZE);
int yMin = Math.max(0, y - HALF_WINDOW_SIZE);
int yMax = Math.min(imageHeight - 1, y + HALF_WINDOW_SIZE);
// Brute force retrieval
Population pop = new Population();
for (int ix = xMin; ix <= xMax; ix++) {
for (int iy = yMin; iy <= yMax; iy++) {
pop.includeValue(source.getPixel(ix, iy));
}
}
if (pop.getCardinality() > 0) {
double mean = pop.getMeanValue();
double stdDev = pop.getStandardDeviation();
double threshold = getThreshold(mean, stdDev);
return new AdaptiveContext(mean, stdDev, threshold);
} else {
return null;
}
}
//---------------------//
// getDefaultMeanCoeff //
//---------------------//
public static double getDefaultMeanCoeff ()
{
return constants.meanCoeff.getValue();
}
//-----------------------//
// getDefaultStdDevCoeff //
//-----------------------//
public static double getDefaultStdDevCoeff ()
{
return constants.stdDevCoeff.getValue();
}
//
// -------//
// isFore //
// -------//
@Override
public boolean isFore (int x,
int y)
{
double mean = tile.getMean(x, y);
double sqrMean = sqrTile.getMean(x, y);
double var = Math.abs(sqrMean - (mean * mean));
double stdDev = Math.sqrt(var);
double threshold = getThreshold(mean, stdDev);
int pixValue = source.getPixel(x, y);
boolean isFore = pixValue <= threshold;
return isFore;
}
//---------------------//
// setDefaultMeanCoeff //
//---------------------//
public static void setDefaultMeanCoeff (double meanCoeff)
{
constants.meanCoeff.setValue(meanCoeff);
}
//-----------------------//
// setDefaultStdDevCoeff //
//-----------------------//
public static void setDefaultStdDevCoeff (double stdDevCoeff)
{
constants.stdDevCoeff.setValue(stdDevCoeff);
}
//------------------//
// getAdaptiveClass //
//------------------//
static Class<?> getImplementationClass ()
{
String name = constants.className.getValue();
try {
return Class.forName(name);
} catch (ClassNotFoundException ex) {
logger.error("Cannot find adaptive filter class " + name);
return null;
}
}
//--------------//
// getThreshold //
//--------------//
private double getThreshold (double mean,
double stdDev)
{
// This is the key formula
return (MEAN_COEFF * mean) + (STD_DEV_COEFF * stdDev);
}
//~ Inner Classes ----------------------------------------------------------
//-----------------//
// AdaptiveContext //
//-----------------//
public static class AdaptiveContext
extends Context
{
//~ Instance fields ----------------------------------------------------
/** Mean pixel value in the neighborhood. */
public final double mean;
/** Standard deviation of pixel values in the neighborhood. */
public final double standardDeviation;
//~ Constructors -------------------------------------------------------
public AdaptiveContext (double mean,
double standardDeviation,
double threshold)
{
super(threshold);
this.mean = mean;
this.standardDeviation = standardDeviation;
}
}
//
//------//
// Tile //
//------//
/**
* Handles a vertical tile of integrals.
*/
protected class Tile
{
//~ Instance fields ----------------------------------------------------
/** Width of the tile circular buffer. */
protected final int TILE_WIDTH;
/** Remember if we handle squared values or plain values. */
protected final boolean squared;
/** Height of the tile = height of the image. */
protected final int height;
/** Abscissa corresponding to the right side of the tile. */
protected int xRight = -1;
/** Circular buffer for integrals. */
protected final long[][] sums;
//~ Constructors -------------------------------------------------------
/**
* Create a tile instance.
*
* @param tileWidth tile width
* @param height tile height = image height
* @param squared true for squared values, false for plain values
*/
public Tile (int tileWidth,
int height,
boolean squared)
{
this.TILE_WIDTH = tileWidth;
this.height = height;
this.squared = squared;
// Allocate buffer of integrals
sums = new long[TILE_WIDTH][height];
// Initialize the "previous" column
Arrays.fill(sums[TILE_WIDTH - 1], 0);
}
//~ Methods ------------------------------------------------------------
/**
* Make sure that the sliding window is positioned around the
* provided location, and return mean data.
*
* @param x provided abscissa
* @param y provided ordinate
* @return the average value around the provided location
*/
public double getMean (int x,
int y)
{
// Compute actual borders of the window
final int imageWidth = getWidth();
int x1 = Math.max(-1, x - HALF_WINDOW_SIZE - 1);
int x2 = Math.min(imageWidth - 1, x + HALF_WINDOW_SIZE);
int y1 = Math.max(-1, y - HALF_WINDOW_SIZE - 1);
int y2 = Math.min(height - 1, y + HALF_WINDOW_SIZE);
// Make sure the tile is positioned correctly
shiftTile(x2);
// Upper left
long a = ((x1 >= 0) && (y1 >= 0)) ? sums[x1 % TILE_WIDTH][y1] : 0;
// Above
long b = (y1 >= 0) ? sums[x2 % TILE_WIDTH][y1] : 0;
// Left
long c = (x1 >= 0) ? sums[x1 % TILE_WIDTH][y2] : 0;
// Lower right
long d = sums[x2 % TILE_WIDTH][y2];
// Integral for window rectangle
double sum = (a + d) - b - c;
// Area = number of values
int area = (y2 - y1) * (x2 - x1);
// Return mean value
return sum / area;
}
/**
* Populate the provided column with proper integrals, building
* on the content of previous column.
*
* @param x the column to populate
*/
protected void populateColumn (int x)
{
// Translate the absolute column to circular buffer column
final int tx = x % TILE_WIDTH;
final long[] column = sums[tx];
// The column to the left (modulo tile width)
final int prevTx = ((x + TILE_WIDTH) - 1) % TILE_WIDTH;
final long[] prevColumn = sums[prevTx];
long top = 0;
long topLeft = 0;
for (int y = 0; y < height; y++) {
long left = prevColumn[y];
long pix = getPixel(x, y);
if (squared) {
pix *= pix;
}
long val = (pix + left + top) - topLeft;
column[y] = val;
// For next iteration
top = val;
topLeft = left;
}
}
/**
* Make sure the column at abscissa 'x2' lies within the tile.
*
* @param x2 the abscissa to check
*/
protected void shiftTile (int x2)
{
// Void by default
}
}
//-----------//
// Constants //
//-----------//
private static final class Constants
extends ConstantSet
{
//~ Instance fields ----------------------------------------------------
Constant.Integer halfWindowSize = new Constant.Integer(
"Pixels",
18,
"Half size of window around a given pixel");
Constant.Ratio meanCoeff = new Constant.Ratio(
0.7,
"Threshold formula coefficient for mean pixel value");
Constant.Ratio stdDevCoeff = new Constant.Ratio(
0.9,
"Threshold formula coefficient for pixel standard deviation");
Constant.String className = new Constant.String(
"omr.run.VerticalFilter",
"omr.run.VerticalFilter or omr.run.RandomFilter");
}
}