diff --git a/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.PteropusSpecies.yml b/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.PteropusSpecies.yml index 53b1e1b77..414acc335 100644 --- a/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.PteropusSpecies.yml +++ b/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.PteropusSpecies.yml @@ -32,8 +32,9 @@ Profiles: Wingbeats: MinHz: 200 MaxHz: 2000 + DecibelThreshold: 6.0 # duration of DCT in seconds - DctDuration: 0.8 + DctDuration: 0.5 # minimum acceptable value of a DCT coefficient DctThreshold: 0.5 # ignore oscillation rates below the min & above the max threshold @@ -46,7 +47,7 @@ Profiles: MinDuration: 1.0 MaxDuration: 10.0 # Event threshold - use this to determine FP / FN trade-off for events. - EventThreshold: 0.60 + EventThreshold: 0.5 #Agonist: # This notation means the Groote profile has all of the settings that the Standard profile has, # however, the MinHz and MaxHz properties have been overridden. diff --git a/src/AnalysisPrograms/Recognizers/LitoriaCaerulea.cs b/src/AnalysisPrograms/Recognizers/LitoriaCaerulea.cs index 897f3c734..4cfcdf25f 100644 --- a/src/AnalysisPrograms/Recognizers/LitoriaCaerulea.cs +++ b/src/AnalysisPrograms/Recognizers/LitoriaCaerulea.cs @@ -1,4 +1,4 @@ -// -------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------- // // All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). // @@ -193,7 +193,7 @@ public override RecognizerResults Recognize(AudioRecording recording, Config con double dctThreshold = recognizerConfig.DctThreshold; double minOscRate = 1 / recognizerConfig.MaxPeriod; double maxOscRate = 1 / recognizerConfig.MinPeriod; - var dctScores = Oscillations2012.DetectOscillations(croakScoreArray, framesPerSecond, dctDuration, minOscRate, maxOscRate, dctThreshold); + Oscillations2019.DetectOscillations(croakScoreArray, framesPerSecond, decibelThreshold, dctDuration, minOscRate, maxOscRate, dctThreshold, out double[] dctScores, out double[] oscFreq); // ###################################################################### // ii: DO THE ANALYSIS AND RECOVER SCORES OR WHATEVER diff --git a/src/AnalysisPrograms/Recognizers/PteropusSpecies.cs b/src/AnalysisPrograms/Recognizers/PteropusSpecies.cs index 0034f5af7..c69c233e8 100644 --- a/src/AnalysisPrograms/Recognizers/PteropusSpecies.cs +++ b/src/AnalysisPrograms/Recognizers/PteropusSpecies.cs @@ -39,6 +39,7 @@ namespace AnalysisPrograms.Recognizers using System.IO; using System.Linq; using System.Reflection; + using Acoustics.Shared; using Acoustics.Shared.ConfigFile; using AnalysisPrograms.Recognizers.Base; @@ -55,14 +56,17 @@ namespace AnalysisPrograms.Recognizers /// internal class PteropusSpecies : RecognizerBase { + private static readonly ILog PteropusLog = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + // The default window for Pteropus sp. Need to be fixed for accurately detecting wing beat oscillations. + private static readonly int DefaultWindow = 512; + public override string Author => "Towsey"; public override string SpeciesName => "PteropusSpecies"; public override string Description => "[ALPHA] Detects acoustic events for species of Flying Fox, Pteropus species"; - private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); - /* /// /// Summarize your results. This method is invoked exactly once per original file. @@ -87,9 +91,9 @@ public override void SummariseResults( /// config file that contains parameters used by all profiles. /// when recording starts. /// not sure what this is. - /// where the recogniser results can be found. + /// where the recognizer results can be found. /// assuming ????. - /// recogniser results. + /// recognizer results. public override RecognizerResults Recognize(AudioRecording audioRecording, Config genericConfig, TimeSpan segmentStartOffset, Lazy getSpectralIndexes, DirectoryInfo outputDirectory, int? imageWidth) { if (ConfigFile.HasProfiles(genericConfig)) @@ -102,44 +106,40 @@ public override RecognizerResults Recognize(AudioRecording audioRecording, Confi message = message + (s + ", "); } - log.Debug(message); + PteropusLog.Debug(message); } else { - log.Warn("No configuration profiles found. Two profiles expected for the Flying Fox recogniser."); + PteropusLog.Warn("No configuration profiles found. Two profiles expected for the Flying Fox recogniser."); } - RecognizerResults territorialResults = null; + var territorialResults = new RecognizerResults(); - if (ConfigFile.TryGetProfile(genericConfig, "Territorial", out var profile1)) + if (ConfigFile.TryGetProfile(genericConfig, "Territorial", out var _)) { territorialResults = TerritorialCall(audioRecording, genericConfig, "Territorial", segmentStartOffset); - log.Debug("Territory event count = " + territorialResults.Events.Count); + PteropusLog.Debug("Territory event count = " + territorialResults.Events.Count); } else { - log.Warn("Could not access Territorial configuration parameters"); + PteropusLog.Warn("Could not access Territorial configuration parameters"); } - RecognizerResults wingbeatResults = null; - if (ConfigFile.TryGetProfile(genericConfig, "Wingbeats", out var profile2)) + var wingbeatResults = new RecognizerResults(); + if (ConfigFile.TryGetProfile(genericConfig, "Wingbeats", out var _)) { wingbeatResults = WingBeats(audioRecording, genericConfig, "Wingbeats", segmentStartOffset); - log.Debug("Wingbeat event count = " + wingbeatResults.Events.Count); + PteropusLog.Debug("Wingbeat event count = " + wingbeatResults.Events.Count); } else { - log.Warn("Could not access Wingbeats configuration parameters"); + PteropusLog.Warn("Could not access Wingbeats configuration parameters"); } // combine the results i.e. add wing-beat events to the list of territorial call events. - - // results1.Events.Concat(results2.Events.Concat) - if (territorialResults != null && wingbeatResults != null) - { - territorialResults.Events.AddRange(wingbeatResults.Events); - territorialResults.Plots.AddRange(wingbeatResults.Plots); - } + //NOTE: The returned territorialResults and wingbeatResults will never be null. + territorialResults.Events.AddRange(wingbeatResults.Events); + territorialResults.Plots.AddRange(wingbeatResults.Plots); //UNCOMMENT following line if you want special debug spectrogram, i.e. with special plots. // NOTE: Standard spectrograms are produced by setting SaveSonogramImages: "True" or "WhenEventsDetected" in config file. @@ -176,24 +176,8 @@ private static RecognizerResults TerritorialCall(AudioRecording audioRecording, var maxTimeSpan = TimeSpan.FromSeconds(maxDurationSeconds); //###################### - //2. Don't use samples in this recogniser. - //var samples = audioRecording.WavReader.Samples; - //Instead, convert each segment to a spectrogram. + //2. Convert each segment to a spectrogram. var sonogram = GetSonogram(configuration, audioRecording); - /* - // make a spectrogram - var sonoConfig = new SonogramConfig - { - WindowSize = 512, - NoiseReductionType = NoiseReductionType.Standard, - NoiseReductionParameter = configuration.GetDoubleOrNull(AnalysisKeys.NoiseBgThreshold) ?? 0.0, - }; - sonoConfig.WindowOverlap = 0.0; - - // 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 decibelArray = SNR.CalculateFreqBandAvIntensity(sonogram.Data, minHz, maxHz, sonogram.NyquistFrequency); // prepare plots @@ -218,6 +202,7 @@ private static RecognizerResults TerritorialCall(AudioRecording audioRecording, //iV add additional info to the acoustic events acousticEvents.ForEach(ae => { + ae.FileName = audioRecording.BaseName; ae.SpeciesName = speciesName; ae.Name = abbreviatedSpeciesName + profileName; ae.Profile = profileName; @@ -246,7 +231,7 @@ private static RecognizerResults TerritorialCall(AudioRecording audioRecording, private static List FilterEventsForSpectralProfile(List events, BaseSonogram sonogram) { double[,] spectrogramData = sonogram.Data; - int colCount = spectrogramData.GetLength(1); + //int colCount = spectrogramData.GetLength(1); // The following freq bins are used to demarcate freq bands for spectral tests below. // The hertz values are hard coded but could be included in the config.yml file. @@ -258,7 +243,7 @@ private static List FilterEventsForSpectralProfile(List FilterEventsForSpectralProfile(List FilterEventsForSpectralProfile(List { plot1, plot2 }; // ###################################################################### + + // add additional information about the recording and sonogram properties from which the event is derived. acousticEvents.ForEach(ae => { + ae.FileName = audioRecording.BaseName; ae.SpeciesName = speciesName; ae.Name = abbreviatedSpeciesName + profileName; ae.Profile = profileName; ae.SegmentDurationSeconds = audioRecording.Duration.TotalSeconds; ae.SegmentStartSeconds = segmentStartOffset.TotalSeconds; + var frameOffset = sonogram.FrameStep; + var frameDuration = sonogram.FrameDuration; + ae.SetTimeAndFreqScales(frameOffset, frameDuration, sonogram.FBinWidth); //UNCOMMENT following lines to get spectral profiles of the Wingbeat events. /* double[,] spectrogramData = sonogram.Data; @@ -475,13 +438,13 @@ internal static BaseSonogram GetSonogram(Config configuration, AudioRecording au { var sonoConfig = new SonogramConfig { - WindowSize = 512, + WindowSize = DefaultWindow, NoiseReductionType = NoiseReductionType.Standard, NoiseReductionParameter = configuration.GetDoubleOrNull(AnalysisKeys.NoiseBgThreshold) ?? 0.0, + WindowOverlap = 0.0, }; - sonoConfig.WindowOverlap = 0.0; - // now construct the standard decibel spectrogram WITH noise removal, and look for LimConvex + // now construct the standard decibel spectrogram WITH noise removal // get frame parameters for the analysis var sonogram = (BaseSonogram)new SpectrogramStandard(sonoConfig, audioRecording.WavReader); return sonogram; diff --git a/src/AnalysisPrograms/Sandpit.cs b/src/AnalysisPrograms/Sandpit.cs index 3df8a93d5..faa43cd51 100644 --- a/src/AnalysisPrograms/Sandpit.cs +++ b/src/AnalysisPrograms/Sandpit.cs @@ -291,11 +291,14 @@ public static void Audio2CsvOverOneFile() // FLYING FOX RECORDINGS //string recordingPath = @"C:\Ecoacoustics\WavFiles\BradLawData\FlyingFox\20190127_Bellingen_Feeding_SM4.wav"; - //string recordingPath = @"C:\Ecoacoustics\WavFiles\FlyingFox\20190115_Bellingen_Feeding.wav"; + string recordingPath = @"D:\Ecoacoustics\WavFiles\FlyingFox\20190115_Bellingen_Feeding.wav"; + //string recordingPath = @"C:\Ecoacoustics\WavFiles\FlyingFox\20190115_Bellingen_Feeding_minute6.wav"; + //string recordingPath = @"C:\Ecoacoustics\WavFiles\FlyingFox\20190115_Bellingen_Feeding_minute6_OneChannel22050.wav"; //string recordingPath = @"C:\Ecoacoustics\WavFiles\FlyingFox\20190121_2_Bellingen_Feeding.wav"; - string recordingPath = @"C:\Ecoacoustics\WavFiles\FlyingFox\20190127_Bellingen_Feeding_SM4.wav"; + //string recordingPath = @"C:\Ecoacoustics\WavFiles\FlyingFox\20190127_Bellingen_Feeding_SM4.wav"; string configPath = @"C:\Work\GitHub\audio-analysis\src\AnalysisConfigFiles\RecognizerConfigFiles\Towsey.PteropusSpecies.yml"; - string outputPath = @"C:\Ecoacoustics\Output\BradLaw\FlyingFox"; + //string outputPath = @"C:\Ecoacoustics\Output\BradLaw\FlyingFox"; + string outputPath = @"C:\Ecoacoustics\FlyingFox"; // TSHERING DEMA BHUTAN RECORDINGS //string recordingPath = @"C:\SensorNetworks\WavFiles\TsheringDema\WBH12HOURS-D_20160403_120000.wav"; @@ -358,7 +361,6 @@ public static void Audio2CsvOverOneFile() //string outputPath = @"C:\Ecoacoustics\Output\Test\Test24HourRecording\Delete"; //string configPath = @"C:\Work\GitHub\audio-analysis\src\AnalysisConfigFiles\Towsey.Acoustic.yml"; - // Ivan Campos recordings //string recordingPath = @"G:\SensorNetworks\WavFiles\Ivancampos\INCIPO01_20161031_024006_898.wav"; //string outputPath = @"G:\SensorNetworks\Output\IvanCampos\17"; @@ -420,7 +422,7 @@ public static void Audio2CsvOverOneFile() //string outputPath = @"C:\Ecoacoustics\Output\SERF\SERFIndicesNew_2013June19"; //string configPath = @"C:\Work\GitHub\audio-analysis\src\AnalysisConfigFiles\Towsey.Acoustic.yml"; - // USE 24-hour data or parts of from MEZ, TASMAn ISLAND, liz Znidersic + // USE 24-hour data or parts of from MEZ, TASMAn ISLAND, liz Znidersic // these are six hour recordings //string recordingPath = @"C:\Ecoacoustics\WavFiles\LizZnidersic\TasmanIsland2015_Unit2_Mez\SM304256_0+1_20151114_031652.wav"; //string outputPath = @"C:\Ecoacoustics\Output\Test\Test24HourRecording\TasmanIslandMez\04"; diff --git a/src/AudioAnalysisTools/AcousticEvent.cs b/src/AudioAnalysisTools/AcousticEvent.cs index 02e51195f..8a44b233f 100644 --- a/src/AudioAnalysisTools/AcousticEvent.cs +++ b/src/AudioAnalysisTools/AcousticEvent.cs @@ -11,19 +11,17 @@ namespace AudioAnalysisTools { using System; using System.Collections.Generic; - using System.Collections.ObjectModel; using System.Drawing; using System.IO; using System.Linq; using System.Text; - using System.Text.RegularExpressions; using Acoustics.Shared.Contracts; using Acoustics.Shared.Csv; using AForge.Imaging.Filters; using AnalysisBase.ResultBases; + using AudioAnalysisTools.DSP; + using AudioAnalysisTools.StandardSpectrograms; using CsvHelper.Configuration; - using DSP; - using StandardSpectrograms; using TowseyLibrary; public class AcousticEvent : EventBase @@ -113,8 +111,8 @@ public void SetEventPositionRelative( } /// - /// Gets or sets units = Hertz - /// Proxied to EventBase.MinHz + /// Gets or sets units = Hertz. + /// Proxied to EventBase.MinHz. /// public new double LowFrequencyHertz { @@ -301,15 +299,12 @@ public void DoMelScale(bool doMelscale, int freqBinCount) public void SetTimeAndFreqScales(int samplingRate, int windowSize, int windowOffset) { - double frameDuration, frameOffset, framesPerSecond; - CalculateTimeScale(samplingRate, windowSize, windowOffset, out frameDuration, out frameOffset, out framesPerSecond); + CalculateTimeScale(samplingRate, windowSize, windowOffset, out var frameDuration, out var frameOffset, out var framesPerSecond); this.FrameDuration = frameDuration; //frame duration in seconds this.FrameOffset = frameOffset; //frame offset in seconds this.FramesPerSecond = framesPerSecond; //inverse of the frame offset - int binCount; - double binWidth; - CalculateFreqScale(samplingRate, windowSize, out binCount, out binWidth); + CalculateFreqScale(samplingRate, windowSize, out var binCount, out var binWidth); this.FreqBinCount = binCount; //required for conversions to & from MEL scale this.FreqBinWidth = binWidth; //required for freq-binID conversions @@ -319,11 +314,22 @@ public void SetTimeAndFreqScales(int samplingRate, int windowSize, int windowOff } } + /// + /// This method assumes that there is no frame overlap i.e. frame duration = frame offset. + /// + /// frames per second assuming no overlap. + /// Number of hertz per freq bin. public void SetTimeAndFreqScales(double framesPerSec, double freqBinWidth) { - //this.FrameDuration = frameDuration; //frame duration in seconds - this.FramesPerSecond = framesPerSec; //inverse of the frame offset - this.FrameOffset = 1 / framesPerSec; //frame offset in seconds + double frameOffset = 1 / framesPerSec; //frame offset in seconds + this.SetTimeAndFreqScales(frameOffset, frameOffset, freqBinWidth); + } + + public void SetTimeAndFreqScales(double frameOffset, double frameDuration, double freqBinWidth) + { + this.FramesPerSecond = 1 / frameOffset; //inverse of the frame offset + this.FrameDuration = frameDuration; //frame duration in seconds + this.FrameOffset = frameOffset; //frame duration in seconds //this.FreqBinCount = binCount; //required for conversions to & from MEL scale this.FreqBinWidth = freqBinWidth; //required for freq-binID conversions @@ -343,18 +349,19 @@ public void SetTimeAndFreqScales(double framesPerSec, double freqBinWidth) public static Oblong ConvertEvent2Oblong(AcousticEvent ae) { // Translate time dimension = frames = matrix rows. - int topRow; - int bottomRow; - Time2RowIDs(ae.TimeStart, ae.EventDurationSeconds, ae.FrameOffset, out topRow, out bottomRow); + Time2RowIDs(ae.TimeStart, ae.EventDurationSeconds, ae.FrameOffset, out var topRow, out var bottomRow); //Translate freq dimension = freq bins = matrix columns. - int leftCol; - int rightCol; - Freq2BinIDs(ae.IsMelscale, (int)ae.LowFrequencyHertz, (int)ae.HighFrequencyHertz, ae.FreqBinCount, ae.FreqBinWidth, out leftCol, out rightCol); + Freq2BinIDs(ae.IsMelscale, (int)ae.LowFrequencyHertz, (int)ae.HighFrequencyHertz, ae.FreqBinCount, ae.FreqBinWidth, out var leftCol, out var rightCol); return new Oblong(topRow, leftCol, bottomRow, rightCol); } + /// + /// Should check that Oblong is not null before calling this method. + /// + public Rectangle GetEventAsRectangle() => new Rectangle(this.Oblong.ColumnLeft, this.Oblong.RowTop, this.Oblong.ColWidth, this.Oblong.RowWidth); + /// /// Sets the passed score and also a value normalised between a min and a max. /// @@ -934,15 +941,6 @@ public static void CalculateAccuracy(List results, List - /// - /// - /// - /// - /// - /// - /// - /// - /// public static void CalculateAccuracyOnOneRecording(List results, List labels, out int tp, out int fp, out int fn, out double precision, out double recall, out double accuracy, out string resultsText) { @@ -1041,8 +1039,6 @@ public static void CalculateAccuracyOnOneRecording(List results, /// /// duration of event must exceed this to count as an event /// - /// array value must exceed this dB threshold to count as an event - /// name of source file to be added to AcousticEvent class /// a list of acoustic events //public static List ConvertIntensityArray2Events(double[] values, int minHz, int maxHz, // double framesPerSec, double freqBinWidth, @@ -1079,7 +1075,7 @@ public static List ConvertIntensityArray2Events( startFrame = i; } else //check for the end of an event - if (isHit == true && values[i] <= scoreThreshold) //this is end of an event, so initialise it + if (isHit && values[i] <= scoreThreshold) //this is end of an event, so initialise it { isHit = false; double endTime = i * frameOffset; @@ -1146,6 +1142,7 @@ public static List GetEventsAroundMaxima( // convert min an max Hertz durations to freq bins int minBin = (int)Math.Round(minHz / freqBinWidth); int maxBin = (int)Math.Round(maxHz / freqBinWidth); + int binCount = maxBin - minBin + 1; // tried smoothing but not advisable since event onset can be very sudden //values = DataTools.filterMovingAverageOdd(values, 3); @@ -1191,22 +1188,23 @@ public static List GetEventsAroundMaxima( endFrame = i; - int frameDuration = endFrame - startFrame + 1; - if (frameDuration >= minFrames && frameDuration <= maxFrames) + int frameCount = endFrame - startFrame + 1; + if (frameCount >= minFrames && frameCount <= maxFrames) { double startTime = startFrame * frameOffset; // time in seconds - double eventDuration = frameDuration * frameOffset; // time in seconds + double eventDuration = frameCount * frameOffset; // time in seconds AcousticEvent ev = new AcousticEvent(segmentStartOffset, startTime, eventDuration, minHz, maxHz) { Name = "Event", //default name - FrameCount = frameDuration, + FrameCount = frameCount, + FreqBinCount = binCount, Oblong = new Oblong(startFrame, minBin, endFrame, maxBin), }; ev.SetTimeAndFreqScales(framesPerSec, freqBinWidth); //obtain average intensity score. Note-first frame is not actually in the event. - var subArray = DataTools.Subarray(values, startFrame + 1, frameDuration); + var subArray = DataTools.Subarray(values, startFrame + 1, frameCount); ev.Score = subArray.Average(); events.Add(ev); } @@ -1225,18 +1223,15 @@ public static List GetEventsAroundMaxima( /// Some analysis techniques (e.g. OD) have their own methods for extracting events from score arrays. /// /// the array of scores - /// lower freq bound of the acoustic event - /// upper freq bound of the acoustic event - /// the time scale required by AcousticEvent class - /// the freq scale required by AcousticEvent class - /// - /// duration of event must exceed this to count as an event - /// duration of event must be less than this to count as an event - /// - /// score must exceed this threshold to count as an event - /// name of source file to be added to AcousticEvent class - /// name of the event to be added to AcousticEvent class - /// a list of acoustic events + /// lower freq bound of the acoustic event. + /// upper freq bound of the acoustic event. + /// the time scale required by AcousticEvent class. + /// the freq scale required by AcousticEvent class. + /// threshold. + /// duration of event must exceed this to count as an event. + /// duration of event must be less than this to count as an event. + /// offset. + /// a list of acoustic events. public static List ConvertScoreArray2Events( double[] scores, int minHz, @@ -1250,23 +1245,26 @@ public static List ConvertScoreArray2Events( { int count = scores.Length; var events = new List(); - double maxPossibleScore = 5 * scoreThreshold; // used to calcualte a normalised score bewteen 0 - 1.0 + double maxPossibleScore = 5 * scoreThreshold; // used to calculate a normalised score between 0 - 1.0 bool isHit = false; double frameOffset = 1 / framesPerSec; // frame offset in fractions of second double startTime = 0.0; int startFrame = 0; - for (int i = 0; i < count; i++) // pass over all frames + // pass over all frames + for (int i = 0; i < count; i++) { - if (isHit == false && scores[i] >= scoreThreshold) //start of an event + if (isHit == false && scores[i] >= scoreThreshold) { + //start of an event isHit = true; startTime = i * frameOffset; startFrame = i; } else // check for the end of an event - if (isHit == true && scores[i] <= scoreThreshold) // this is end of an event, so initialise it + if (isHit && scores[i] <= scoreThreshold) { + // this is end of an event, so initialise it isHit = false; double endTime = i * frameOffset; double duration = endTime - startTime; @@ -1326,10 +1324,6 @@ public static List ConvertScoreArray2Events( /// The events are required to have the passed name. /// The events are assumed to contain sufficient info about frame rate in order to populate the array. /// - /// - /// - /// - /// public static double[] ExtractScoreArrayFromEvents(List events, int arraySize, string nameOfTargetEvent) { double[] scores = new double[arraySize]; @@ -1341,7 +1335,7 @@ public static double[] ExtractScoreArrayFromEvents(List events, i double windowOffset = events[0].FrameOffset; double frameRate = 1 / windowOffset; //frames per second - int count = events.Count; + //int count = events.Count; foreach ( AcousticEvent ae in events) { if (!ae.Name.Equals(nameOfTargetEvent)) @@ -1359,7 +1353,7 @@ public static double[] ExtractScoreArrayFromEvents(List events, i } return scores; - } //end method + } //############################################################################################################################################## @@ -1367,10 +1361,6 @@ public static double[] ExtractScoreArrayFromEvents(List events, i /// This method is used to do unit test on lists of events. /// First developed for frog recognizers - October 2016. /// - /// - /// - /// - /// public static void TestToCompareEvents(string fileName, DirectoryInfo opDir, string testName, List events) { var testDir = new DirectoryInfo(opDir + $"\\UnitTest_{testName}"); @@ -1385,7 +1375,7 @@ public static void TestToCompareEvents(string fileName, DirectoryInfo opDir, str var eventsFile = new FileInfo(eventsFilePath); Csv.WriteToCsv(eventsFile, events); - LoggedConsole.WriteLine($"# EVENTS TEST: Camparing List of {testName} events with those in benchmark file:"); + LoggedConsole.WriteLine($"# EVENTS TEST: Comparing List of {testName} events with those in benchmark file:"); var benchmarkFile = new FileInfo(benchmarkFilePath); if (!benchmarkFile.Exists) { diff --git a/src/AudioAnalysisTools/AudioAnalysisTools.csproj b/src/AudioAnalysisTools/AudioAnalysisTools.csproj index 3e85ba1a3..f2ac66098 100644 --- a/src/AudioAnalysisTools/AudioAnalysisTools.csproj +++ b/src/AudioAnalysisTools/AudioAnalysisTools.csproj @@ -300,6 +300,7 @@ + diff --git a/src/AudioAnalysisTools/Oscillations2012.cs b/src/AudioAnalysisTools/Oscillations2012.cs index def1bc1a2..e4e6173ff 100644 --- a/src/AudioAnalysisTools/Oscillations2012.cs +++ b/src/AudioAnalysisTools/Oscillations2012.cs @@ -1,4 +1,4 @@ -// +// // All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). // @@ -6,20 +6,44 @@ namespace AudioAnalysisTools { using System; using System.Collections.Generic; - using DSP; - using StandardSpectrograms; + using AudioAnalysisTools.DSP; + using AudioAnalysisTools.StandardSpectrograms; using TowseyLibrary; /// /// NOTE: 21st June 2012. /// /// This class contains methods to detect oscillations in a the sonogram of an audio signal. - /// The method Execute() returns all info about oscillaitons in the passed sonogram. + /// The method Execute() returns all info about oscillations in the passed sonogram. /// This method should be called in preference to those in the class OscillationAnalysis. - /// (The latter should be depracated.) + /// (The latter should be deprecated.) /// public static class Oscillations2012 { + public static void Execute( + SpectrogramStandard sonogram, + int minHz, + int maxHz, + double dctDuration, + int minOscilFreq, + int maxOscilFreq, + double dctThreshold, + double scoreThreshold, + double minDuration, + double maxDuration, + out double[] scores, + out List events, + out double[,] hits, + TimeSpan segmentStartOffset) + { + int scoreSmoothingWindow = 11; // sets a default that is good for Cane toad but not necessarily for other recognizers + + Execute(sonogram, minHz, maxHz, dctDuration, minOscilFreq, maxOscilFreq, dctThreshold, scoreThreshold, + minDuration, maxDuration, scoreSmoothingWindow, + out scores, out events, out hits, + segmentStartOffset); + } + public static void Execute( SpectrogramStandard sonogram, int minHz, @@ -42,10 +66,6 @@ public static void Execute( //DETECT OSCILLATIONS hits = DetectOscillations(sonogram, minHz, maxHz, dctDuration, minOscilFreq, maxOscilFreq, dctThreshold); - - // debug - ////var sum = hits.Fold((x, y) => x + y, 0.0); - if (hits == null) { LoggedConsole.WriteLine("###### WARNING: DCT length too short to detect the maxOscilFreq"); @@ -76,50 +96,24 @@ public static void Execute( segmentStartOffset); } - public static void Execute( - SpectrogramStandard sonogram, - int minHz, - int maxHz, - double dctDuration, - int minOscilFreq, - int maxOscilFreq, - double dctThreshold, - double scoreThreshold, - double minDuration, - double maxDuration, - out double[] scores, - out List events, - out double[,] hits, - TimeSpan segmentStartOffset) - { - int scoreSmoothingWindow = 11; // sets a default that is good for Canetoad but not necessarily for other recognisers - - Execute(sonogram, minHz, maxHz, dctDuration, minOscilFreq, maxOscilFreq, dctThreshold, scoreThreshold, - minDuration, maxDuration, scoreSmoothingWindow, - out scores, out events, out hits, - segmentStartOffset); - } - /// /// Detects oscillations in a given freq bin. /// there are several important parameters for tuning. /// a) DCTLength: Good values are 0.25 to 0.50 sec. Do not want too long because DCT requires stationarity. /// Do not want too short because too small a range of oscillations - /// b) DCTindex: Sets lower bound for oscillations of interest. Index refers to array of coeff returned by DCT. + /// b) DCTindex: Sets lower bound for oscillations of interest. Index refers to array of coefficient returned by DCT. /// Array has same length as the length of the DCT. Low freq oscillations occur more often by chance. Want to exclude them. /// c) MinAmplitude: minimum acceptable value of a DCT coefficient if hit is to be accepted. /// The algorithm is sensitive to this value. A lower value results in more oscillation hits being returned. /// - /// - /// min freq bin of search band - /// max freq bin of search band - /// number of values - /// - /// threshold - do not accept a DCT coefficient if its value is less than this threshold - /// - /// - public static double[,] DetectOscillations(SpectrogramStandard sonogram, int minHz, int maxHz, - double dctDuration, int minOscilFreq, int maxOscilFreq, double dctThreshold) + /// A spectrogram. + /// min freq bin of search band. + /// max freq bin of search band. + /// number of values. + /// minimum oscillation freq. + /// maximum oscillation freq. + /// threshold - do not accept a DCT coefficient if its value is less than this threshold. + public static double[,] DetectOscillations(SpectrogramStandard sonogram, int minHz, int maxHz,double dctDuration, int minOscilFreq, int maxOscilFreq, double dctThreshold) { int minBin = (int)(minHz / sonogram.FBinWidth); int maxBin = (int)(maxHz / sonogram.FBinWidth); @@ -130,9 +124,10 @@ public static void Execute( int midOscilFreq = minOscilFreq + ((maxOscilFreq - minOscilFreq) / 2); + //safety check if (maxIndex > dctLength) { - return null; //safety check + return null; } int rows = sonogram.Data.GetLength(0); @@ -150,7 +145,8 @@ public static void Execute( //string bmpPath = @"C:\SensorNetworks\Output\cosines.png"; //ImageTools.DrawMatrix(cosines, bmpPath, true); - for (int c = minBin; c <= maxBin; c++) //traverse columns - skip DC column + //traverse columns - skip DC column + for (int c = minBin; c <= maxBin; c++) { var dctArray = new double[dctLength]; @@ -162,10 +158,9 @@ public static void Execute( dctArray[i] = matrix[r + i, c]; } - dctArray = DataTools.SubtractMean(dctArray); - //dctArray = DataTools.Vector2Zscores(dctArray); + dctArray = DataTools.SubtractMean(dctArray); double[] dctCoeff = MFCCStuff.DCT(dctArray, cosines); // convert to absolute values because not interested in negative values due to phase. @@ -190,7 +185,7 @@ public static void Execute( // #### Tried this option for scoring oscillation hits but did not work well. // #### Requires very fine tuning of thresholds - //dctCoeff = DataTools.Normalise2Probabilites(dctCoeff); + //dctCoeff = DataTools.Normalise2Probabilities(dctCoeff); //// sum area under curve where looking for oscillations //double sum = 0.0; //for (int i = minIndex; i <= maxIndex; i++) @@ -202,8 +197,8 @@ public static void Execute( // DEBUGGING // DataTools.MinMax(dctCoeff, out min, out max); - //DataTools.writeBarGraph(dctArray); - //DataTools.writeBarGraph(dctCoeff); + //DataTools.writeBarGraph(dctArray); + //DataTools.writeBarGraph(dctCoeff); //mark DCT location with oscillation freq, only if oscillation freq is in correct range and amplitude if (indexOfMaxValue >= minIndex && indexOfMaxValue <= maxIndex && dctCoeff[indexOfMaxValue] > dctThreshold) @@ -223,115 +218,20 @@ public static void Execute( return hits; } - public static double[] DetectOscillations(double[] ipArray, double framesPerSecond, double dctDuration, double minOscilFreq, double maxOscilFreq, double dctThreshold) - { - int dctLength = (int)Math.Round(framesPerSecond * dctDuration); - int minIndex = (int)(minOscilFreq * dctDuration * 2); //multiply by 2 because index = Pi and not 2Pi - int maxIndex = (int)(maxOscilFreq * dctDuration * 2); //multiply by 2 because index = Pi and not 2Pi - - //double midOscilFreq = minOscilFreq + ((maxOscilFreq - minOscilFreq) / 2); - - if (maxIndex > dctLength) - { - return null; //safety check - } - - int length = ipArray.Length; - var dctScores = new double[length]; - - //var hits = new double[length]; - - double[,] cosines = MFCCStuff.Cosines(dctLength, dctLength); //set up the cosine coefficients - - //following two lines write bmp image of cosine matrix values for checking. - //string bmpPath = @"C:\SensorNetworks\Output\cosines.png"; - //ImageTools.DrawMatrix(cosines, bmpPath, true); - - for (int r = 1; r < length - dctLength; r++) - { - // only stop if current location is a peak - if (ipArray[r] < ipArray[r - 1] || ipArray[r] < ipArray[r + 1]) - { - continue; - } - - // extract array and ready for DCT - //for (int i = 0; i < dctLength; i++) dctArray[i] = ipArray[r + i]; - var dctArray = DataTools.Subarray(ipArray, r, dctLength); - - dctArray = DataTools.SubtractMean(dctArray); - - //dctArray = DataTools.Vector2Zscores(dctArray); - - double[] dctCoeff = MFCCStuff.DCT(dctArray, cosines); - - // convert to absolute values because not interested in negative values due to phase. - for (int i = 0; i < dctLength; i++) - { - dctCoeff[i] = Math.Abs(dctCoeff[i]); - } - - // remove low freq oscillations from consideration - int thresholdIndex = minIndex / 4; - for (int i = 0; i < thresholdIndex; i++) - { - dctCoeff[i] = 0.0; - } - - dctCoeff = DataTools.normalise2UnitLength(dctCoeff); - - //dct = DataTools.NormaliseMatrixValues(dct); //another option to NormaliseMatrixValues - int indexOfMaxValue = DataTools.GetMaxIndex(dctCoeff); - - //double oscilFreq = indexOfMaxValue / dctDuration * 0.5; //Times 0.5 because index = Pi and not 2Pi - - // #### Tried this option for scoring oscillation hits but did not work well. - // #### Requires very fine tuning of thresholds - //dctCoeff = DataTools.Normalise2Probabilites(dctCoeff); - //// sum area under curve where looking for oscillations - //double sum = 0.0; - //for (int i = minIndex; i <= maxIndex; i++) - // sum += dctCoeff[i]; - //if (sum > dctThreshold) - //{ - // for (int i = 0; i < dctLength; i++) hits[r + i, c] = midOscilFreq; - //} - - // DEBUGGING - // DataTools.MinMax(dctCoeff, out min, out max); - //DataTools.writeBarGraph(dctArray); - //DataTools.writeBarGraph(dctCoeff); - - //mark DCT location with oscillation freq, only if oscillation freq is in correct range and amplitude - if (indexOfMaxValue >= minIndex && indexOfMaxValue <= maxIndex && dctCoeff[indexOfMaxValue] > dctThreshold) - { - //for (int i = 0; i < dctLength; i++) dctScores[r + i] = midOscilFreq; - for (int i = 0; i < dctLength; i++) - { - if (dctScores[r + i] < dctCoeff[indexOfMaxValue]) - { - dctScores[r + i] = dctCoeff[indexOfMaxValue]; - } - } - } - } - - //return hits; //dctArray - return dctScores; - } - /// /// Removes single lines of hits from Oscillation matrix. /// - /// the Oscillation matrix - /// + /// the Oscillation matrix. + /// a matrix. public static double[,] RemoveIsolatedOscillations(double[,] matrix) { int rows = matrix.GetLength(0); int cols = matrix.GetLength(1); double[,] cleanMatrix = matrix; const double tolerance = double.Epsilon; - for (int c = 3; c < cols - 3; c++) //traverse columns - skip DC column + + //traverse columns - skip DC column + for (int c = 3; c < cols - 3; c++) { for (int r = 0; r < rows; r++) { @@ -340,7 +240,8 @@ public static double[] DetectOscillations(double[] ipArray, double framesPerSeco continue; } - if (Math.Abs(matrix[r, c - 2]) < tolerance && Math.Abs(matrix[r, c + 2]) < tolerance) //+2 because alternate columns + //+2 because alternate columns + if (Math.Abs(matrix[r, c - 2]) < tolerance && Math.Abs(matrix[r, c + 2]) < tolerance) { cleanMatrix[r, c] = 0.0; } @@ -348,29 +249,31 @@ public static double[] DetectOscillations(double[] ipArray, double framesPerSeco } return cleanMatrix; - } //end method RemoveIsolatedOscillations() + } /// - /// Converts the hits derived from the oscilation detector into a score for each frame. + /// Converts the hits derived from the oscillation detector into a score for each frame. /// NOTE: The oscillation detector skips every second row, so score must be adjusted for this. /// - /// sonogram as matrix showing location of oscillation hits - /// lower freq bound of the acoustic event - /// upper freq bound of the acoustic event - /// the freq scale required by AcousticEvent class - /// + /// sonogram as matrix showing location of oscillation hits. + /// lower freq bound of the acoustic event. + /// upper freq bound of the acoustic event. + /// the freq scale required by AcousticEvent class. public static double[] GetOscillationScores(double[,] hits, int minHz, int maxHz, double freqBinWidth) { int rows = hits.GetLength(0); int minBin = (int)(minHz / freqBinWidth); int maxBin = (int)(maxHz / freqBinWidth); int binCount = maxBin - minBin + 1; - double hitRange = binCount * 0.5 * 0.8; //set hit range slightly < half the bins. Half because only scan every second bin. + + //set hit range slightly < half the bins. Half because only scan every second bin. + double hitRange = binCount * 0.5 * 0.8; var scores = new double[rows]; for (int r = 0; r < rows; r++) { + //traverse columns in required band int score = 0; - for (int c = minBin; c <= maxBin; c++) //traverse columns in required band + for (int c = minBin; c <= maxBin; c++) { if (hits[r, c] > 0) { @@ -378,7 +281,8 @@ public static double[] GetOscillationScores(double[,] hits, int minHz, int maxHz } } - scores[r] = score / hitRange; //NormaliseMatrixValues the hit score in [0,1] + //Normalize the Matrix Values the hit score in [0,1] + scores[r] = score / hitRange; if (scores[r] > 1.0) { scores[r] = 1.0; @@ -386,7 +290,7 @@ public static double[] GetOscillationScores(double[,] hits, int minHz, int maxHz } return scores; - }//end method GetODScores() + } public static double[] GetOscillationFrequency(double[,] hits, int minHz, int maxHz, double freqBinWidth) { @@ -394,14 +298,15 @@ public static double[] GetOscillationFrequency(double[,] hits, int minHz, int ma int minBin = (int)(minHz / freqBinWidth); int maxBin = (int)(maxHz / freqBinWidth); - //int binCount = maxBin - minBin + 1; - - var oscFreq = new double[rows]; //to store the oscillation frequency + //to store the oscillation frequency + var oscFreq = new double[rows]; for (int r = 0; r < rows; r++) { double freq = 0; int count = 0; - for (int c = minBin; c <= maxBin; c++) //traverse columns in required band + + //traverse columns in required band + for (int c = minBin; c <= maxBin; c++) { if (hits[r, c] > 0) { @@ -416,30 +321,28 @@ public static double[] GetOscillationFrequency(double[,] hits, int minHz, int ma } else { - oscFreq[r] = freq / count; //return the average frequency + //return the average frequency + oscFreq[r] = freq / count; } - - //if (oscFreq[r] > 1.0) oscFreq[r] = 1.0; } return oscFreq; - }//end method GetODFrequency() + } /// /// Converts the Oscillation Detector score array to a list of AcousticEvents. /// - /// the array of OD scores - /// - /// lower freq bound of the acoustic event - /// upper freq bound of the acoustic event - /// the time scale required by AcousticEvent class - /// the freq scale required by AcousticEvent class - /// - /// - /// - /// name of source file to be added to AcousticEvent class - /// - /// + /// the array of OD scores. + /// oscillation freq. + /// lower freq bound of the acoustic event. + /// upper freq bound of the acoustic event. + /// the time scale required by AcousticEvent class. + /// the freq scale required by AcousticEvent class. + /// threshold. + /// min threshold. + /// max threshold. + /// name of source file to be added to AcousticEvent class. + /// time offset. public static List ConvertOscillationScores2Events( double[] scores, double[] oscFreq, @@ -467,79 +370,75 @@ public static List ConvertOscillationScores2Events( double startTime = 0.0; int startFrame = 0; - for (int i = 0; i < count; i++) //pass over all frames + //pass over all frames + for (int i = 0; i < count; i++) { - if (isHit == false && scores[i] >= scoreThreshold) //start of an event + if (isHit == false && scores[i] >= scoreThreshold) { + //start of an event isHit = true; startTime = i * frameOffset; startFrame = i; } else //check for the end of an event - if (isHit && (scores[i] < scoreThreshold || i == count - 1)) //this is end of an event, so initialise it - { - isHit = false; + if (isHit && (scores[i] < scoreThreshold || i == count - 1)) + { + isHit = false; - //double endTime = i * frameOffset; - //double duration = endTime - startTime; - double duration = (i - startFrame + 1) * frameOffset; - if (duration < minDurationThreshold) + //double endTime = i * frameOffset; + //double duration = endTime - startTime; + double duration = (i - startFrame + 1) * frameOffset; + if (duration < minDurationThreshold) { continue; //skip events with duration shorter than threshold } - if (duration > maxDurationThreshold) + if (duration > maxDurationThreshold) { continue; //skip events with duration longer than threshold } - var ev = new AcousticEvent(segmentStartOffset, startTime, duration, minHz, maxHz); - ev.Name = "Oscillation"; //default name + //this is end of an event, so initialise it + var ev = new AcousticEvent(segmentStartOffset, startTime, duration, minHz, maxHz) + { + Name = "Oscillation", //default name + FileName = fileName, + }; - //ev.SetTimeAndFreqScales(framesPerSec, freqBinWidth); - ev.FileName = fileName; + ev.SetTimeAndFreqScales(framesPerSec, freqBinWidth); - //obtain average score. - double av = 0.0; - for (int n = startFrame; n <= i; n++) + //obtain average score. + double av = 0.0; + for (int n = startFrame; n <= i; n++) { av += scores[n]; } - ev.Score = av / (i - startFrame + 1); + ev.Score = av / (i - startFrame + 1); - //obtain oscillation freq. - av = 0.0; - for (int n = startFrame; n <= i; n++) + //obtain oscillation freq. + av = 0.0; + for (int n = startFrame; n <= i; n++) { av += oscFreq[n]; } - ev.Score2 = av / (i - startFrame + 1); - ev.Intensity = (int)ev.Score2; // store this info for later inclusion in csv file as Event Intensity - events.Add(ev); - } - - //adapt the threshold - //if ((scores[i] >= maxThreshold) && (maxThreshold >= scoreThreshold)) scoreThreshold *= 1.01; - //else - //if ((scores[i] <= minThreshold) && (minThreshold <= scoreThreshold)) scoreThreshold *= 0.95; + ev.Score2 = av / (i - startFrame + 1); + ev.Intensity = (int)ev.Score2; // store this info for later inclusion in csv file as Event Intensity + events.Add(ev); + } } //end of pass over all frames return events; }//end method ConvertODScores2Events() /// - /// Calculates the optimal frame overlap for the given sample rate, frame width and max oscilation or pulse rate. + /// Calculates the optimal frame overlap for the given sample rate, frame width and max oscillation or pulse rate. /// Pulse rate is determined using a DCT and efficient use of the DCT requires that the dominant pulse sit somewhere 3.4 along the array of coefficients. /// - /// - /// - /// - /// - public static double CalculateRequiredFrameOverlap(int sr, int frameWidth, double maxOscilation) + public static double CalculateRequiredFrameOverlap(int sr, int frameWidth, double maxOscillation) { - double optimumFrameRate = 3 * maxOscilation; //so that max oscillation sits in 3/4 along the array of DCT coefficients + double optimumFrameRate = 3 * maxOscillation; //so that max oscillation sits in 3/4 along the array of DCT coefficients int frameOffset = (int)(sr / optimumFrameRate); // this line added 17 Aug 2016 to deal with high Oscillation rate frog ribits. @@ -551,5 +450,5 @@ public static double CalculateRequiredFrameOverlap(int sr, int frameWidth, doubl double overlap = (frameWidth - frameOffset) / (double)frameWidth; return overlap; } - }//end class -} //AudioAnalysisTools + } +} diff --git a/src/AudioAnalysisTools/Oscillations2019.cs b/src/AudioAnalysisTools/Oscillations2019.cs new file mode 100644 index 000000000..199d1c332 --- /dev/null +++ b/src/AudioAnalysisTools/Oscillations2019.cs @@ -0,0 +1,178 @@ +// +// All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). +// + +namespace AudioAnalysisTools +{ + using System; + using System.Collections.Generic; + using AudioAnalysisTools.DSP; + using AudioAnalysisTools.StandardSpectrograms; + using TowseyLibrary; + + /// + /// NOTE: 26th October 2019. + /// + /// This class contains methods to detect oscillations in a the sonogram of an audio signal. + /// The method Execute() returns all info about oscillations in the passed sonogram. + /// + public static class Oscillations2019 + { + public static void Execute( + SpectrogramStandard sonogram, + int minHz, + int maxHz, + double decibelThreshold, + double dctDuration, + int minOscFreq, + int maxOscFreq, + double dctThreshold, + double scoreThreshold, + double minDuration, + double maxDuration, + int smoothingWindow, + out double[] dctScores, + out List events, + TimeSpan segmentStartOffset) + { + // smooth the frames to make oscillations more regular. + sonogram.Data = MatrixTools.SmoothRows(sonogram.Data, 5); + + // extract array of decibel values, frame averaged over required frequency band + var decibelArray = SNR.CalculateFreqBandAvIntensity(sonogram.Data, minHz, maxHz, sonogram.NyquistFrequency); + + // if first value is negative dB, this means noise removal was not done. + // Do noise removal now + //if (decibelArray[0] < 0.0) + //{ + // NoiseRemovalModal.CalculateNoiseUsingLamelsAlgorithm(decibelArray, out double _, out double _, out double noiseMode, out double _); + // decibelArray = SNR.SubtractAndTruncate2Zero(decibelArray, noiseMode); + //} + + //DETECT OSCILLATIONS + var framesPerSecond = sonogram.FramesPerSecond; + DetectOscillations( + decibelArray, + framesPerSecond, + decibelThreshold, + dctDuration, + minOscFreq, + maxOscFreq, + dctThreshold, + out dctScores, + out var oscFreq); + + // smooth the scores - window=11 has been the DEFAULT. Now letting user set this. + dctScores = DataTools.filterMovingAverage(dctScores, smoothingWindow); + + //double midOscFreq = minOscFreq + ((maxOscFreq - minOscFreq) / 2); + events = Oscillations2012.ConvertOscillationScores2Events( + dctScores, + oscFreq, + minHz, + maxHz, + sonogram.FramesPerSecond, + sonogram.FBinWidth, + scoreThreshold, + minDuration, + maxDuration, + sonogram.Configuration.SourceFName, + segmentStartOffset); + } + + /// + /// Currently this method is called by only one species recognizer - LitoriaCaerulea. + /// + /// an array of decibel values. + /// the frame rate. + /// Ignore frames below this threshold. + /// Duration in seconds of the required DCT. + /// minimum oscillation frequency. + /// maximum oscillation frequency. + /// Threshold for the maximum DCT coefficient. + /// an array of dct scores. + /// an array of oscillation frequencies. + public static void DetectOscillations( + double[] ipArray, + double framesPerSecond, + double decibelThreshold, + double dctDuration, + double minOscFreq, + double maxOscFreq, + double dctThreshold, + out double[] dctScores, + out double[] oscFreq) + { + int dctLength = (int)Math.Round(framesPerSecond * dctDuration); + int minIndex = (int)(minOscFreq * dctDuration * 2); //multiply by 2 because index = Pi and not 2Pi + int maxIndex = (int)(maxOscFreq * dctDuration * 2); //multiply by 2 because index = Pi and not 2Pi + if (maxIndex > dctLength) + { + LoggedConsole.WriteWarnLine("MaxIndex > DCT length. Therefore set maxIndex = DCT length."); + maxIndex = dctLength; + } + + int length = ipArray.Length; + dctScores = new double[length]; + oscFreq = new double[length]; + + //set up the cosine coefficients + double[,] cosines = MFCCStuff.Cosines(dctLength, dctLength); + + //following two lines write bmp image of cosine matrix values for checking. + //string bmpPath = @"C:\SensorNetworks\Output\cosines.png"; + //ImageTools.DrawMatrix(cosines, bmpPath, true); + + for (int r = 1; r < length - dctLength; r++) + { + // only stop if current location is a peak + if (ipArray[r] < ipArray[r - 1] || ipArray[r] < ipArray[r + 1]) + { + continue; + } + + // only stop if current location is a peak + if (ipArray[r] < decibelThreshold) + { + continue; + } + + // extract array and ready for DCT + var dctArray = DataTools.Subarray(ipArray, r, dctLength); + + dctArray = DataTools.SubtractMean(dctArray); + double[] dctCoefficient = MFCCStuff.DCT(dctArray, cosines); + + // convert to absolute values because not interested in negative values due to phase. + for (int i = 0; i < dctLength; i++) + { + dctCoefficient[i] = Math.Abs(dctCoefficient[i]); + } + + // remove low freq oscillations from consideration + int thresholdIndex = minIndex / 4; + for (int i = 0; i < thresholdIndex; i++) + { + dctCoefficient[i] = 0.0; + } + + dctCoefficient = DataTools.normalise2UnitLength(dctCoefficient); + + int indexOfMaxValue = DataTools.GetMaxIndex(dctCoefficient); + + //mark DCT location with oscillation freq, only if oscillation freq is in correct range and amplitude + if (indexOfMaxValue >= minIndex && indexOfMaxValue <= maxIndex && dctCoefficient[indexOfMaxValue] > dctThreshold) + { + for (int i = 0; i < dctLength; i++) + { + if (dctScores[r + i] < dctCoefficient[indexOfMaxValue]) + { + dctScores[r + i] = dctCoefficient[indexOfMaxValue]; + oscFreq[r + i] = indexOfMaxValue / dctDuration / 2; + } + } + } + } + } + } +} diff --git a/src/TowseyLibrary/PulseTrain.cs b/src/TowseyLibrary/PulseTrain.cs index 1579da573..59788d8d3 100644 --- a/src/TowseyLibrary/PulseTrain.cs +++ b/src/TowseyLibrary/PulseTrain.cs @@ -6,7 +6,6 @@ // This class contains methods to recognise pulse trains. // It is an alternative to using the Oscillations class. - using System; using System.Collections.Generic; using System.Linq; @@ -18,8 +17,13 @@ namespace TowseyLibrary using Acoustics.Shared.ConfigFile; using MathNet.Numerics.LinearAlgebra.Solvers; + /// + /// This class was an attempt to detect pulse trains as an alternative to using the Oscillation recognition methods. + /// It did not work effectively so discontinued the idea and have commented out the three methods. + /// public static class PulseTrain { + /* /// /// This method creates a template to recognise two pulses that are possibly part of a pulse train. /// The template is designed to detect pulse trains of at least 2 pulses! @@ -157,5 +161,6 @@ public static double[] GetPulseTrainScore(double[] signal, double pulsesPerSecon return scores; } + */ } } diff --git a/tests/Acoustics.Test/Acoustics.Test.csproj b/tests/Acoustics.Test/Acoustics.Test.csproj index 24f6dbafe..4d9317438 100644 --- a/tests/Acoustics.Test/Acoustics.Test.csproj +++ b/tests/Acoustics.Test/Acoustics.Test.csproj @@ -307,6 +307,7 @@ + diff --git a/tests/Acoustics.Test/AnalysisPrograms/Recognizers/PteropusSp/PteropusSpTests.cs b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/PteropusSp/PteropusSpTests.cs new file mode 100644 index 000000000..6b37bf0d9 --- /dev/null +++ b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/PteropusSp/PteropusSpTests.cs @@ -0,0 +1,159 @@ +// +// All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). +// + +namespace Acoustics.Test.AnalysisPrograms.Recognizers.PteropusSp +{ + using System; + using System.Collections.Generic; + using System.Drawing; + using System.IO; + using System.Linq; + using Acoustics.Shared; + using Acoustics.Shared.ConfigFile; + using Acoustics.Test.TestHelpers; + using Acoustics.Tools.Wav; + using global::AudioAnalysisTools; + using global::AudioAnalysisTools.DSP; + using global::AudioAnalysisTools.StandardSpectrograms; + using global::AudioAnalysisTools.WavTools; + using global::TowseyLibrary; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class PteropusSpTests + { + private DirectoryInfo outputDirectory; + private AudioRecording audioRecording; + private BaseSonogram sonogram; + + /// + /// The one-minute recording used for these tests was originally recorded at 40kHz in two channels. + /// It was resampled to 22050 in one channel using Audacity for these tests. + /// The number of wing-beat and call events is somewhat sensitive to parameter settings. + /// With test settings get and extra call event. + /// + [TestInitialize] + public void Setup() + { + this.outputDirectory = PathHelper.GetTempDir(); + this.audioRecording = new AudioRecording(PathHelper.ResolveAsset("Recordings", "20190115_Bellingen_Feeding_minute6_OneChannel22050.wav")); + var sonoConfig = new SonogramConfig + { + WindowSize = 512, + NoiseReductionType = NoiseReductionType.Standard, + NoiseReductionParameter = 3.0, + WindowOverlap = 0.0, + }; + this.sonogram = new SpectrogramStandard(sonoConfig, this.audioRecording.WavReader); + } + + [TestCleanup] + public void Cleanup() + { + PathHelper.DeleteTempDir(this.outputDirectory); + } + + [TestMethod] + public void TestGetWingBeatEvents() + { + //string speciesName = "Pteropus species"; + //string abbreviatedSpeciesName = "Pteropus"; + int minHz = 200; + int maxHz = 2000; + double minDurationSeconds = 1.0; + double maxDurationSeconds = 10.0; + double dctDuration = 0.8; + double dctThreshold = 0.5; + double minOscilFreq = 4.0; + double maxOscilFreq = 6.0; + double eventThreshold = 0.6; + TimeSpan segmentStartOffset = TimeSpan.Zero; + + // Look for wing beats using oscillation detector + Oscillations2012.Execute( + (SpectrogramStandard)this.sonogram, + minHz, + maxHz, + dctDuration, + (int)Math.Floor(minOscilFreq), + (int)Math.Floor(maxOscilFreq), + dctThreshold, + eventThreshold, + minDurationSeconds, + maxDurationSeconds, + out var scores, + out var acousticEvents, + out var hits, + segmentStartOffset); + + Assert.AreEqual(4, acousticEvents.Count); + + Assert.AreEqual(5, acousticEvents[0].Oblong.ColumnLeft); + Assert.AreEqual(46, acousticEvents[0].Oblong.ColumnRight); + Assert.AreEqual(1280, acousticEvents[0].Oblong.RowTop); + Assert.AreEqual(1381, acousticEvents[0].Oblong.RowBottom); + + Assert.AreEqual(5, acousticEvents[1].Oblong.ColumnLeft); + Assert.AreEqual(46, acousticEvents[1].Oblong.ColumnRight); + Assert.AreEqual(1762, acousticEvents[1].Oblong.RowTop); + Assert.AreEqual(1826, acousticEvents[1].Oblong.RowBottom); + + Assert.AreEqual(5, acousticEvents[2].Oblong.ColumnLeft); + Assert.AreEqual(46, acousticEvents[2].Oblong.ColumnRight); + Assert.AreEqual(2083, acousticEvents[2].Oblong.RowTop); + Assert.AreEqual(2208, acousticEvents[2].Oblong.RowBottom); + + Assert.AreEqual(5, acousticEvents[3].Oblong.ColumnLeft); + Assert.AreEqual(46, acousticEvents[3].Oblong.ColumnRight); + Assert.AreEqual(2334, acousticEvents[3].Oblong.RowTop); + Assert.AreEqual(2383, acousticEvents[3].Oblong.RowBottom); + + //Assert.AreEqual(0.6062, stats.SpectralEnergyDistribution, 1E-4); + } + + [TestMethod] + public void TestGetEventsAroundMaxima() + { + //string abbreviatedSpeciesName = "Pteropus"; + string speciesName = "Pteropus species"; + int minHz = 800; + int maxHz = 8000; + var minTimeSpan = TimeSpan.FromSeconds(0.15); + var maxTimeSpan = TimeSpan.FromSeconds(0.8); + double decibelThreshold = 9.0; + TimeSpan segmentStartOffset = TimeSpan.Zero; + + var decibelArray = SNR.CalculateFreqBandAvIntensity(this.sonogram.Data, minHz, maxHz, this.sonogram.NyquistFrequency); + + // prepare plots + double intensityNormalisationMax = 3 * decibelThreshold; + var eventThreshold = decibelThreshold / intensityNormalisationMax; + var normalisedIntensityArray = DataTools.NormaliseInZeroOne(decibelArray, 0, intensityNormalisationMax); + var plot = new Plot(speciesName + " Territory", normalisedIntensityArray, eventThreshold); + var plots = new List { plot }; + + //iii: CONVERT decibel SCORES TO ACOUSTIC EVENTS + var acousticEvents = AcousticEvent.GetEventsAroundMaxima( + decibelArray, + segmentStartOffset, + minHz, + maxHz, + decibelThreshold, + minTimeSpan, + maxTimeSpan, + this.sonogram.FramesPerSecond, + this.sonogram.FBinWidth); + + Assert.AreEqual(10, acousticEvents.Count); + + Assert.AreEqual(new Rectangle(19, 1751, 168, 27), acousticEvents[0].GetEventAsRectangle()); + Assert.AreEqual(new Rectangle(19, 1840, 168, 10), acousticEvents[2].GetEventAsRectangle()); + Assert.AreEqual(new Rectangle(19, 1961, 168, 31), acousticEvents[5].GetEventAsRectangle()); + Assert.AreEqual(new Rectangle(19, 2294, 168, 17), acousticEvents[7].GetEventAsRectangle()); + Assert.AreEqual(new Rectangle(19, 2504, 168, 7), acousticEvents[9].GetEventAsRectangle()); + + //Assert.AreEqual(28.Seconds() + segmentOffset, stats.ResultStartSeconds.Seconds()); + } + } +} diff --git a/tests/Acoustics.Test/AudioAnalysisTools/StandardSpectrograms/SonogramTests.cs b/tests/Acoustics.Test/AudioAnalysisTools/StandardSpectrograms/SonogramTests.cs index 00b22f818..f8151b7cd 100644 --- a/tests/Acoustics.Test/AudioAnalysisTools/StandardSpectrograms/SonogramTests.cs +++ b/tests/Acoustics.Test/AudioAnalysisTools/StandardSpectrograms/SonogramTests.cs @@ -4,15 +4,18 @@ namespace Acoustics.Test.AudioAnalysisTools.StandardSpectrograms { + using System; + using System.Collections.Generic; using System.IO; using Acoustics.Shared; - using DSP; + using Acoustics.Test.AudioAnalysisTools.DSP; + using Acoustics.Test.TestHelpers; using global::AudioAnalysisTools; using global::AudioAnalysisTools.DSP; using global::AudioAnalysisTools.StandardSpectrograms; using global::AudioAnalysisTools.WavTools; + using global::TowseyLibrary; using Microsoft.VisualStudio.TestTools.UnitTesting; - using TestHelpers; /// /// Test methods for the various standard Sonograms or Spectrograms @@ -28,6 +31,9 @@ public class SonogramTests { private const double AllowedDelta = 0.000001; private DirectoryInfo outputDirectory; + private AudioRecording recording; + private FrequencyScale freqScale; + private SonogramConfig sonoConfig; /* // You can use the following additional attributes as you write your tests: @@ -53,12 +59,30 @@ public class SonogramTests public void Setup() { this.outputDirectory = PathHelper.GetTempDir(); + this.recording = new AudioRecording(PathHelper.ResolveAsset("Recordings", "BAC2_20071008-085040.wav")); + + // specified linear scale + this.freqScale = new FrequencyScale(nyquist: 11025, frameSize: 1024, hertzGridInterval: 1000); + + // set up the config for each spectrogram + this.sonoConfig = new SonogramConfig + { + WindowSize = this.freqScale.FinalBinCount * 2, + WindowOverlap = 0.2, + SourceFName = this.recording.BaseName, + NoiseReductionType = NoiseReductionType.None, + NoiseReductionParameter = 0.0, + }; } [TestCleanup] public void Cleanup() { PathHelper.DeleteTempDir(this.outputDirectory); + this.recording.Dispose(); + + //this.freqScale.(); + //this.sonoConfig.Dispose(); } /// @@ -88,23 +112,9 @@ public void TestAverageOfDecibelValues() [TestMethod] public void TestAmplitudeSonogram() { - var recording = new AudioRecording(PathHelper.ResolveAsset("Recordings", "BAC2_20071008-085040.wav")); - - // specfied linear scale - var freqScale = new FrequencyScale(nyquist: 11025, frameSize: 1024, hertzGridInterval: 1000); - - var sonoConfig = new SonogramConfig - { - WindowSize = freqScale.FinalBinCount * 2, - WindowOverlap = 0.2, - SourceFName = recording.BaseName, - NoiseReductionType = NoiseReductionType.None, - NoiseReductionParameter = 0.0, - }; - // DO EQUALITY TEST on the AMPLITUDE SONGOGRAM DATA // Do not bother with the image because this is only an amplitude spectrogram. - var sonogram = new AmplitudeSonogram(sonoConfig, recording.WavReader); + var sonogram = new AmplitudeSonogram(this.sonoConfig, this.recording.WavReader); var expectedFile = PathHelper.ResolveAsset("StandardSonograms", "BAC2_20071008_AmplSonogramData.EXPECTED.bin"); // run this once to generate expected test data @@ -120,23 +130,9 @@ public void TestAmplitudeSonogram() [TestMethod] public void TestDecibelSpectrogram() { - var recording = new AudioRecording(PathHelper.ResolveAsset("Recordings", "BAC2_20071008-085040.wav")); - - // specfied linear scale - var freqScale = new FrequencyScale(nyquist: 11025, frameSize: 1024, hertzGridInterval: 1000); - - var sonoConfig = new SonogramConfig - { - WindowSize = freqScale.FinalBinCount * 2, - WindowOverlap = 0.2, - SourceFName = recording.BaseName, - NoiseReductionType = NoiseReductionType.None, - NoiseReductionParameter = 0.0, - }; - // DO EQUALITY TEST on the AMPLITUDE SONGOGRAM DATA // Do not bother with the image because this is only an amplitude spectrogram. - var sonogram = new AmplitudeSonogram(sonoConfig, recording.WavReader); + var sonogram = new AmplitudeSonogram(this.sonoConfig, this.recording.WavReader); // DO FILE EQUALITY TEST on the DECIBEL SONGOGRAM DATA // Do not bother with the image because this has been tested elsewhere. @@ -157,28 +153,69 @@ public void TestDecibelSpectrogram() [TestMethod] public void SonogramDecibelMethodsAreEquivalent() { - var recording = new AudioRecording(PathHelper.ResolveAsset("Recordings", "BAC2_20071008-085040.wav")); + // Method 1 + var sonogram = new AmplitudeSonogram(this.sonoConfig, this.recording.WavReader); + var expectedDecibelSonogram = MFCCStuff.DecibelSpectra(sonogram.Data, sonogram.Configuration.WindowPower, sonogram.SampleRate, sonogram.Configuration.epsilon); + + // Method 2: make sure that the decibel spectrum is the same no matter which path we take to calculate it. + var actualDecibelSpectrogram = new SpectrogramStandard(this.sonoConfig, this.recording.WavReader); - // specfied linear scale - var freqScale = new FrequencyScale(nyquist: 11025, frameSize: 1024, hertzGridInterval: 1000); + CollectionAssert.That.AreEqual(expectedDecibelSonogram, actualDecibelSpectrogram.Data, EnvelopeAndFftTests.Delta); + } - var sonoConfig = new SonogramConfig + [TestMethod] + public void TestAnnotatedSonogramWithPlots() + { + // Make a decibel spectrogram + var actualDecibelSpectrogram = new SpectrogramStandard(this.sonoConfig, this.recording.WavReader); + + // prepare normalisation bounds for three plots + double minDecibels = -100.0; + double maxDecibels = -50; + + //double decibelThreshold = 12.5 dB above -100 dB; + var normThreshold = 0.25; + + //plot 1 + int minHz = 2000; + int maxHz = 3000; + var decibelArray = SNR.CalculateFreqBandAvIntensity(actualDecibelSpectrogram.Data, minHz, maxHz, actualDecibelSpectrogram.NyquistFrequency); + var normalisedIntensityArray = DataTools.NormaliseInZeroOne(decibelArray, minDecibels, maxDecibels); + var plot1 = new Plot("Intensity 2-3 kHz", normalisedIntensityArray, normThreshold); + + //plot 2 + minHz = 3000; + maxHz = 4000; + decibelArray = SNR.CalculateFreqBandAvIntensity(actualDecibelSpectrogram.Data, minHz, maxHz, actualDecibelSpectrogram.NyquistFrequency); + normalisedIntensityArray = DataTools.NormaliseInZeroOne(decibelArray, minDecibels, maxDecibels); + var plot2 = new Plot("Intensity 3-4 kHz", normalisedIntensityArray, normThreshold); + + //plot 3 + minHz = 4000; + maxHz = 5000; + decibelArray = SNR.CalculateFreqBandAvIntensity(actualDecibelSpectrogram.Data, minHz, maxHz, actualDecibelSpectrogram.NyquistFrequency); + normalisedIntensityArray = DataTools.NormaliseInZeroOne(decibelArray, minDecibels, maxDecibels); + var plot3 = new Plot("Intensity 4-5 kHz", normalisedIntensityArray, normThreshold); + + // combine the plots + var plots = new List { plot1, plot2, plot3 }; + + // create three events + var startOffset = TimeSpan.Zero; + var events = new List { - WindowSize = freqScale.FinalBinCount * 2, - WindowOverlap = 0.2, - SourceFName = recording.BaseName, - NoiseReductionType = NoiseReductionType.None, - NoiseReductionParameter = 0.0, + new AcousticEvent(startOffset, 10.0, 10.0, 2000, 3000), + new AcousticEvent(startOffset, 25.0, 10.0, 3000, 4000), + new AcousticEvent(startOffset, 40.0, 10.0, 4000, 5000), }; - // Method 1 - var sonogram = new AmplitudeSonogram(sonoConfig, recording.WavReader); - var expectedDecibelSonogram = MFCCStuff.DecibelSpectra(sonogram.Data, sonogram.Configuration.WindowPower, sonogram.SampleRate, sonogram.Configuration.epsilon); + var image = SpectrogramTools.GetSonogramPlusCharts(actualDecibelSpectrogram, events, plots, null); - // Method 2: make sure that the decibel spectrum is the same no matter which path we take to calculate it. - var actualDecibelSpectrogram = new SpectrogramStandard(sonoConfig, recording.WavReader); + // create the image for visual confirmation + image.Save(Path.Combine(this.outputDirectory.FullName, this.recording.BaseName + ".png")); - CollectionAssert.That.AreEqual(expectedDecibelSonogram, actualDecibelSpectrogram.Data, EnvelopeAndFftTests.Delta); + Assert.AreEqual(1621, image.Width); + Assert.AreEqual(647, image.Height); } } } diff --git a/tests/Fixtures/Recordings/20190115_Bellingen_Feeding_minute6_OneChannel22050.wav b/tests/Fixtures/Recordings/20190115_Bellingen_Feeding_minute6_OneChannel22050.wav new file mode 100644 index 000000000..dd8d2b700 --- /dev/null +++ b/tests/Fixtures/Recordings/20190115_Bellingen_Feeding_minute6_OneChannel22050.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a98f3b2e5009318e2867172b7c5b9802f625c8c03d585a4134bf2ed03b191644 +size 2647962