diff --git a/src/AnalysisPrograms/Recognizers/Base/MultiRecognizer.cs b/src/AnalysisPrograms/Recognizers/Base/MultiRecognizer.cs index c0baa6254..c578a315c 100644 --- a/src/AnalysisPrograms/Recognizers/Base/MultiRecognizer.cs +++ b/src/AnalysisPrograms/Recognizers/Base/MultiRecognizer.cs @@ -64,15 +64,15 @@ public override RecognizerResults Recognize(AudioRecording audioRecording, Confi MultiRecognizerConfig multiRecognizerConfig = (MultiRecognizerConfig)configuration; // make a standard spectrogram in which to render acoustic events and to append score tracks - // currently using Hamming window. Worth trying Hanning Window var config = new SonogramConfig { + WindowSize = 512, + WindowFunction = WindowFunctions.HANNING.ToString(), NoiseReductionType = NoiseReductionType.Standard, NoiseReductionParameter = 0.1, - WindowSize = 512, }; - var sonogram = (BaseSonogram)new SpectrogramStandard(config, audioRecording.WavReader); + var spectrogram = (BaseSonogram)new SpectrogramStandard(config, audioRecording.WavReader); var scoreTracks = new List>(); var plots = new List(); var events = new List(); @@ -103,8 +103,8 @@ public override RecognizerResults Recognize(AudioRecording audioRecording, Confi newEvents.AddRange(output.NewEvents); - // rescale scale of plots - output.Plots.ForEach(p => p.ScaleDataArray(sonogram.FrameCount)); + // rescale all the plots to fit the current spectrogram. + output.Plots.ForEach(p => p.ScaleDataArray(spectrogram.FrameCount)); plots.AddRange(output.Plots); } @@ -117,7 +117,7 @@ public override RecognizerResults Recognize(AudioRecording audioRecording, Confi Events = events, NewEvents = newEvents, ScoreTrack = scoreTrackImage, - Sonogram = sonogram, + Sonogram = spectrogram, Plots = plots, Hits = null, }; diff --git a/src/AnalysisPrograms/Recognizers/Base/RecognizerBase.cs b/src/AnalysisPrograms/Recognizers/Base/RecognizerBase.cs index 6e9c72bfa..88515807f 100644 --- a/src/AnalysisPrograms/Recognizers/Base/RecognizerBase.cs +++ b/src/AnalysisPrograms/Recognizers/Base/RecognizerBase.cs @@ -380,7 +380,7 @@ protected virtual Image DrawSonogram( List predictedEvents, double eventThreshold) { - var image = SpectrogramTools.GetSonogramPlusCharts(sonogram, predictedEvents, scores, hits); + var image = SpectrogramTools.GetSonogramPlusCharts(sonogram, predictedEvents, scores, hits, sonogram.Configuration.SourceFName); return image; } diff --git a/src/AnalysisPrograms/Recognizers/Frogs/LitoriaFallax.cs b/src/AnalysisPrograms/Recognizers/Frogs/LitoriaFallax.cs index f064e8999..8d0cb2f97 100644 --- a/src/AnalysisPrograms/Recognizers/Frogs/LitoriaFallax.cs +++ b/src/AnalysisPrograms/Recognizers/Frogs/LitoriaFallax.cs @@ -196,7 +196,7 @@ private void WriteDebugImage( bool displayDebugImage = MainEntry.InDEBUG; if (displayDebugImage) { - Image debugImage1 = SpectrogramTools.GetSonogramPlusCharts(sonogram, acousticEvents, plots, hits); + Image debugImage1 = SpectrogramTools.GetSonogramPlusCharts(sonogram, acousticEvents, plots, hits, sonogram.Configuration.SourceFName); var debugPath1 = outputDirectory.Combine( FilenameHelpers.AnalysisResultName( @@ -226,7 +226,7 @@ private void WriteDebugImage( this.Identifier, "png", "DebugSpectrogram2")); - Image debugImage2 = SpectrogramTools.GetSonogramPlusCharts(sonogram2, acousticEvents, plots, null); + Image debugImage2 = SpectrogramTools.GetSonogramPlusCharts(sonogram2, acousticEvents, plots, null, sonogram.Configuration.SourceFName); debugImage2.Save(debugPath2.FullName); } } diff --git a/src/AnalysisPrograms/Recognizers/Frogs/LitoriaFreycineti.cs b/src/AnalysisPrograms/Recognizers/Frogs/LitoriaFreycineti.cs index dfe80c4ca..d3cf54100 100644 --- a/src/AnalysisPrograms/Recognizers/Frogs/LitoriaFreycineti.cs +++ b/src/AnalysisPrograms/Recognizers/Frogs/LitoriaFreycineti.cs @@ -190,7 +190,7 @@ private void WriteDebugImage( bool displayDebugImage = MainEntry.InDEBUG; if (displayDebugImage) { - Image debugImage1 = SpectrogramTools.GetSonogramPlusCharts(sonogram, acousticEvents, plots, hits); + Image debugImage1 = SpectrogramTools.GetSonogramPlusCharts(sonogram, acousticEvents, plots, hits, sonogram.Configuration.SourceFName); var debugPath1 = outputDirectory.Combine( FilenameHelpers.AnalysisResultName( @@ -220,7 +220,7 @@ private void WriteDebugImage( this.Identifier, "png", "DebugSpectrogram2")); - Image debugImage2 = SpectrogramTools.GetSonogramPlusCharts(sonogram2, acousticEvents, plots, null); + Image debugImage2 = SpectrogramTools.GetSonogramPlusCharts(sonogram2, acousticEvents, plots, null, sonogram2.Configuration.SourceFName); debugImage2.Save(debugPath2.FullName); } } diff --git a/src/AnalysisPrograms/Recognizers/Frogs/LitoriaRothii.cs b/src/AnalysisPrograms/Recognizers/Frogs/LitoriaRothii.cs index 9a861268a..83b87f8e3 100644 --- a/src/AnalysisPrograms/Recognizers/Frogs/LitoriaRothii.cs +++ b/src/AnalysisPrograms/Recognizers/Frogs/LitoriaRothii.cs @@ -222,7 +222,7 @@ public override RecognizerResults Recognize(AudioRecording recording, Config con var lowPassPlot = new Plot("Low Pass", normalisedScores, normalisedThreshold); var debugPlots = new List { ampltdPlot, lowPassPlot, demeanedPlot, plot }; - Image debugImage = SpectrogramTools.GetSonogramPlusCharts(sonogram, acousticEvents, debugPlots, null); + Image debugImage = SpectrogramTools.GetSonogramPlusCharts(sonogram, acousticEvents, debugPlots, null, sonogram.Configuration.SourceFName); var debugPath = outputDirectory.Combine(FilenameHelpers.AnalysisResultName(Path.GetFileNameWithoutExtension(recording.BaseName), this.Identifier, "png", "DebugSpectrogram")); debugImage.Save(debugPath.FullName); } diff --git a/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs b/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs index cc9dcb8b1..32a19ecfa 100644 --- a/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs +++ b/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs @@ -303,6 +303,13 @@ public static RecognizerResults RunProfiles( } } + // Rescale the plots in case profiles used different window/frame lengths. + var lastSpectrogram = combinedResults.Sonogram; + combinedResults.Plots.ForEach(plot => + { + plot.ScaleDataArray(lastSpectrogram.FrameCount); + }); + return combinedResults; } @@ -425,10 +432,11 @@ public static RecognizerResults RunOneProfile( }; //add info about decibel threshold into the event. - //This info is used later during post-processing of events. + //This info is used later during post-processing of events and for drawing of events. foreach (var ev in spectralEvents) { ev.DecibelDetectionThreshold = decibelThreshold.Value; + ev.SegmentDurationSeconds = spectrogram.Duration.TotalSeconds; } allResults.NewEvents.AddRange(spectralEvents); @@ -459,7 +467,7 @@ public override void SummariseResults( /// public static string SaveDebugSpectrogram(RecognizerResults results, Config genericConfig, DirectoryInfo outputDirectory, string baseName) { - var image3 = SpectrogramTools.GetSonogramPlusCharts(results.Sonogram, results.NewEvents, results.Plots, null); + var image3 = SpectrogramTools.GetSonogramPlusCharts(results.Sonogram, results.NewEvents, results.Plots, null, baseName); var path = Path.Combine(outputDirectory.FullName, baseName + ".profile.png"); image3.Save(path); diff --git a/src/AnalysisPrograms/Recognizers/PteropusSpecies.cs b/src/AnalysisPrograms/Recognizers/PteropusSpecies.cs index 82221a2fa..504d60b46 100644 --- a/src/AnalysisPrograms/Recognizers/PteropusSpecies.cs +++ b/src/AnalysisPrograms/Recognizers/PteropusSpecies.cs @@ -464,8 +464,7 @@ internal static BaseSonogram GetSonogram(Config configuration, AudioRecording au /// internal static void SaveDebugSpectrogram(RecognizerResults results, Config genericConfig, DirectoryInfo outputDirectory, string baseName) { - //var image = sonogram.GetImageFullyAnnotated("Test"); - var image = SpectrogramTools.GetSonogramPlusCharts(results.Sonogram, results.Events, results.Plots, null); + var image = SpectrogramTools.GetSonogramPlusCharts(results.Sonogram, results.Events, results.Plots, null, results.Sonogram.Configuration.SourceFName); image.Save(Path.Combine(outputDirectory.FullName, baseName + ".profile.png")); } } diff --git a/src/AudioAnalysisTools/Events/Types/BlobEvent.cs b/src/AudioAnalysisTools/Events/Types/BlobEvent.cs index 6dd0e7fca..68ebf6905 100644 --- a/src/AudioAnalysisTools/Events/Types/BlobEvent.cs +++ b/src/AudioAnalysisTools/Events/Types/BlobEvent.cs @@ -66,8 +66,6 @@ public static (List Events, List DecibelPlots) GetBlobEvents( var events = acEvents.ConvertAcousticEventsToSpectralEvents(); spectralEvents.AddRange(events); - plots.Add(plot); - return (spectralEvents, plots); } diff --git a/src/AudioAnalysisTools/Events/Types/OscillationEvent.cs b/src/AudioAnalysisTools/Events/Types/OscillationEvent.cs index bf4e2b7f1..a70a05f46 100644 --- a/src/AudioAnalysisTools/Events/Types/OscillationEvent.cs +++ b/src/AudioAnalysisTools/Events/Types/OscillationEvent.cs @@ -5,9 +5,13 @@ namespace AudioAnalysisTools { using System; + using System.Collections.Generic; using AudioAnalysisTools.Events; using AudioAnalysisTools.Events.Drawing; + using AudioAnalysisTools.StandardSpectrograms; + using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; + using TowseyLibrary; public class OscillationEvent : SpectralEvent { @@ -17,17 +21,82 @@ public OscillationEvent() // TODO: add extra metadata!!! + /// + /// Gets or sets the period in seconds between consecutive high points in an oscillation event. + /// + public double Periodicity { get; set; } + + /// + /// Draws a border around this oscillation event. + /// public override void Draw(IImageProcessingContext graphics, EventRenderingOptions options) { - // foreach (var track in tracks) { - // track.Draw(...) - // } + if (options.DrawBorder) + { + var border = options.Converters.GetPixelRectangle(this); + graphics.NoAA().DrawBorderInset(options.Border, border); + } + + this.DrawScoreIndicator(graphics, options); + this.DrawEventLabel(graphics, options); + } + + /// + /// Extracts an event from a spectrogram given its bounds. + /// Then trims the event because oscillation events do not typically start where the DCT places them. + /// It a;sp returns the periodicity of the oscillation event. + /// + public static (int EventStart, int EventEnd, double FramePeriod) TrimEvent(SpectrogramStandard spectrogram, int startFrame, int minBin, int endFrame, int maxBin) + { + //obtain the oscillation event's periodicity. + //extract the relevant portion of the spectrogram. + var eventMatrix = MatrixTools.Submatrix(spectrogram.Data, startFrame, minBin, endFrame, maxBin); + var frameAverages = MatrixTools.GetRowAverages(eventMatrix); + frameAverages = DataTools.normalise(frameAverages); + double threshold = 0.25; + + // find the true start frame + int startFrameOffset = 0; + for (int frame = 1; frame < frameAverages.Length; frame++) + { + startFrameOffset++; + if (frameAverages[frame - 1] < threshold && frameAverages[frame] >= threshold) + { + break; + } + } + + int endFrameOffset = 0; + for (int frame = frameAverages.Length - 1; frame >= 0; frame--) + { + endFrameOffset++; + if (frameAverages[frame - 1] >= threshold && frameAverages[frame] < threshold) + { + break; + } + } + + int trueStartFrame = startFrame + startFrameOffset; + int trueEndFrame = endFrame - endFrameOffset; + + // determine the number of times the frame values step from below to above threshold. + // also the frame index in which the steps happen. + int stepCount = 0; + var peakOnsets = new List(); + for (int frame = 1; frame < frameAverages.Length; frame++) + { + if (frameAverages[frame - 1] < threshold && frameAverages[frame] >= threshold) + { + stepCount++; + peakOnsets.Add(frame); + } + } - //this.Track.Draw(graphics, options); + // calculate the length of a whole number of complete periods. + int framePeriods = peakOnsets[peakOnsets.Count - 1] - peakOnsets[0]; + double framePeriod = framePeriods / (double)(stepCount - 1); - // base drawing (border) - // TODO: unless border is disabled - base.Draw(graphics, options); + return (trueStartFrame, trueEndFrame, framePeriod); } } } \ No newline at end of file diff --git a/src/AudioAnalysisTools/Ocillations/Oscillations2012.cs b/src/AudioAnalysisTools/Ocillations/Oscillations2012.cs index e34643971..1db5199ee 100644 --- a/src/AudioAnalysisTools/Ocillations/Oscillations2012.cs +++ b/src/AudioAnalysisTools/Ocillations/Oscillations2012.cs @@ -30,7 +30,7 @@ public static (List SpectralEvents, List DecibelPlots) GetCom TimeSpan segmentStartOffset, string profileName) { - var spectralEvents = new List(); + var oscEvents = new List(); var plots = new List(); Oscillations2012.Execute( @@ -49,13 +49,13 @@ public static (List SpectralEvents, List DecibelPlots) GetCom out var hits, segmentStartOffset); - spectralEvents.AddRange(oscillationEvents); + oscEvents.AddRange(oscillationEvents); // prepare plot of resultant Harmonics decibel array. var plot = Plot.PreparePlot(scores, $"{profileName} (Oscillations:{decibelThreshold:F0}db)", decibelThreshold.Value); plots.Add(plot); - return (spectralEvents, plots); + return (oscEvents, plots); } public static void Execute( @@ -131,18 +131,14 @@ public static void Execute( // smooth the scores - window=11 has been the DEFAULT. Now letting user set this. scores = DataTools.filterMovingAverage(scores, smoothingWindow); - double[] oscFreq = GetOscillationFrequency(hits, minHz, maxHz, sonogram.FBinWidth); events = ConvertOscillationScores2Events( + sonogram, scores, - oscFreq, minHz, maxHz, - sonogram.FramesPerSecond, - sonogram.FBinWidth, scoreThreshold, minDuration, maxDuration, - sonogram.Configuration.SourceFName, segmentStartOffset); } @@ -187,14 +183,6 @@ public static void Execute( double[,] cosines = MFCCStuff.Cosines(dctLength, dctLength); //set up the cosine coefficients - //following two lines write matrix of cos values for checking. - //string txtPath = @"C:\SensorNetworks\Output\cosines.txt"; - //FileTools.WriteMatrix2File_Formatted(cosines, txtPath, "F3"); - - //following two lines write bmp image of cos values for checking. - //string bmpPath = @"C:\SensorNetworks\Output\cosines.png"; - //ImageTools.DrawMatrix(cosines, bmpPath, true); - //traverse columns - skip DC column for (int c = minBin; c <= maxBin; c++) { @@ -327,82 +315,39 @@ public static double[] GetOscillationScores(double[,] hits, int minHz, int maxHz return scores; } - public static double[] GetOscillationFrequency(double[,] hits, int minHz, int maxHz, double freqBinWidth) - { - int rows = hits.GetLength(0); - int minBin = (int)(minHz / freqBinWidth); - int maxBin = (int)(maxHz / freqBinWidth); - - //to store the oscillation frequency - var oscFreq = new double[rows]; - for (int r = 0; r < rows; r++) - { - double freq = 0; - int count = 0; - - //traverse columns in required band - for (int c = minBin; c <= maxBin; c++) - { - if (hits[r, c] > 0) - { - freq += hits[r, c]; - count++; - } - } - - if (count == 0) - { - oscFreq[r] = 0; - } - else - { - //return the average frequency - oscFreq[r] = freq / count; - } - } - - return oscFreq; - } - /// - /// Converts the Oscillation Detector score array to a list of AcousticEvents. + /// Converts the Oscillation Detector score array to a list of Oscillation Events. /// /// 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. + /// threshold. /// min threshold. /// max threshold. - /// name of source file to be added to AcousticEvent class. /// time offset. public static List ConvertOscillationScores2Events( + SpectrogramStandard spectrogram, double[] scores, - double[] oscFreq, int minHz, int maxHz, - double framesPerSec, - double freqBinWidth, - double maxScoreThreshold, + double scoreThreshold, double minDurationThreshold, double maxDurationThreshold, - string fileName, TimeSpan segmentStartOffset) { - //double minThreshold = 0.1; - //double scoreThreshold = minThreshold; //set this to the minimum threshold to start with - double scoreThreshold = maxScoreThreshold; //set this to the maximum threshold to start with + // The name of source file + string fileName = spectrogram.Configuration.SourceFName; + double framesPerSec = spectrogram.FramesPerSecond; + double freqBinWidth = spectrogram.FBinWidth; int count = scores.Length; - //int minBin = (int)(minHz / freqBinWidth); - //int maxBin = (int)(maxHz / freqBinWidth); - //int binCount = maxBin - minBin + 1; + // get the bin bounds of the frequency band of interest. + int minBin = (int)(minHz / freqBinWidth); + int maxBin = (int)(maxHz / freqBinWidth); + var events = new List(); bool isHit = false; double frameOffset = 1 / framesPerSec; - double startTime = 0.0; int startFrame = 0; //pass over all frames @@ -412,64 +357,61 @@ public static List ConvertOscillationScores2Events( { //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)) { isHit = false; - - //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 + //skip events with duration shorter than threshold + continue; } if (duration > maxDurationThreshold) { - continue; //skip events with duration longer than threshold + //skip events with duration longer than threshold + continue; } - //this is end of an event, so initialise it - var ev = new OscillationEvent() - { - //########################################################################################## - //Name = "Oscillation", //default name - EventStartSeconds = segmentStartOffset.TotalSeconds + startTime, - EventEndSeconds = segmentStartOffset.TotalSeconds + startTime + duration, - LowFrequencyHertz = minHz, - HighFrequencyHertz = maxHz, - FileName = fileName, - }; - - //########################################################################################## - //ev.SetTimeAndFreqScales(framesPerSec, freqBinWidth); + // This is end of an event, so initialise it + // First trim the event because oscillation events spill over the edges of the true event due to use of the DCT. + (int trueStartFrame, int trueEndFrame, double framePeriodicity) = OscillationEvent.TrimEvent(spectrogram, startFrame, minBin, i, maxBin); + double trueStartTime = trueStartFrame * frameOffset; + double trueEndTime = trueEndFrame * frameOffset; + int trueFrameLength = trueEndFrame - trueStartFrame + 1; //obtain average score. - double av = 0.0; - for (int n = startFrame; n <= i; n++) + double sum = 0.0; + for (int n = trueStartFrame; n <= trueEndFrame; n++) { - av += scores[n]; + sum += scores[n]; } - ev.Score = av / (i - startFrame + 1); + double score = sum / trueFrameLength; - //obtain oscillation freq. - av = 0.0; - for (int n = startFrame; n <= i; n++) + var ev = new OscillationEvent() { - av += oscFreq[n]; - } + Name = "Oscillation", + SegmentStartSeconds = segmentStartOffset.TotalSeconds, + ResultStartSeconds = segmentStartOffset.TotalSeconds + trueStartTime, + EventStartSeconds = segmentStartOffset.TotalSeconds + trueStartTime, + EventEndSeconds = segmentStartOffset.TotalSeconds + trueEndTime, + LowFrequencyHertz = minHz, + HighFrequencyHertz = maxHz, + Periodicity = framePeriodicity * frameOffset, + Score = score, + FileName = fileName, + }; //########################################################################################## //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 frames + } return events; } diff --git a/src/AudioAnalysisTools/Ocillations/Oscillations2019.cs b/src/AudioAnalysisTools/Ocillations/Oscillations2019.cs index eaca1652d..f7c528d69 100644 --- a/src/AudioAnalysisTools/Ocillations/Oscillations2019.cs +++ b/src/AudioAnalysisTools/Ocillations/Oscillations2019.cs @@ -14,8 +14,8 @@ namespace AudioAnalysisTools /// /// 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. + /// This class contains methods to detect oscillations in a the spectrogram of an audio signal. + /// The method Execute() returns all info about oscillations in the passed spectrogram. /// public static class Oscillations2019 { @@ -66,18 +66,14 @@ public static void Execute( // 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( + sonogram, dctScores, - oscFreq, minHz, maxHz, - sonogram.FramesPerSecond, - sonogram.FBinWidth, scoreThreshold, minDuration, maxDuration, - sonogram.Configuration.SourceFName, segmentStartOffset); } diff --git a/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramTools.cs b/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramTools.cs index 0f0994642..28cb60ac4 100644 --- a/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramTools.cs +++ b/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramTools.cs @@ -147,25 +147,31 @@ public static Image GetSonogramPlusCharts( BaseSonogram sonogram, List events, List plots, - double[,] hits) + double[,] hits, + string title) { - var spectrogram = sonogram.GetImage(doHighlightSubband: false, add1KHzLines: true, doMelScale: false); + // get the spectrogram as image but do not add gridlines at this stage. + var spectrogram = sonogram.GetImage(doHighlightSubband: false, add1KHzLines: false, doMelScale: false); Contract.RequiresNotNull(spectrogram, nameof(spectrogram)); var height = spectrogram.Height; var width = spectrogram.Width; var frameSize = sonogram.Configuration.WindowSize; - //var segmentDuration = sonogram.Duration; var spectrogramDuration = width * sonogram.FrameStep; // init with linear frequency scale and draw freq grid lines on image - int hertzInterval = 1000; - if (height < 200) + var nyquist = sonogram.NyquistFrequency; + int hertzInterval = 2000; + if (nyquist <= 16000) + { + hertzInterval = 1000; + } else + if (nyquist <= 8000) { - hertzInterval = 2000; + hertzInterval = 500; } - var nyquist = sonogram.NyquistFrequency; + // now add gridlines var freqScale = new FrequencyScale(nyquist, frameSize, hertzInterval); FrequencyScale.DrawFrequencyLinesOnImage(spectrogram, freqScale.GridLineLocations, includeLabels: true); @@ -186,7 +192,7 @@ public static Image GetSonogramPlusCharts( } int pixelWidth = spectrogram.Width; - var titleBar = LDSpectrogramRGB.DrawTitleBarOfGrayScaleSpectrogram("TITLE", pixelWidth); + var titleBar = LDSpectrogramRGB.DrawTitleBarOfGrayScaleSpectrogram(title, pixelWidth); var timeTrack = ImageTrack.DrawTimeTrack(sonogram.Duration, pixelWidth); var imageList = new List> @@ -226,11 +232,12 @@ public static Image GetSonogramPlusCharts( BaseSonogram sonogram, List events, List plots, - double[,] hits) + double[,] hits, + string title) { // convert AcousticEvents to EventsCommon List newEvents = EventConverters.ConvertAcousticEventsToSpectralEvents(events); - var compositeImage = GetSonogramPlusCharts(sonogram, newEvents, plots, hits); + var compositeImage = GetSonogramPlusCharts(sonogram, newEvents, plots, hits, title); return compositeImage; } diff --git a/tests/Acoustics.Test/AnalysisPrograms/Recognizers/GenericRecognizerTests.cs b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/GenericRecognizerTests.cs index 37e3ee6e7..926bcf0b9 100644 --- a/tests/Acoustics.Test/AnalysisPrograms/Recognizers/GenericRecognizerTests.cs +++ b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/GenericRecognizerTests.cs @@ -310,10 +310,9 @@ public void TestWhistleAlgorithm() var results = recognizer.Recognize(recording, config, 100.Seconds(), null, this.TestOutputDirectory, null); - // add next two lines because cannot find spectrogram included with the test results. // Used for debugging only - var image = SpectrogramTools.GetSonogramPlusCharts(results.Sonogram, results.NewEvents, results.Plots, null); - image.SaveAsPng("C:/temp/image.png"); + var image1 = SpectrogramTools.GetSonogramPlusCharts(results.Sonogram, results.NewEvents, results.Plots, null, "TestWhistleAlgorithm"); + image1.Save(this.TestOutputDirectory.CombineFile("image1.png")); Assert.AreEqual(4, results.NewEvents.Count); var @event = (SpectralEvent)results.NewEvents[0]; @@ -387,9 +386,9 @@ public void TestHarmonicsAlgorithm() }; var spectrogram = this.CreateArtificialSpectrogramToTestTracksAndHarmonics(sonoConfig); - - //var image1 = SpectrogramTools.GetSonogramPlusCharts(spectrogram, null, null, null); - //results.Sonogram.GetImage().Save(this.outputDirectory + "\\debug.png"); + var emptyList = new List(); + var image2 = SpectrogramTools.GetSonogramPlusCharts(spectrogram, emptyList, null, null, "TestHarmonicsAlgorithm"); + image2.Save(this.TestOutputDirectory.CombineFile("image2.png")); //var results = recognizer.Recognize(recording, sonoConfig, 100.Seconds(), null, this.TestOutputDirectory, null); //get the array of intensity values minus intensity in side/buffer bands. @@ -432,7 +431,7 @@ public void TestHarmonicsAlgorithm() allResults.Sonogram = spectrogram; // DEBUG PURPOSES COMMENT NEXT LINE - //GenericRecognizer.SaveDebugSpectrogram(allResults, null, outputDirectory, "name"); + //GenericRecognizer.SaveDebugSpectrogram(allResults, null, this.TestOutputDirectory, "name"); Assert.AreEqual(4, allResults.NewEvents.Count); @@ -580,7 +579,7 @@ public void TestForwardTrackAlgorithm() var spectrogram = this.CreateArtificialSpectrogramToTestTracksAndHarmonics(sonoConfig); //var image1 = SpectrogramTools.GetSonogramPlusCharts(spectrogram, null, null, null); - //results.Sonogram.GetImage().Save(this.outputDirectory + "\\debug.png"); + //results.Sonogram.GetImage().Save(this.outputDirectory.CombineFile("debug.png"); var segmentStartOffset = TimeSpan.Zero; var (spectralEvents, plotList) = ForwardTrackAlgorithm.GetForwardTracks( @@ -659,9 +658,9 @@ public void TestOneframeTrackAlgorithm() }; var spectrogram = this.CreateArtificialSpectrogramToTestTracksAndHarmonics(sonoConfig); - - //var image1 = SpectrogramTools.GetSonogramPlusCharts(spectrogram, null, null, null); - //results.Sonogram.GetImage().Save(this.outputDirectory + "\\debug.png"); + var emptyList = new List(); + var image1 = SpectrogramTools.GetSonogramPlusCharts(spectrogram, emptyList, null, null, "TestOneframeTrackAlgorithm"); + image1.Save(this.TestOutputDirectory.CombineFile("debug.png")); var segmentStartOffset = TimeSpan.Zero; var (spectralEvents, plotList) = OneframeTrackAlgorithm.GetOneFrameTracks( @@ -864,7 +863,7 @@ public void Test2UpwardsTrackAlgorithm() allResults2.Plots.AddRange(plots); allResults2.Sonogram = spectrogram; - // DEBUG PURPOSES ONLY - COMMENT NEXT LINE + // DEBUG PURPOSES ONLY this.SaveTestOutput( outputDirectory => GenericRecognizer.SaveDebugSpectrogram(allResults2, null, outputDirectory, "UpwardTracks2")); @@ -1176,6 +1175,10 @@ public void TestMultipleAlgorithms() var results = recognizer.Recognize(recording, config, 100.Seconds(), null, this.TestOutputDirectory, null); + // DEBUG PURPOSES ONLY + this.SaveTestOutput( + outputDirectory => GenericRecognizer.SaveDebugSpectrogram(results, null, outputDirectory, "ThreeProfiles")); + Assert.AreEqual(3, results.NewEvents.Count); var @event = (SpectralEvent)results.NewEvents[0]; @@ -1205,5 +1208,83 @@ public void TestMultipleAlgorithms() Assert.AreEqual("DTMFupper", @event.Name); Assert.AreEqual("UpperBandDTMF_z", @event.Profile); } + + [TestMethod] + public void TestBlobPlusOscillationProfiles() + { + var config = new GenericRecognizer.GenericRecognizerConfig() + { + Profiles = new Dictionary() + { + { + "TestBlob", new BlobParameters() + { + FrameSize = 1024, + FrameStep = 1024, + MaxHertz = 7200, + MinHertz = 4800, + BgNoiseThreshold = 0.0, + BottomHertzBuffer = 1000, + TopHertzBuffer = 500, + DecibelThresholds = new double?[] { 3.0 }, + } + }, + { + "TestOscillation", + new OscillationParameters() + { + SpeciesName = "DTMFlower", + FrameSize = 512, + FrameStep = 512, + BgNoiseThreshold = 0.0, + MaxHertz = 1050, + MinHertz = 700, + BottomHertzBuffer = 0, + TopHertzBuffer = 0, + DctDuration = 1.0, + MinOscillationFrequency = 1, + MaxOscillationFrequency = 2, + MinDuration = 4, + MaxDuration = 6, + EventThreshold = 0.3, + DecibelThresholds = new double?[] { 3.0 }, + } + }, + }, + PostProcessing = new PostProcessingConfig() + { + CombineOverlappingEvents = false, + }, + }; + + var results = recognizer.Recognize(recording, config, 100.Seconds(), null, this.TestOutputDirectory, null); + + // DEBUG PURPOSES + this.SaveTestOutput( + outputDirectory => GenericRecognizer.SaveDebugSpectrogram(results, null, outputDirectory, "BlobPlusOsc")); + + Assert.AreEqual(2, results.NewEvents.Count); + + var event1 = (SpectralEvent)results.NewEvents[0]; + Assert.AreEqual(120, event1.EventStartSeconds, 0.1); + Assert.AreEqual(122, event1.EventEndSeconds, 0.1); + Assert.AreEqual(4800, event1.LowFrequencyHertz, 0.1); + Assert.AreEqual(7200, event1.HighFrequencyHertz, 0.1); + Assert.AreEqual("TestBlob", event1.Profile); + Assert.AreEqual(null, event1.Name); + Assert.AreEqual("SpectralEvent", event1.ComponentName); + + var event2 = (OscillationEvent)results.NewEvents[1]; + Assert.AreEqual(typeof(OscillationEvent), event2.GetType()); + Assert.AreEqual(108.1, event2.EventStartSeconds, 0.1); + Assert.AreEqual(113.1, event2.EventEndSeconds, 0.1); + Assert.AreEqual(700, event2.LowFrequencyHertz); + Assert.AreEqual(1050, event2.HighFrequencyHertz); + Assert.AreEqual("TestOscillation", event2.Profile); + Assert.AreEqual("DTMFlower", event2.Name); + Assert.AreEqual("OscillationEvent", event2.ComponentName); + Assert.AreEqual("acoustic_components", event2.FileName); + Assert.AreEqual(0.83, event2.Periodicity, 0.1); + } } } \ No newline at end of file diff --git a/tests/Acoustics.Test/AnalysisPrograms/Recognizers/PteropusSpTests.cs b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/PteropusSpTests.cs index 4d7a1333b..6c0663165 100644 --- a/tests/Acoustics.Test/AnalysisPrograms/Recognizers/PteropusSpTests.cs +++ b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/PteropusSpTests.cs @@ -85,25 +85,29 @@ public void TestGetWingBeatEvents() Assert.AreEqual(4, acousticEvents.Count); - Assert.AreEqual(29.72, acousticEvents[0].EventStartSeconds, 0.1); - Assert.AreEqual(32.06, acousticEvents[0].EventEndSeconds, 0.1); + Assert.AreEqual(29.9, acousticEvents[0].EventStartSeconds, 0.1); + Assert.AreEqual(32.0, acousticEvents[0].EventEndSeconds, 0.1); Assert.AreEqual(200, acousticEvents[0].LowFrequencyHertz); Assert.AreEqual(2000, acousticEvents[0].HighFrequencyHertz); + Assert.AreEqual(0.2, acousticEvents[0].Periodicity, 0.1); - Assert.AreEqual(40.91, acousticEvents[1].EventStartSeconds, 0.1); - Assert.AreEqual(42.40, acousticEvents[1].EventEndSeconds, 0.1); + Assert.AreEqual(41.2, acousticEvents[1].EventStartSeconds, 0.1); + Assert.AreEqual(42.0, acousticEvents[1].EventEndSeconds, 0.1); Assert.AreEqual(200, acousticEvents[1].LowFrequencyHertz); Assert.AreEqual(2000, acousticEvents[1].HighFrequencyHertz); + Assert.AreEqual(0.2, acousticEvents[0].Periodicity, 0.1); - Assert.AreEqual(48.37, acousticEvents[2].EventStartSeconds, 0.1); - Assert.AreEqual(51.27, acousticEvents[2].EventEndSeconds, 0.1); + Assert.AreEqual(48.6, acousticEvents[2].EventStartSeconds, 0.1); + Assert.AreEqual(51.0, acousticEvents[2].EventEndSeconds, 0.1); Assert.AreEqual(200, acousticEvents[2].LowFrequencyHertz); Assert.AreEqual(2000, acousticEvents[2].HighFrequencyHertz); + Assert.AreEqual(0.2, acousticEvents[0].Periodicity, 0.1); - Assert.AreEqual(54.19, acousticEvents[3].EventStartSeconds, 0.1); - Assert.AreEqual(55.33, acousticEvents[3].EventEndSeconds, 0.1); + Assert.AreEqual(54.4, acousticEvents[3].EventStartSeconds, 0.1); + Assert.AreEqual(55.1, acousticEvents[3].EventEndSeconds, 0.1); Assert.AreEqual(200, acousticEvents[3].LowFrequencyHertz); Assert.AreEqual(2000, acousticEvents[3].HighFrequencyHertz); + Assert.AreEqual(0.2, acousticEvents[0].Periodicity, 0.1); //Assert.AreEqual(0.6062, stats.SpectralEnergyDistribution, 1E-4); } diff --git a/tests/Acoustics.Test/AudioAnalysisTools/StandardSpectrograms/SonogramTests.cs b/tests/Acoustics.Test/AudioAnalysisTools/StandardSpectrograms/SonogramTests.cs index baf30c39b..5473f637d 100644 --- a/tests/Acoustics.Test/AudioAnalysisTools/StandardSpectrograms/SonogramTests.cs +++ b/tests/Acoustics.Test/AudioAnalysisTools/StandardSpectrograms/SonogramTests.cs @@ -194,7 +194,7 @@ public void TestAnnotatedSonogramWithPlots() new AcousticEvent(startOffset, 40.0, 10.0, 4000, 5000), }; - var image = SpectrogramTools.GetSonogramPlusCharts(actualDecibelSpectrogram, events, plots, null); + var image = SpectrogramTools.GetSonogramPlusCharts(actualDecibelSpectrogram, events, plots, null, actualDecibelSpectrogram.Configuration.SourceFName); // create the image for visual confirmation this.SaveTestOutput(outputDirectory => BaseSonogram.SaveDebugSpectrogram(image, outputDirectory, this.recording.BaseName)); diff --git a/tests/Acoustics.Test/TowseyLibrary/DataToolsTests.cs b/tests/Acoustics.Test/TowseyLibrary/DataToolsTests.cs index 77b6ec0ff..4bf51d547 100644 --- a/tests/Acoustics.Test/TowseyLibrary/DataToolsTests.cs +++ b/tests/Acoustics.Test/TowseyLibrary/DataToolsTests.cs @@ -5,11 +5,14 @@ namespace Acoustics.Test.TowseyLibrary { using System.Collections.Generic; + using Acoustics.Test.TestHelpers; using global::TowseyLibrary; using Microsoft.VisualStudio.TestTools.UnitTesting; + using SixLabors.ImageSharp; + using SixLabors.ImageSharp.PixelFormats; [TestClass] - public class DataToolsTests + public class DataToolsTests : OutputDirectoryTest { [TestMethod] public void TestConcatenateVectors() @@ -38,5 +41,85 @@ public void TestConcatenateVectorsOverload() CollectionAssert.AreEqual(expected, actual); } + + [TestMethod] + public void TestPlotImages() + { + var plots = new List(); + + // Prepare and initialise three different plots. All linear but having different lengths. + //prepare plot 2 + double[] array1 = new double[500]; + for (int i = 0; i < 500; i++) + { + array1[i] = i; + } + + string title1 = "Plot 1"; + double threshold1 = 200.0; + Plot plot1 = Plot.PreparePlot(array1, title1, threshold1); + plots.Add(plot1); + + //prepare plot 2 + double[] array2 = new double[400]; + for (int i = 0; i < 400; i++) + { + array2[i] = i; + } + + string title2 = "Plot 2"; + double threshold2 = 150.0; + Plot plot2 = Plot.PreparePlot(array2, title2, threshold2); + plots.Add(plot2); + + //prepare plot 3 + double[] array3 = new double[200]; + for (int i = 0; i < 200; i++) + { + array3[i] = i; + } + + string title3 = "Plot 3"; + double threshold3 = 80.0; + Plot plot3 = Plot.PreparePlot(array3, title3, threshold3); + plots.Add(plot3); + + int plotHeight = 50; + + // now concatenate the plots WITHOUT rescaling their length. + var imageList1 = new List>(); + foreach (var plot in plots) + { + var image = plot.DrawAnnotatedPlot(plotHeight); + imageList1.Add(image); + } + + // create the image for visual confirmation + var concatImage1 = ImageTools.CombineImagesVertically(imageList1); + concatImage1.Save(this.TestOutputDirectory + "ConcatImage1.png"); + + // concatenate the plots again but this time WITH length rescaling. + int rescaledWidth = 400; + var imageList2 = new List>(); + foreach (var plot in plots) + { + plot.ScaleDataArray(rescaledWidth); + var image = plot.DrawAnnotatedPlot(plotHeight); + imageList2.Add(image); + } + + // create the image for visual confirmation + var concatImage2 = ImageTools.CombineImagesVertically(imageList2); + concatImage2.Save(this.TestOutputDirectory + "ConcatImage2.png"); + + // now confirm the widths. + // concatImage1 has width equal to longest plot. + Assert.AreEqual(150, concatImage1.Height); + Assert.AreEqual(500, concatImage1.Width); + + // concatImage2 has width equal to longest rescaled width. + Assert.AreEqual(150, concatImage2.Height); + Assert.AreEqual(400, concatImage2.Width); + } } } \ No newline at end of file