Skip to content

Commit

Permalink
Refactor acoustic events method
Browse files Browse the repository at this point in the history
Issue #238: change method which makes first attempt to look for FF chirps. Start second method which filters those on the basis of the similarity of their spectral profile to that of a flying fox.
  • Loading branch information
towsey authored and atruskie committed Aug 28, 2019
1 parent aa85432 commit f74098d
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 46 deletions.
77 changes: 56 additions & 21 deletions src/AnalysisPrograms/Recognizers/PteropusSpecies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,18 @@ public override RecognizerResults Recognize(AudioRecording audioRecording, Confi
internal static RecognizerResults Gruntwork(AudioRecording audioRecording, Config configuration, DirectoryInfo outputDirectory, TimeSpan segmentStartOffset)
{
// get the common properties
string speciesName = configuration[AnalysisKeys.SpeciesName] ?? "<no species>";
string abbreviatedSpeciesName = configuration[AnalysisKeys.AbbreviatedSpeciesName] ?? "<no.sp>";
int minHz = configuration.GetIntOrNull(AnalysisKeys.MinHz) ?? 500;
string speciesName = configuration[AnalysisKeys.SpeciesName] ?? "Pteropus species";
string abbreviatedSpeciesName = configuration[AnalysisKeys.AbbreviatedSpeciesName] ?? "Pteropus";
int minHz = configuration.GetIntOrNull(AnalysisKeys.MinHz) ?? 800;
int maxHz = configuration.GetIntOrNull(AnalysisKeys.MaxHz) ?? 8000;

//var samples = audioRecording.WavReader.Samples;
//double minDuration = configuration.GetIntOrNull(AnalysisKeys.MinDuration) ?? 0.1;
//double maxDuration = configuration.GetIntOrNull(AnalysisKeys.MaxDuration) ?? 0.5;
var neighbourhoodDuration = TimeSpan.FromSeconds(0.05);
double minDurationSeconds = configuration.GetIntOrNull(AnalysisKeys.MinDuration) ?? 0.2;
double maxDurationSeconds = configuration.GetIntOrNull(AnalysisKeys.MaxDuration) ?? 0.5;
var minTimeSpan = TimeSpan.FromSeconds(minDurationSeconds);
var maxTimeSpan = TimeSpan.FromSeconds(maxDurationSeconds);

double decibelsIntensityThreshold = configuration.GetDoubleOrNull(AnalysisKeys.NoiseBgThreshold) ?? 12.0;
double intensityNormalisationMax = 3 * decibelsIntensityThreshold;
var eventThreshold = decibelsIntensityThreshold / intensityNormalisationMax;
double decibelThreshold = configuration.GetDoubleOrNull(AnalysisKeys.NoiseBgThreshold) ?? 9.0;

//######################
//2.Convert each segment to a spectrogram.
Expand All @@ -128,24 +127,27 @@ internal static RecognizerResults Gruntwork(AudioRecording audioRecording, Confi
// now construct the standard decibel spectrogram WITH noise removal, and look for LimConvex
// get frame parameters for the analysis
var sonogram = (BaseSonogram)new SpectrogramStandard(sonoConfig, audioRecording.WavReader);
var intensityArray = SNR.CalculateFreqBandAvIntensity(sonogram.Data, minHz, maxHz, sonogram.NyquistFrequency);
var decibelArray = SNR.CalculateFreqBandAvIntensity(sonogram.Data, minHz, maxHz, sonogram.NyquistFrequency);

//var data = sonogram.Data;
//var intensityArray = MatrixTools.GetRowAverages(data);
intensityArray = DataTools.NormaliseInZeroOne(intensityArray, 0, intensityNormalisationMax);
var plot = new Plot(speciesName, intensityArray, eventThreshold);
// prepare plots
double intensityNormalisationMax = 3 * decibelThreshold;
var eventThreshold = decibelThreshold / intensityNormalisationMax;
var normalisedIntensityArray = DataTools.NormaliseInZeroOne(decibelArray, 0, intensityNormalisationMax);
var plot = new Plot(speciesName, normalisedIntensityArray, eventThreshold);
var plots = new List<Plot> { plot };

//iii: CONVERT decibel SCORES TO ACOUSTIC EVENTS
var acousticEvents = AcousticEvent.GetEventsAroundMaxima(
intensityArray,
decibelArray,
segmentStartOffset,
minHz,
maxHz,
decibelThreshold,
minTimeSpan,
maxTimeSpan,
sonogram.FramesPerSecond,
sonogram.FBinWidth,
eventThreshold,
neighbourhoodDuration);
sonogram.FBinWidth
);

// ######################################################################
acousticEvents.ForEach(ae =>
Expand All @@ -156,15 +158,17 @@ internal static RecognizerResults Gruntwork(AudioRecording audioRecording, Confi
ae.Name = abbreviatedSpeciesName;
});

acousticEvents = FilterEventsForSpectralProfile(acousticEvents, sonogram.Data);

//var sonoImage = sonogram.GetImageFullyAnnotated("Test");
var sonoImage = SpectrogramTools.GetSonogramPlusCharts(sonogram, acousticEvents, plots, null);
//var sonoImage = SpectrogramTools.GetSonogramPlusCharts(sonogram, acousticEvents, plots, null);

//var opPath =
// outputDirectory.Combine(
// FilenameHelpers.AnalysisResultName(
// Path.GetFileNameWithoutExtension(recording.BaseName), speciesName, "png", "DebugSpectrogram"));
string imageFilename = audioRecording.BaseName + ".png";
sonoImage.Save(Path.Combine(outputDirectory.FullName, imageFilename));
//string imageFilename = audioRecording.BaseName + ".png";
//sonoImage.Save(Path.Combine(outputDirectory.FullName, imageFilename));

return new RecognizerResults()
{
Expand All @@ -176,6 +180,37 @@ internal static RecognizerResults Gruntwork(AudioRecording audioRecording, Confi
};
}

/// <summary>
/// Remove events whose acoustic profile does not match that of a flying fox.
/// </summary>
/// <param name="events">unfiltered acoustic events.</param>
/// <param name="spectrogramData">matrix of spectrogram values</param>
/// <returns>filtered acoustic events.</returns>
private static List<AcousticEvent> FilterEventsForSpectralProfile(List<AcousticEvent> events, double[,] spectrogramData)
{
int colCount = spectrogramData.GetLength(1);
var filteredEvents = new List<AcousticEvent>();
foreach (AcousticEvent ae in events)
{
int startFrame = ae.Oblong.RowTop;
int endFrame = ae.Oblong.RowBottom;
var subMatrix = DataTools.Submatrix(spectrogramData, startFrame, 0, endFrame, colCount - 1);
var spectrum = MatrixTools.GetColumnAverages(subMatrix);

// do test to determine if event has spectrum matching a Flying fox.
// TODO write method to determine similarity of spectrum to a true flying fox spectrum.
// There should be little energy in 0-600 Hz band.
// There should three peaks at around 1.5 kHz, 3 kHz and 6 kHz.
bool goodMatch = true;
if (goodMatch)
{
filteredEvents.Add(ae);
}
}

return filteredEvents;
}

/*
// example method
private static List<AcousticEvent> RunFemaleProfile(configuration, rest of arguments)
Expand Down
90 changes: 65 additions & 25 deletions src/AudioAnalysisTools/AcousticEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1111,62 +1111,102 @@ public static List<AcousticEvent> ConvertIntensityArray2Events(
return events;
}

/// <summary>
/// Given a time series of acoustic amplitude (typically in decibels), this method finds events that match the passed constraints.
/// </summary>
/// <param name="values">an array of amplitude values, typically decibel values.</param>
/// <param name="segmentStartOffset">not sure what this is about!.</param>
/// <param name="minHz">minimum freq of event.</param>
/// <param name="maxHz">maximum freq of event.</param>
/// <param name="thresholdValue">event threshold in same units as the value array.</param>
/// <param name="minDuration">minimum duration of an event.</param>
/// <param name="maxDuration">maximum duration of an event.</param>
/// <param name="framesPerSec">the time scale - required for drawing events.</param>
/// <param name="freqBinWidth">the frequency scale - required for drawing events.</param>
/// <returns>an array of class AcousticEvent.</returns>
public static List<AcousticEvent> GetEventsAroundMaxima(
double[] values,
TimeSpan segmentStartOffset,
int minHz,
int maxHz,
double framesPerSec,
double freqBinWidth,
double thresholdValue,
TimeSpan nh)
TimeSpan minDuration,
TimeSpan maxDuration,
double framesPerSec,
double freqBinWidth)
{
int count = values.Length;
var events = new List<AcousticEvent>();
double frameOffset = 1 / framesPerSec; //frame offset in fractions of second

// every event will have duration of twice the buffer + 1
int frameBuffer = (int)Math.Ceiling(nh.TotalSeconds / frameOffset);
frameBuffer = Math.Max(frameBuffer, 1);
int eventLength = (2 * frameBuffer) + 1;
// convert min an max times durations to frames
int minFrames = (int)Math.Floor(minDuration.TotalSeconds * framesPerSec);
int maxFrames = (int)Math.Ceiling(maxDuration.TotalSeconds * framesPerSec);

// every event has the same duration
double eventDuration = eventLength * frameOffset;
// convert min an max Hertz durations to freq bins
int minBin = (int)Math.Round(minHz / freqBinWidth);
int maxBin = (int)Math.Round(maxHz / freqBinWidth);

//int startFrame = 0;
// tried smoothing but not advisable since event onset can be very sudden
//values = DataTools.filterMovingAverageOdd(values, 3);
int startFrame = 0;
int endFrame = 0;

// for all frames
for (int i = frameBuffer; i < count - frameBuffer; i++)
for (int i = 1; i < count - minFrames; i++)
{
// get the neighbourhood
var nhArray = DataTools.Subarray(values, i - frameBuffer, eventLength);
double avValue = nhArray.Average();
// skip if value is below threshold
if (values[i] < thresholdValue)
{
continue;
}

// skip if average value over the neighbourhood below threshold
if (avValue < thresholdValue)
// skip if value is not maximum
if (values[i] < values[i - 1] || values[i] < values[i + 1])
{
continue;
}

// if local maximum just prior to middle frame AND average of values > threshold, THEN have an event
int maxId = DataTools.GetMaxIndex(nhArray);
if (maxId == (frameBuffer - 2))
int maxFrame = i;

// find start frame of current event
while (values[i] > thresholdValue)
{
i--;
}

startFrame = i;

// find end frame of current event
i = maxFrame;
while (values[i] > thresholdValue)
{
i++;
}

endFrame = i;

int frameDuration = endFrame - startFrame; // +1 ?????????????????
if (frameDuration >= minFrames && frameDuration <= maxFrames)
{
double startTime = (i - frameBuffer) * frameOffset; // time in seconds
double startTime = startFrame * frameOffset; // time in seconds
double eventDuration = frameDuration * frameOffset; // time in seconds
AcousticEvent ev = new AcousticEvent(segmentStartOffset, startTime, eventDuration, minHz, maxHz)
{
Name = "Event", //default name
FrameCount = frameDuration,
Oblong = new Oblong(startFrame, minBin, endFrame, maxBin),
};

ev.SetTimeAndFreqScales(framesPerSec, freqBinWidth);

//obtain average intensity score.
ev.Score = nhArray.Average();
//obtain average intensity score. Note-first frame is not actually in the event.
var subArray = DataTools.Subarray(values, startFrame + 1, frameDuration);
ev.Score = subArray.Average();
events.Add(ev);

// jump to end of event
i += eventLength;
}

i++;
}

return events;
Expand Down
2 changes: 2 additions & 0 deletions src/AudioAnalysisTools/DSP/SNR.cs
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,8 @@ public static Tuple<double[], double, double> SubbandIntensity_NoiseReduced(

/// <summary>
/// Calculates the mean intensity in a freq band defined by its min and max freq.
/// THis method adds dB log values incorrectly but it is faster than doing many log conversions.
/// This method is used to find acoustic events and is accurate enough for the purpose.
/// </summary>
public static double[] CalculateFreqBandAvIntensity(double[,] sonogram, int minHz, int maxHz, int nyquist)
{
Expand Down

0 comments on commit f74098d

Please sign in to comment.