examples.be.tarsos.dsp.example.dissonance.KernelDensityEstimate Maven / Gradle / Ivy
The newest version!
package be.tarsos.dsp.example.dissonance;
/*
* _______
* |__ __|
* | | __ _ _ __ ___ ___ ___
* | |/ _` | '__/ __|/ _ \/ __|
* | | (_| | | \__ \ (_) \__ \
* |_|\__,_|_| |___/\___/|___/
*
* -----------------------------------------------------------
*
* Tarsos is developed by Joren Six at
* The School of Arts,
* University College Ghent,
* Hoogpoort 64, 9000 Ghent - Belgium
*
* -----------------------------------------------------------
*
* Info: http://tarsos.0110.be
* Github: https://github.com/JorenSix/Tarsos
* Releases: http://tarsos.0110.be/releases/Tarsos/
*
* Tarsos includes some source code by various authors,
* for credits and info, see README.
*
*/
import java.util.Arrays;
public class KernelDensityEstimate {
protected final double[] accumulator;
protected final Kernel kernel;
private double sum;
public KernelDensityEstimate(final Kernel kernel, final int size) {
accumulator = new double[size];
sum = 0;
this.kernel = kernel;
if (kernel.size() > accumulator.length) {
throw new IllegalArgumentException("The kernel size should be smaller than the acummulator size.");
}
}
public KernelDensityEstimate(final Kernel kernel, double[] accumulator) {
this.accumulator = accumulator;
this.kernel = kernel;
if (kernel.size() > accumulator.length) {
throw new IllegalArgumentException("The kernel size should be smaller than the acummulator size.");
}
calculateSumFreq();
}
/**
* Add the kernel to an accumulator for each value.
*
* When a kernel with a width of 7 is added at 1 cents it has influence on
* the bins from 1200 - 7 * 10 + 1 to 1 + 7 * 10 so from 1131 to 71. To make
* the modulo calculation easy 1200 is added to each value: -69 % 1200 is
* -69, (-69 + 1200) % 1200 is the expected 1131. If you know what I mean.
* This algorithm computes O(width * n) sums with n the number of
* annotations and width the number of bins affected, rather efficient.
*
* @param value
* The value to add.
*/
public void add(double value) {
int accumulatorSize = accumulator.length;
int calculationAria = kernel.size() / 2;
int start = (int) (value + accumulatorSize - calculationAria);
int stop = (int) (value + accumulatorSize + calculationAria);
if (kernel.size() % 2 != 0)
stop++;
for (int i = start; i < stop; i++) {
double kernelValue = kernel.value(i - start);
accumulator[i % accumulatorSize] += kernelValue;
sum += kernelValue;
}
}
/**
* Remove a value from the kde, removes a kernel at the specified position.
* @param value The value to remove.
*/
public void remove(double value) {
int accumulatorSize = accumulator.length;
int calculationAria = kernel.size() / 2;
int start = (int) (value + accumulatorSize - calculationAria);
int stop = (int) (value + accumulatorSize + calculationAria);
if (kernel.size() % 2 != 0)
stop++;
for (int i = start; i < stop; i++) {
double kernelValue = kernel.value(i - start);
accumulator[i % accumulatorSize] -= kernelValue;
sum -= kernelValue;
}
}
/**
* Shift the accumulator x positions.
* @param shift The number of positions the accumulator should be shifted.
*/
public void shift(int shift){
double[] newValues = new double[size()];
for(int index = 0 ; index < size() ; index++){
newValues[index] = accumulator[(index + shift) % size()];
}
for(int index = 0 ; index < size() ; index++){
accumulator[index] = newValues[index];
}
}
/**
* Returns the current estimate.
*
* @return The current estimate. To prevent unauthorized modification a
* clone of the array is returned. Please cache appropriately.
*/
public double[] getEstimate() {
return accumulator.clone();
}
/**
* Map the kernel density estimate to another size. E.g. a KDE with 4 values
* mapped to two is done by iterating the 4 elements and adding them on
* modulo 2 places. Here 1 + 4 = 5, 2 + 9 = 11
*
*
* (1 2 4 9).map(2) = (5 11)
*
* @param size The new size for the KDE.
* @return A new KDE with the contents of the original mapped to the new size.
*/
public KernelDensityEstimate map(int size){
KernelDensityEstimate newKDE = new KernelDensityEstimate(kernel,size);
for(int index = 0 ; index < size() ; index++){
newKDE.accumulator[index % size] += accumulator[index];
}
newKDE.calculateSumFreq();
return newKDE;
}
/**
* Return the value for the accumulator at a certain index.
*
* @param index
* The index.
* @return The value for the accumulator at a certain index.
*/
public double getValue(final int index) {
return accumulator[index];
}
/**
* @return The size of the accumulator.
*/
public int size() {
return accumulator.length;
}
/**
* Returns the sum of all estimates in the accumulator.
*
* @return The total sum of all estimates.
*/
public double getSumFreq() {
return sum;
}
/**
* Calculates the sum of all estimates in the accummulator. Should be called after each update.
*/
private void calculateSumFreq(){
sum = 0;
for (int i = 0; i < accumulator.length; i++) {
sum += accumulator[i];
}
}
/**
* Sets the maximum value in accumulator to 1.0
*/
public void normalize() {
normalize(1.0);
}
/**
* Sets a new maximum bin value.
* @param newMaxvalue The new maximum bin value.
*/
public void normalize(double newMaxvalue){
double maxElement = getMaxElement();
double scaleFactor = newMaxvalue / getMaxElement();
if (maxElement > 0) {
for (int i = 0; i < size(); i++) {
accumulator[i] = accumulator[i] * scaleFactor;
}
}
calculateSumFreq();
}
/**
* @return the maximum element in the accumulator;
*/
public double getMaxElement() {
double maxElement = 0.0;
for (int i = 0; i < size(); i++) {
maxElement = Math.max(accumulator[i], maxElement);
}
return maxElement;
}
/**
* Sets the area under the curve to 1.0.
* In essence every value is divided by getSumFreq().
* As per definition of a probability density function.
*/
public void pdfify() {
double sumFreq = this.getSumFreq();
if(sumFreq != 0.0){
for (int i = 0; i < accumulator.length; i++) {
accumulator[i] = accumulator[i]/sumFreq;
}
}
//reset sum freq
calculateSumFreq();
assert getSumFreq() == 1.0;
}
/**
* Clears the data in the accumulator.
*/
public void clear(){
for (int i = 0; i < accumulator.length; i++) {
accumulator[i] = 0;
}
//reset sum freq
calculateSumFreq();
assert getSumFreq() == 0.0;
}
/**
* Takes the maximum of the value in the accumulator for two kde's.
* @param other The other kde of the same size.
*/
public void max(KernelDensityEstimate other){
assert other.size() == size() : "The kde size should be the same!";
for (int i = 0; i < accumulator.length; i++) {
accumulator[i] = Math.max(accumulator[i], other.accumulator[i]);
}
calculateSumFreq();
}
/**
* Adds a KDE to this accumulator
* @param other The other KDE of the same size.
*/
public void add(KernelDensityEstimate other){
assert other.size() == size() : "The kde size should be the same!";
for (int i = 0; i < accumulator.length; i++) {
accumulator[i] += other.accumulator[i];
}
calculateSumFreq();
}
/**
*
* Calculate a correlation with another KernelDensityEstimate. The index of
* the other estimates are shifted by a number which can be zero (or
* positive or negative). Beware: the index wraps around the edges.
*
*
* This and the other KernelDensityEstimate should have the same size.
*
*
* @param other
* The other estimate.
* @param positionsToShiftOther
* The number of positions to shift the estimate.
* @return A value between 0 and 1 representing how similar both estimates
* are. 1 means total correlation, 0 no correlation.
*/
public double correlation(final KernelDensityEstimate other,
final int positionsToShiftOther) {
assert other.size() == size() : "The kde size should be the same!";
double correlation;
double matchingArea = 0.0;
double biggestKDEArea = Math.max(getSumFreq(), other.getSumFreq());
//an if, else to prevent modulo calculation
if(positionsToShiftOther == 0){
for (int i = 0; i < accumulator.length; i++) {
matchingArea += Math.min(accumulator[i],other.accumulator[i]);
}
}else{
for (int i = 0; i < accumulator.length; i++) {
int otherIndex = (i + positionsToShiftOther) % other.size();
matchingArea += Math.min(accumulator[i],other.accumulator[otherIndex]);
}
}
if (matchingArea == 0.0) {
correlation = 0.0;
} else {
correlation = matchingArea / biggestKDEArea;
}
return correlation;
}
/**
* Calculates how much the other KernelDensityEstimate needs to be shifted
* for optimal correlation.
*
* @param other
* The other KernelDensityEstimate.
* @return A number between 0 (inclusive) and the size of the
* KernelDensityEstimate (exclusive) which represents how much the
* other KernelDensityEstimate needs to be shifted for optimal
* correlation.
*/
public int shiftForOptimalCorrelation(final KernelDensityEstimate other) {
int optimalShift = 0; // displacement with best correlation
double maximumCorrelation = -1; // best found correlation
for (int shift = 0; shift < size(); shift++) {
final double currentCorrelation = correlation(other, shift);
if (maximumCorrelation < currentCorrelation) {
maximumCorrelation = currentCorrelation;
optimalShift = shift;
}
}
return optimalShift;
}
/**
* Calculates the optimal correlation between two Kernel Density Estimates
* by shifting and searching for optimal correlation.
*
* @param other
* The other KernelDensityEstimate.
* @return A value between 0 and 1 representing how similar both estimates
* are. 1 means total correlation, 0 no correlation.
*/
public double optimalCorrelation(final KernelDensityEstimate other) {
int shift = shiftForOptimalCorrelation(other);
return correlation(other, shift);
}
/**
* Defines a kernel. It has a size and cached values for each index.
*
* @author Joren Six
*/
public static interface Kernel {
/**
* Fetch the value for the kernel at a certain index.
*
* @param kernelIndex
* The index of the previously computed value.
* @return The cached value for a certain index.
*/
double value(final int kernelIndex);
/**
* The size of the kernel.
*
* @return The size of the kernel.
*/
int size();
}
/**
* A Gaussian kernel function.
*
* @author Joren Six
*/
public static class GaussianKernel implements Kernel {
private final double kernel[];
/**
* Construct a kernel with a defined width.
*
* @param kernelWidth
* The width of the kernel.
*/
public GaussianKernel(final double kernelWidth) {
double calculationAria = 5 * kernelWidth;// Aria, not area
double halfWidth = kernelWidth / 2.0;
// Compute a kernel: a lookup table with e.g. a Gaussian curve
kernel = new double[(int) calculationAria * 2 + 1];
double difference = -calculationAria;
for (int i = 0; i < kernel.length; i++) {
double power = Math.pow(difference / halfWidth, 2.0);
kernel[i] = Math.pow(Math.E, -0.5 * power);
difference++;
}
}
public double value(int index) {
return kernel[index];
}
public int size() {
return kernel.length;
}
}
/**
* A rectangular kernel function.
*/
public static class RectangularKernel implements Kernel {
private final double kernel[];
public RectangularKernel(final double kernelWidth) {
// Compute a kernel: a lookup table with a width
kernel = new double[(int) kernelWidth];
for (int i = 0; i < kernel.length; i++) {
kernel[i] = 1.0;
}
}
public double value(int index) {
return kernel[index];
}
public int size() {
return kernel.length;
}
}
/**
* Calculates the optimal correlation between two Kernel Density Estimates
* by shifting and searching for optimal correlation.
* @param correlationMeasure
*
* @param other
* The other KernelDensityEstimate.
* @return A value between 0 and 1 representing how similar both estimates
* are. 1 means total correlation, 0 no correlation.
*/
public double optimalCorrelation(final KDECorrelation correlationMeasure, final KernelDensityEstimate other) {
int shift = shiftForOptimalCorrelation(correlationMeasure,other);
return correlationMeasure.correlation(this,other, shift);
}
/**
* Calculates how much the other KernelDensityEstimate needs to be shifted
* for optimal correlation.
* @param correlationMeasure
*
* @param other
* The other KernelDensityEstimate.
* @return A number between 0 (inclusive) and the size of the
* KernelDensityEstimate (exclusive) which represents how much the
* other KernelDensityEstimate needs to be shifted for optimal
* correlation.
*/
public int shiftForOptimalCorrelation(final KDECorrelation correlationMeasure, final KernelDensityEstimate other) {
int optimalShift = 0; // displacement with best correlation
double maximumCorrelation = -1; // best found correlation
for (int shift = 0; shift < size(); shift++) {
final double currentCorrelation = correlationMeasure.correlation(this,other, shift);
if (maximumCorrelation < currentCorrelation) {
maximumCorrelation = currentCorrelation;
optimalShift = shift;
}
}
return optimalShift;
}
public static interface KDECorrelation{
public double correlation(KernelDensityEstimate first,KernelDensityEstimate other, int shift);
}
public static class Overlap implements KDECorrelation{
public double correlation(KernelDensityEstimate first,KernelDensityEstimate other, int shift) {
double correlation;
int matchingArea = 0;
for (int i = 0; i < first.size(); i++) {
int otherIndex = (other.size() + i + shift) % other.size();
matchingArea += Math.min(first.getValue(i),other.getValue(otherIndex));
}
double biggestKDEArea = Math.max(first.getSumFreq(), other.getSumFreq());
correlation = matchingArea / biggestKDEArea;
return correlation;
}
}
public static class Cosine implements KDECorrelation{
public double correlation(KernelDensityEstimate first,KernelDensityEstimate other, int shift) {
double correlation;
double innerProduct = 0;
double firstSquaredSum = 0;
double otherSquaredSum = 0;
for (int i = 0; i < first.size(); i++) {
int otherIndex = (other.size() + i + shift) % other.size();
double firstValue = first.getValue(i);
double otherValue = other.getValue(otherIndex);
innerProduct += firstValue * otherValue;
firstSquaredSum += firstValue * firstValue;
otherSquaredSum += otherValue * otherValue;
}
correlation = innerProduct / ( Math.pow(firstSquaredSum, 0.5) * Math.pow(otherSquaredSum, 0.5));
return correlation;
}
}
public double[] getMedianFilteredEstimate(int medianFilterLength) {
double[] medianFilterBuffer;
double[] medianFilteredAccumulator = new double[accumulator.length];
double median = median(accumulator.clone());
// Naive median filter implementation.
// For each element take a median of surrounding values (noiseFloorBuffer)
// Store the median as the noise floor.
for (int i = 0; i < accumulator.length; i++) {
medianFilterBuffer = new double[medianFilterLength];
int index = 0;
for (int j = i - medianFilterLength/2; j <= i + medianFilterLength/2 && index < medianFilterBuffer.length; j++) {
if(j >= 0 && j < accumulator.length){
medianFilterBuffer[index] = accumulator[j];
} else{
medianFilterBuffer[index] = median;
}
index++;
}
medianFilteredAccumulator[i] = median(medianFilterBuffer);
}
return medianFilteredAccumulator;
}
public static double median(double[] m) {
// Sort the array in ascending order.
Arrays.sort(m);
int middle = m.length/2;
if (m.length%2 == 1) {
return m[middle];
} else {
return (m[middle-1] + m[middle]) / 2.0;
}
}
}