diff --git a/src/AnalysisBase/ResultBases/EventBase.cs b/src/AnalysisBase/ResultBases/EventBase.cs index ea37af352..80959cc82 100644 --- a/src/AnalysisBase/ResultBases/EventBase.cs +++ b/src/AnalysisBase/ResultBases/EventBase.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). // @@ -12,14 +12,14 @@ namespace AnalysisBase.ResultBases using System; /// - /// The base class for all Event style results + /// The base class for all Event style results. /// public abstract class EventBase : ResultBase { private double eventStartSeconds; /// - /// Gets or sets the time the current audio segment is offset from the start of the file/recording. + /// Gets or sets the time (in seconds) from start of the file/recording to start of the current audio segment. /// /// /// will always be greater than or equal to . @@ -36,7 +36,7 @@ public abstract class EventBase : ResultBase /// /// /// 2017-09: This field USED to be offset relative to the current segment. - /// 2017-09: This field is NOW equivalent to + /// 2017-09: This field is NOW equivalent to . /// public virtual double EventStartSeconds { @@ -60,6 +60,13 @@ public virtual double EventStartSeconds /// public virtual double? LowFrequencyHertz { get; protected set; } + /// + /// Sets both the Segment start and the Event start. + /// is measured relative to the start of the recording. + /// is measured relative to the start of the segment. + /// This method sets both and which + /// are both measured relative to the start of the recording. + /// protected void SetEventStartRelative(TimeSpan segmentStart, double eventStartSegmentRelative) { this.SegmentStartSeconds = segmentStart.TotalSeconds; diff --git a/src/AnalysisPrograms/AnalyseLongRecordings/AnalyseLongRecording.cs b/src/AnalysisPrograms/AnalyseLongRecordings/AnalyseLongRecording.cs index 1cb00f4f5..9643f87e3 100644 --- a/src/AnalysisPrograms/AnalyseLongRecordings/AnalyseLongRecording.cs +++ b/src/AnalysisPrograms/AnalyseLongRecordings/AnalyseLongRecording.cs @@ -73,7 +73,8 @@ public static void Execute(Arguments arguments) { Log.Warn($"Config file {configFile.FullName} not found... attempting to resolve config file"); - // we use .ToString() here to get the original input string - Using fullname always produces an absolute path wrt to pwd... we don't want to prematurely make asusmptions: + // we use .ToString() here to get the original input string. + // Using fullname always produces an absolute path relative to pwd... we don't want to prematurely make assumptions: // e.g. We require a missing absolute path to fail... that wouldn't work with .Name // e.g. We require a relative path to try and resolve, using .FullName would fail the first absolute check inside ResolveConfigFile configFile = ConfigFile.Resolve(configFile.ToString(), Directory.GetCurrentDirectory().ToDirectoryInfo()); diff --git a/src/AnalysisPrograms/EventStatistics/EventStatisticsEntry.cs b/src/AnalysisPrograms/EventStatistics/EventStatisticsEntry.cs index eb5fab25b..bea3e007a 100644 --- a/src/AnalysisPrograms/EventStatistics/EventStatisticsEntry.cs +++ b/src/AnalysisPrograms/EventStatistics/EventStatisticsEntry.cs @@ -49,7 +49,7 @@ public static async Task ExecuteAsync(Arguments arguments) Log.Warn($"Config file {config.FullName} not found... attempting to resolve config file"); // we use the original input string - Using FileInfo fullname always produces an - // absolute path wrt to pwd... we don't want to prematurely make assumptions: + // absolute path relative to pwd... we don't want to prematurely make assumptions: // e.g. We require a missing absolute path to fail... that wouldn't work with .Name // e.g. We require a relative path to try and resolve, using .FullName would fail the first absolute // check inside ResolveConfigFile diff --git a/src/AnalysisPrograms/OscillationRecogniser.cs b/src/AnalysisPrograms/OscillationRecogniser.cs index 4cfb76af1..71e932483 100644 --- a/src/AnalysisPrograms/OscillationRecogniser.cs +++ b/src/AnalysisPrograms/OscillationRecogniser.cs @@ -12,6 +12,7 @@ namespace AnalysisPrograms using System.Linq; using System.Text; using System.Threading.Tasks; + using Acoustics.Shared.Csv; using AudioAnalysisTools; using AudioAnalysisTools.StandardSpectrograms; using AudioAnalysisTools.WavTools; @@ -126,15 +127,10 @@ public static void Execute(Arguments arguments) pcHIF = 100 * hifCount / sonogram.FrameCount; } - //write event count to results file. - double sigDuration = sonogram.Duration.TotalSeconds; + // write event count to results file. string fname = recordingFile.BaseName(); - int count = predictedEvents.Count; - //string str = String.Format("#RecordingName\tDuration(sec)\t#Ev\tCompT(ms)\t%hiFrames\n{0}\t{1}\t{2}\t{3}\t{4}\n", fname, sigDuration, count, analysisDuration.TotalMilliseconds, pcHIF); - string str = string.Format("{0}\t{1}\t{2}\t{3}\t{4}", fname, sigDuration, count, analysisDuration.TotalMilliseconds, pcHIF); - StringBuilder sb = AcousticEvent.WriteEvents(predictedEvents, str); - FileTools.WriteTextFile(opPath, sb.ToString()); + Csv.WriteToCsv(opPath.ToFileInfo(), predictedEvents); //draw images of sonograms string imagePath = outputDir + fname + ".png"; diff --git a/src/AnalysisPrograms/Recognizers/Base/RecognizerBase.cs b/src/AnalysisPrograms/Recognizers/Base/RecognizerBase.cs index 8e068092a..9f2306d53 100644 --- a/src/AnalysisPrograms/Recognizers/Base/RecognizerBase.cs +++ b/src/AnalysisPrograms/Recognizers/Base/RecognizerBase.cs @@ -336,35 +336,6 @@ protected virtual Image DrawSonogram( double eventThreshold) { var image = SpectrogramTools.GetSonogramPlusCharts(sonogram, predictedEvents, scores, hits); - - //const bool doHighlightSubband = false; - //const bool add1KHzLines = true; - //var image = new Image_MultiTrack(sonogram.GetImage(doHighlightSubband, add1KHzLines, doMelScale: false)); - //image.AddTrack(ImageTrack.GetTimeTrack(sonogram.Duration, sonogram.FramesPerSecond)); - //image.AddTrack(ImageTrack.GetSegmentationTrack(sonogram)); - - //if (scores != null) - //{ - // foreach (var plot in scores) - // { - // image.AddTrack(ImageTrack.GetNamedScoreTrack(plot.data, 0.0, 1.0, plot.threshold, plot.title)); - // } - //} - - //if (hits != null) - //{ - // image.OverlayRedTransparency(hits); - //} - - //if (predictedEvents != null && predictedEvents.Count > 0) - //{ - // image.AddEvents( - // predictedEvents, - // sonogram.NyquistFrequency, - // sonogram.Configuration.FreqBinCount, - // sonogram.FramesPerSecond); - //} - return image; } diff --git a/src/AnalysisPrograms/Recognizers/Base/WhistleParameters.cs b/src/AnalysisPrograms/Recognizers/Base/WhistleParameters.cs index 071b49772..77231d622 100644 --- a/src/AnalysisPrograms/Recognizers/Base/WhistleParameters.cs +++ b/src/AnalysisPrograms/Recognizers/Base/WhistleParameters.cs @@ -39,10 +39,6 @@ public static (List, double[]) GetWhistles( double binWidth = nyquist / (double)binCount; int minBin = (int)Math.Round(minHz / binWidth); int maxBin = (int)Math.Round(maxHz / binWidth); - //int binCountInBand = maxBin - minBin + 1; - - // buffer zone around whistle is four bins wide. - int N = 4; // list of accumulated acoustic events var events = new List(); @@ -54,7 +50,8 @@ public static (List, double[]) GetWhistles( // set up an intensity array for the frequency bin. double[] intensity = new double[frameCount]; - if (minBin < N) + // buffer zone around whistle is four bins wide. + if (minBin < 4) { // for all time frames in this frequency bin for (int t = 0; t < frameCount; t++) @@ -110,33 +107,9 @@ public static (List, double[]) GetWhistles( } //end for all freq bins // combine adjacent acoustic events - events = AcousticEvent.CombineOverlappingEvents(events); + events = AcousticEvent.CombineOverlappingEvents(events, segmentStartOffset); return (events, combinedIntensityArray); } - - /* - /// - /// Calculates the average intensity in a freq band having min and max freq, - /// AND then subtracts average intensity in the side/buffer bands, below and above. - /// THis method adds dB log values incorrectly but it is faster than doing many log conversions. - /// This method is used to find acoustic events and is accurate enough for the purpose. - /// - public static double[] CalculateFreqBandAvIntensityMinusBufferIntensity(double[,] sonogramData, int minHz, int maxHz, int nyquist) - { - var bandIntensity = SNR.CalculateFreqBandAvIntensity(sonogramData, minHz, maxHz, nyquist); - var bottomSideBandIntensity = SNR.CalculateFreqBandAvIntensity(sonogramData, minHz - bottomHzBuffer, minHz, nyquist); - var topSideBandIntensity = SNR.CalculateFreqBandAvIntensity(sonogramData, maxHz, maxHz + topHzBuffer, nyquist); - - int frameCount = sonogramData.GetLength(0); - double[] netIntensity = new double[frameCount]; - for (int i = 0; i < frameCount; i++) - { - netIntensity[i] = bandIntensity[i] - bottomSideBandIntensity[i] - topSideBandIntensity[i]; - } - - return netIntensity; - } - */ } } diff --git a/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs b/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs index 76c451136..21aa9a855 100644 --- a/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs +++ b/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs @@ -325,18 +325,9 @@ private static Plot PreparePlot(double[] array, string title, double threshold) /// static void SaveDebugSpectrogram(RecognizerResults results, Config genericConfig, DirectoryInfo outputDirectory, string baseName) { - //var image1 = results.Sonogram.GetImage(false, true, false); - //image1.Save(Path.Combine("C:\\temp\\test1.profile.png")); - - //var image2 = results.Sonogram.GetImageFullyAnnotated("Test"); - //image2.Save(Path.Combine("C:\\temp\\test2.profile.png")); - var image3 = SpectrogramTools.GetSonogramPlusCharts(results.Sonogram, results.Events, results.Plots, null); - //image3.Save(Path.Combine(outputDirectory.FullName, baseName + ".profile.png")); - image3.Save(Path.Combine("C:\\temp", baseName + ".profile.png")); - - //sonogram.GetImageFullyAnnotated("test").Save("C:\\temp\\test.png"); + image3.Save(Path.Combine(outputDirectory.FullName, baseName + ".profile.png")); } /// /> diff --git a/src/AnalysisPrograms/Recognizers/LewiniaPectoralis.cs b/src/AnalysisPrograms/Recognizers/LewiniaPectoralis.cs index ca8dee5d9..499b1f654 100644 --- a/src/AnalysisPrograms/Recognizers/LewiniaPectoralis.cs +++ b/src/AnalysisPrograms/Recognizers/LewiniaPectoralis.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,14 +193,6 @@ public override RecognizerResults Recognize( prunedEvents.Add(ae); } - // do a recognizer TEST. - if (false) - { - var testDir = new DirectoryInfo(outputDirectory.Parent.Parent.FullName); - TestTools.RecognizerScoresTest(recording.BaseName, testDir, recognizerConfig.AnalysisName, scoreArray); - AcousticEvent.TestToCompareEvents(recording.BaseName, testDir, recognizerConfig.AnalysisName, predictedEvents); - } - // increase very low scores for (int j = 0; j < scoreArray.Length; j++) { diff --git a/src/AnalysisPrograms/Recognizers/LitoriaBicolor.cs b/src/AnalysisPrograms/Recognizers/LitoriaBicolor.cs index a0a6109f7..cf14962cb 100644 --- a/src/AnalysisPrograms/Recognizers/LitoriaBicolor.cs +++ b/src/AnalysisPrograms/Recognizers/LitoriaBicolor.cs @@ -152,14 +152,6 @@ public override RecognizerResults Recognize(AudioRecording recording, Config con ae.SegmentDurationSeconds = recordingDuration.TotalSeconds; } - // do a RECOGNIZER TEST. - if (false) - { - var testDir = new DirectoryInfo(outputDirectory.Parent.Parent.FullName); - TestTools.RecognizerScoresTest(recording.BaseName, testDir, recognizerConfig.AnalysisName, scoreArray); - AcousticEvent.TestToCompareEvents(recording.BaseName, testDir, recognizerConfig.AnalysisName, predictedEvents); - } - var plot = new Plot(this.DisplayName, scoreArray, recognizerConfig.EventThreshold); return new RecognizerResults() { @@ -171,7 +163,7 @@ public override RecognizerResults Recognize(AudioRecording recording, Config con } /// - /// ################ THE KEY ANALYSIS METHOD + /// THE KEY ANALYSIS METHOD. /// /// /// diff --git a/src/AnalysisPrograms/Recognizers/LitoriaWatjulumensis.cs b/src/AnalysisPrograms/Recognizers/LitoriaWatjulumensis.cs index a8741adb3..501aa406c 100644 --- a/src/AnalysisPrograms/Recognizers/LitoriaWatjulumensis.cs +++ b/src/AnalysisPrograms/Recognizers/LitoriaWatjulumensis.cs @@ -157,14 +157,6 @@ public override RecognizerResults Recognize(AudioRecording recording, Config con ae.SegmentStartSeconds = segmentStartOffset.TotalSeconds; } - // do a recognizer TEST. - if (false) - { - var testDir = new DirectoryInfo(outputDirectory.Parent.Parent.FullName); - TestTools.RecognizerScoresTest(recording.BaseName, testDir, recognizerConfig.AnalysisName, scoreArray); - AcousticEvent.TestToCompareEvents(recording.BaseName, testDir, recognizerConfig.AnalysisName, predictedEvents); - } - var plot = new Plot(this.DisplayName, scoreArray, recognizerConfig.EventThreshold); return new RecognizerResults() { @@ -175,8 +167,8 @@ public override RecognizerResults Recognize(AudioRecording recording, Config con }; } - /// - /// ################ THE KEY ANALYSIS METHOD for TRILLS + /// + /// ################ THE KEY ANALYSIS METHOD for TRILLS /// /// See Anthony's ExempliGratia.Recognize() method in order to see how to use methods for config profiles. /// diff --git a/src/AudioAnalysisTools/AcousticEvent.cs b/src/AudioAnalysisTools/AcousticEvent.cs index 4b188aa29..b28e06b17 100644 --- a/src/AudioAnalysisTools/AcousticEvent.cs +++ b/src/AudioAnalysisTools/AcousticEvent.cs @@ -11,10 +11,8 @@ namespace AudioAnalysisTools { using System; using System.Collections.Generic; - using System.IO; using System.Linq; using System.Text; - using Acoustics.Shared; using Acoustics.Shared.Contracts; using Acoustics.Shared.Csv; using Acoustics.Shared.ImageSharp; @@ -26,7 +24,7 @@ namespace AudioAnalysisTools using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using TowseyLibrary; - using Path = System.IO.Path; + using static Acoustics.Shared.ImageSharp.Drawing; public class AcousticEvent : EventBase { @@ -40,7 +38,7 @@ public sealed class AcousticEventClassMap : ClassMap { nameof(TimeStart), nameof(TimeEnd), nameof(Bandwidth), nameof(IsMelscale), nameof(FrameOffset), - nameof(FramesPerSecond), nameof(Name2), nameof(ScoreComment), + nameof(FramesPerSecond), nameof(ScoreNormalised), nameof(Score_MaxPossible), nameof(Score_MaxInEvent), nameof(Score_TimeOfMaxInEvent), nameof(Score2Name), nameof(Score2), nameof(Periodicity), nameof(DominantFreq), @@ -73,42 +71,44 @@ public AcousticEventClassMap() /// /// Gets the time offset from start of current segment to start of event in seconds. - /// NOTE: AcousticEvents do not have a notion of time offset wrt start of recording ; - only to start of current recording segment. - /// Proxied to EventBase.EventStartSeconds. + /// Proxied to EventBase.EventStartSeconds. /// /// + /// + /// NOTE: is relative to the start of a segment. This notion is obsolete! + /// Events must always be stored relative to start of the recording. + /// /// Note: converted to private setter so we can control how this is set. Recommend using /// after event instantiation to modify bounds. /// + [Obsolete("Bounds relative to the segment are inconsistent with our rules for always measuring from the start of the recording.")] public double TimeStart { get; private set; } /// - /// Gets or sets units = seconds - /// Time offset from start of current segment to end of the event. - /// Written into the csv file under column "EventEndSeconds" + /// Gets the time offset (in seconds) from start of current segment to end of the event. /// This field is NOT in EventBase. EventBase only requires TimeStart because it is designed to also accomodate points. /// /// - /// Note: converted to private setter so we can control how this is set. Recommend using - /// after event instantiation to modify bounds. + /// + /// NOTE: is relative to the start of a segment. This notion is obsolete! + /// Events must always be stored relative to start of the recording. + /// + /// Note: converted to private setter so we can control how this is set. + /// Recommend using after event instantiation to modify bounds. /// + [Obsolete("Bounds relative to the segment are inconsistent with our rules for always measuring from the start of the recording.")] public double TimeEnd { get; private set; } + /// + /// Gets the end time of an event relative to the recording start. + /// public double EventEndSeconds => this.TimeEnd + this.SegmentStartSeconds; + /// + /// Gets the start time of an event relative to the recording start. + /// public override double EventStartSeconds => this.TimeStart + this.SegmentStartSeconds; - public void SetEventPositionRelative( - TimeSpan segmentStartOffset, - double eventStartSegment, - double eventEndSegment) - { - this.TimeStart = eventStartSegment; - this.TimeEnd = eventEndSegment; - - this.SetEventStartRelative(segmentStartOffset, eventStartSegment); - } - /// /// Gets or sets units = Hertz. /// Proxied to EventBase.MinHz. @@ -126,30 +126,39 @@ public void SetEventPositionRelative( } } - /// Gets or sets units = Hertz + /// Gets or sets units = Hertz. public double HighFrequencyHertz { get; set; } + /// + /// Gets the bandwidth of an acoustic event. + /// public double Bandwidth => this.HighFrequencyHertz - this.LowFrequencyHertz + 1; public bool IsMelscale { get; set; } + /// + /// Gets or sets the bounds of an event with respect to the segment start + /// BUT in terms of the frame count (from segment start) and frequency bin (from zero Hertz). + /// This is no longer the preferred way to operate with acoustic event bounds. + /// Better to use real units (seconds and Hertz) and provide the acoustic event with scale information. + /// public Oblong Oblong { get; set; } - /// Gets or sets required for conversions to & from MEL scale AND for drawing event on spectrum + /// Gets or sets required for conversions to & from MEL scale AND for drawing event on spectrum. public int FreqBinCount { get; set; } /// - /// Gets required for freq-binID conversions + /// Gets required for freq-binID conversions. /// public double FreqBinWidth { get; private set; } - /// Gets frame duration in seconds + /// Gets frame duration in seconds. public double FrameDuration { get; private set; } - /// Gets or sets time between frame starts in seconds. Inverse of FramesPerSecond + /// Gets or sets time between frame starts in seconds. Inverse of FramesPerSecond. public double FrameOffset { get; set; } - /// Gets or sets number of frame starts per second. Inverse of the frame offset + /// Gets or sets number of frame starts per second. Inverse of the frame offset. public double FramesPerSecond { get; set; } //PROPERTIES OF THE EVENTS i.e. Name, SCORE ETC @@ -157,12 +166,7 @@ public void SetEventPositionRelative( public string Name { get; set; } - public string Name2 { get; set; } - - /// Gets or sets average score through the event. - public string ScoreComment { get; set; } - - /// Gets or sets score normalised in range [0,1]. NOTE: Max is set = to five times user supplied threshold + /// Gets or sets score normalised in range [0,1]. NOTE: Max is set = to five times user supplied threshold. public double ScoreNormalised { get; set; } /// Gets max Possible Score: set = to 5x user supplied threshold. An arbitrary value used for score normalisation. @@ -174,55 +178,47 @@ public void SetEventPositionRelative( public string Score2Name { get; set; } - /// Gets or sets second score if required - public double Score2 { get; set; } // e.g. for Birgits recognisers + /// Gets or sets second score if required. + /// + [Obsolete("We should use another type of Event class to represent this concept")] + public double Score2 { get; set; } + + ///// + ///// Gets or sets a list of points that can be used to identifies features in spectrogram relative to the Event. + ///// i.e. Points can be outside of events and can have negative values. + ///// Point location is relative to the top left corner of the event. + ///// + //public List Points { get; set; } /// - /// Gets or sets a list of points that can be used to identifies features in spectrogram relative to the Event. - /// i.e. Points can be outside of events and can have negative values. - /// Point location is relative to the top left corner of the event. + /// Gets or sets the periodicity of acoustic energy in an event. + /// Use for events which have an oscillating acoustic energy - e.g. for frog calls. /// - public List Points { get; set; } - - public double Periodicity { get; set; } // for events which have an oscillating acoustic energy - used for frog calls + public double Periodicity { get; set; } public double DominantFreq { get; set; } // the dominant freq in the event - used for frog calls - // double I1MeandB; //mean intensity of pixels in the event prior to noise subtraction - // double I1Var; //, - // double I2MeandB; // mean intensity of pixels in the event after Wiener filter, prior to noise subtraction - // double I2Var; //, - private double I3Mean; // mean intensity of pixels in the event AFTER noise reduciton - USED FOR CLUSTERING - private double I3Var; // variance of intensity of pixels in the event. - - // following are no longer needed. Delete eventually. - /* - //KIWI SCORES - public double kiwi_durationScore; - public double kiwi_hitScore; - public double kiwi_snrScore; - public double kiwi_sdPeakScore; - public double kiwi_intensityScore; - public double kiwi_gridScore; - public double kiwi_chirpScore; - public double kiwi_bandWidthScore; - public double kiwi_deltaPeriodScore; - public double kiwi_comboScore; - */ - - /// Gets or sets a value indicating whether use this if want to filter or tag some members of a list for some purpose. + /// + /// Gets or sets a value that can be used to filter or tag some members of a list of acoustic events. + /// Was used for constructing data sets. + /// public bool Tag { get; set; } - /// Gets or sets assigned value when reading in a list of user identified events. Indicates a user assigned assessment of event intensity + /// Gets or sets assigned value when reading in a list of user identified events. Indicates a user assigned assessment of event intensity. public int Intensity { get; set; } - /// Gets or sets assigned value when reading in a list of user identified events. Indicates a user assigned assessment of event quality + /// Gets or sets assigned value when reading in a list of user identified events. Indicates a user assigned assessment of event quality. public int Quality { get; set; } public Color BorderColour { get; set; } public Color ScoreColour { get; set; } + /// + /// Initializes a new instance of the class. + /// Sets some default colors for drawing an event on a spectrogram. + /// THis is the first of three constructors. + /// public AcousticEvent() { this.BorderColour = DefaultBorderColor; @@ -231,11 +227,20 @@ public AcousticEvent() this.IsMelscale = false; } - public AcousticEvent(TimeSpan segmentStartOffset, double startTime, double eventDuration, double minFreq, double maxFreq) + /// + /// Initializes a new instance of the class. + /// This constructor requires the minimum information to establish the temporal and frequency bounds of an acoustic event. + /// + /// The start of the current segment relative to start of recording. + /// event start with respect to start of segment. + /// event end with respect to start of segment. + /// Lower frequency bound of event. + /// Upper frequency bound of event. + public AcousticEvent(TimeSpan segmentStartOffset, double eventStartSegmentRelative, double eventDuration, double minFreq, double maxFreq) : this() { - this.SetEventPositionRelative(segmentStartOffset, startTime, startTime + eventDuration); - + var eventEndSegmentRelative = eventStartSegmentRelative + eventDuration; + this.SetEventPositionRelative(segmentStartOffset, eventStartSegmentRelative, eventEndSegmentRelative); this.LowFrequencyHertz = minFreq; this.HighFrequencyHertz = maxFreq; @@ -246,6 +251,8 @@ public AcousticEvent(TimeSpan segmentStartOffset, double startTime, double event /// /// Initializes a new instance of the class. /// This constructor currently works ONLY for linear Hertz scale events. + /// It requires the event bounds to provided (using Oblong) in terms of time frame and frequency bin counts. + /// Scale information must also be provided to convert bounds into real values (seconds, Hertz). /// /// An oblong initialized with bin and frame numbers marking location of the event. /// to set the freq scale. @@ -288,22 +295,44 @@ public AcousticEvent(TimeSpan segmentStartOffset, Oblong o, int nyquistFrequency /// public string Profile { get; set; } - public void DoMelScale(bool doMelscale, int freqBinCount) + //public void DoMelScale(bool doMelscale, int freqBinCount) + //{ + // this.IsMelscale = doMelscale; + // this.FreqBinCount = freqBinCount; + //} + + /// + /// Set the start and end times of an event with respect to the segment start time + /// AND also calls method to set event start time with respect the recording/file start. + /// + public void SetEventPositionRelative( + TimeSpan segmentStartOffset, + double eventStartSegmentRelative, + double eventEndSegmentRelative) { - this.IsMelscale = doMelscale; - this.FreqBinCount = freqBinCount; + this.TimeStart = eventStartSegmentRelative; + this.TimeEnd = eventEndSegmentRelative; + + this.SetEventStartRelative(segmentStartOffset, eventStartSegmentRelative); } + /// + /// THe only call to this method is from a no-longer used recogniser. + /// Could be deleted. + /// It sets the time and frequency scales for an event given the sr, and window size. + /// public void SetTimeAndFreqScales(int samplingRate, int windowSize, int windowOffset) { - 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 + //set the frame duration and offset in seconds + this.FrameDuration = windowSize / (double)samplingRate; + this.FrameOffset = windowOffset / (double)samplingRate; + this.FramesPerSecond = 1 / this.FrameOffset; - 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 + //set the Freq Scale. Required for freq-binID conversions + this.FreqBinWidth = samplingRate / (double)windowSize; + + //required for conversions to & from MEL scale + this.FreqBinCount = windowSize / 2; if (this.Oblong == null) { @@ -326,9 +355,7 @@ public void SetTimeAndFreqScales(double frameOffset, double frameDuration, doubl { 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.FrameOffset = frameOffset; //frame duration in seconds this.FreqBinWidth = freqBinWidth; //required for freq-binID conversions if (this.Oblong == null) @@ -337,6 +364,37 @@ public void SetTimeAndFreqScales(double frameOffset, double frameDuration, doubl } } + /// + /// Converts the Hertz (frequency) bounds of an event to the frequency bin number. + /// The frequency bin is an index into the columns of the spectrogram data matrix. + /// Since the spectrogram data matrix is oriented with the origin at top left, + /// the low frequency bin will have a lower column index than the high freq bin. + /// + /// mel scale. + /// lower freq bound. + /// upper freq bound. + /// Nyquist freq in Herz. + /// frequency scale. + /// return bin index for lower freq bound. + /// return bin index for upper freq bound. + public static void ConvertHertzToFrequencyBin(bool doMelscale, int minFreq, int maxFreq, int nyquist, double binWidth, out int leftCol, out int rightCol) + { + if (doMelscale) + { + int binCount = (int)(nyquist / binWidth) + 1; + double maxMel = MFCCStuff.Mel(nyquist); + int melRange = (int)(maxMel - 0 + 1); + double binsPerMel = binCount / (double)melRange; + leftCol = (int)Math.Round(MFCCStuff.Mel(minFreq) * binsPerMel); + rightCol = (int)Math.Round(MFCCStuff.Mel(maxFreq) * binsPerMel); + } + else + { + leftCol = (int)Math.Round(minFreq / binWidth); + rightCol = (int)Math.Round(maxFreq / binWidth); + } + } + /// /// Calculates the matrix/image indices of the acoustic event, when given the time/freq scales. /// This method called only by previous method:- Acousticevent.SetTimeAndFreqScales(). @@ -345,11 +403,12 @@ public void SetTimeAndFreqScales(double frameOffset, double frameDuration, doubl /// public static Oblong ConvertEvent2Oblong(AcousticEvent ae) { - // Translate time dimension = frames = matrix rows. - Time2RowIDs(ae.TimeStart, ae.EventDurationSeconds, ae.FrameOffset, out var topRow, out var bottomRow); + // Translate time dimension (seconds) to frames to matrix rows. + var topRow = (int)Math.Round(ae.TimeStart / ae.FrameOffset); + var bottomRow = (int)Math.Round((ae.TimeStart + ae.EventDurationSeconds) / ae.FrameOffset); //Translate freq dimension = freq bins = matrix columns. - Freq2BinIDs(ae.IsMelscale, (int)ae.LowFrequencyHertz, (int)ae.HighFrequencyHertz, ae.FreqBinCount, ae.FreqBinWidth, out var leftCol, out var rightCol); + ConvertHertzToFrequencyBin(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); } @@ -377,21 +436,6 @@ public void SetScores(double score, double min, double max) } } - public string WriteProperties() - { - return " min-max=" + this.LowFrequencyHertz + "-" + this.HighFrequencyHertz + ", " + this.Oblong.ColumnLeft + "-" + this.Oblong.ColumnRight; - } - - /// - /// Draws an event on the image. Uses the fields already set on the audio event to determine correct placement. - /// Fields requireed to be set include: `FramesPerSecond`, `FreqBinWidth`. - /// - public void DrawEvent(Image sonogram) - where T : unmanaged, IPixel - { - this.DrawEvent(sonogram, this.FramesPerSecond, this.FreqBinWidth, sonogram.Height); - } - /// /// Draws an event on the image. Allows for custom specification of variables. /// Drawing the event requires a time scale and a frequency scale. Hence the additional arguments. @@ -417,6 +461,7 @@ public void DrawEvent(Image imageToReturn, double framesPerSecond, double if (duration >= 0.0 && framesPerSecond >= 0.0) { t1 = (int)Math.Round(this.TimeStart * framesPerSecond); + t2 = (int)Math.Round(this.TimeEnd * framesPerSecond); } else if (this.Oblong != null) @@ -426,8 +471,7 @@ public void DrawEvent(Image imageToReturn, double framesPerSecond, double t2 = this.Oblong.RowBottom; } - imageToReturn.Mutate(g => g.DrawRectangle(borderPen, t1, y1, t2, y2)); - + imageToReturn.Mutate(g => g.NoAA().DrawRectangle(borderPen, t1, y1, t2, y2)); if (this.HitElements != null) { foreach (var hitElement in this.HitElements) @@ -441,135 +485,83 @@ public void DrawEvent(Image imageToReturn, double framesPerSecond, double int scoreHt = (int)Math.Round(eventHeight * this.ScoreNormalised); imageToReturn.Mutate(g => { - g.DrawLine(scorePen, t1, y2 - scoreHt, t1, y2); + g.NoAA().DrawLine(scorePen, t1, y2 - scoreHt, t1, y2 + 1); g.DrawTextSafe(this.Name, Drawing.Tahoma6, Color.Black, new PointF(t1, y1 - 4)); }); } - /// - /// Passed point is relative to top-left corner of the Acoustic Event. - /// Oblong needs to be set for this method to work. - /// - public void DrawPoint(Image bmp, Point point, Color colour) - { - if (bmp == null) - { - return; - } - - int maxFreqBin = (int)Math.Round(this.HighFrequencyHertz / this.FreqBinWidth); - int row = bmp.Height - maxFreqBin - 1 + point.Y; - int t1 = (int)Math.Round(this.TimeStart * this.FramesPerSecond); // temporal start of event - int col = t1 + point.X; - if (row >= bmp.Height) - { - row = bmp.Height - 1; - } - - bmp[col, row] = colour; - } + //################################################################################################################# + //FOLLOWING METHODS DEAL WITH THE OVERLAP OF EVENTS /// - /// Returns the first event in the passed list which overlaps with this one IN THE SAME RECORDING. - /// If no event overlaps return null. + /// Determines if two events overlap in frequency. /// - public AcousticEvent OverlapsEventInList(List events) + /// event one. + /// event two. + /// true if events overlap. + public static bool EventsOverlapInFrequency(AcousticEvent event1, AcousticEvent event2) { - foreach (AcousticEvent ae in events) + //check if event 1 freq band overlaps event 2 freq band + if (event1.HighFrequencyHertz >= event2.LowFrequencyHertz && event1.HighFrequencyHertz <= event2.HighFrequencyHertz) { - if (this.FileName.Equals(ae.FileName) && this.Overlaps(ae)) - { - return ae; - } + return true; } - return null; - } - - /// - /// Returns true/false if this event time-overlaps the passed event. - /// - public bool Overlaps(AcousticEvent ae) - { - return EventsOverlap(this, ae); - } - - /// - /// Returns the fractional overlap of two events. - /// Translate time/freq dimensions to coordinates in a matrix. - /// Freq dimension = bins = matrix columns. Origin is top left - as per matrix in the sonogram class. - /// Time dimension = frames = matrix rows. - /// - public static double EventFractionalOverlap(AcousticEvent event1, AcousticEvent event2) - { - //if (event1.EndTime < event2.StartTime) return 0.0; - //if (event2.EndTime < event1.StartTime) return 0.0; - //if (event1.MaxFreq < event2.MinFreq) return 0.0; - //if (event2.MaxFreq < event1.MinFreq) return 0.0; - //at this point the two events do overlap - - int timeOverlap = Oblong.RowOverlap(event1.Oblong, event2.Oblong); - if (timeOverlap == 0) + // check if event 1 freq band overlaps event 2 freq band + if (event1.LowFrequencyHertz >= event2.LowFrequencyHertz && event1.LowFrequencyHertz <= event2.HighFrequencyHertz) { - return 0.0; + return true; } - int hzOverlap = Oblong.ColumnOverlap(event1.Oblong, event2.Oblong); - if (hzOverlap == 0) + //check if event 2 freq band overlaps event 1 freq band + if (event2.HighFrequencyHertz >= event1.LowFrequencyHertz && event2.HighFrequencyHertz <= event1.HighFrequencyHertz) { - return 0.0; + return true; } - int overlapArea = timeOverlap * hzOverlap; - double fractionalOverlap1 = overlapArea / (double)event1.Oblong.Area(); - double fractionalOverlap2 = overlapArea / (double)event2.Oblong.Area(); - - if (fractionalOverlap1 > fractionalOverlap2) + // check if event 2 freq band overlaps event 1 freq band + if (event2.LowFrequencyHertz >= event1.LowFrequencyHertz && event2.LowFrequencyHertz <= event1.HighFrequencyHertz) { - return fractionalOverlap1; - } - else - { - return fractionalOverlap2; + return true; } + + return false; } /// - /// Determines if two events overlap in time or frequency or both. + /// Determines if two events overlap in time. /// /// event one. /// event two. /// true if events overlap. - public static bool EventsOverlap(AcousticEvent event1, AcousticEvent event2) + public static bool EventsOverlapInTime(AcousticEvent event1, AcousticEvent event2) { - var timeOverlap = false; - var freqOverlap = false; - //check if event 1 starts within event 2 if (event1.EventStartSeconds >= event2.EventStartSeconds && event1.EventStartSeconds <= event2.EventEndSeconds) { - timeOverlap = true; + return true; } // check if event 1 ends within event 2 if (event1.EventEndSeconds >= event2.EventStartSeconds && event1.EventEndSeconds <= event2.EventEndSeconds) { - timeOverlap = true; + return true; } - //check if event 1 freq band overlaps event 2 freq band - if (event1.HighFrequencyHertz >= event1.LowFrequencyHertz && event1.HighFrequencyHertz <= event2.HighFrequencyHertz) + // now check possibility that event2 is inside event1. + //check if event 2 starts within event 1 + if (event2.EventStartSeconds >= event1.EventStartSeconds && event2.EventStartSeconds <= event1.EventEndSeconds) { - freqOverlap = true; + return true; } - // check if event 1 freq band overlaps event 2 freq band - if (event1.LowFrequencyHertz >= event2.LowFrequencyHertz && event1.LowFrequencyHertz <= event2.HighFrequencyHertz) + // check if event 2 ends within event 1 + if (event2.EventEndSeconds >= event1.EventStartSeconds && event2.EventEndSeconds <= event1.EventEndSeconds) { - freqOverlap = true; + return true; } - return timeOverlap && freqOverlap; + return false; } /// @@ -578,7 +570,7 @@ public static bool EventsOverlap(AcousticEvent event1, AcousticEvent event2) /// Freq dimension = bins = matrix columns. Origin is top left - as per matrix in the sonogram class. /// Time dimension = frames = matrix rows. /// - public static List CombineOverlappingEvents(List events) + public static List CombineOverlappingEvents(List events, TimeSpan segmentStartOffset) { if (events.Count < 2) { @@ -589,9 +581,9 @@ public static List CombineOverlappingEvents(List e { for (int j = i - 1; j >= 0; j--) { - if (EventsOverlap(events[i], events[j])) + if (EventsOverlapInTime(events[i], events[j]) && EventsOverlapInFrequency(events[i], events[j])) { - events[j] = AcousticEvent.MergeTwoEvents(events[i], events[j]); + events[j] = AcousticEvent.MergeTwoEvents(events[i], events[j], segmentStartOffset); events.RemoveAt(i); break; } @@ -601,158 +593,76 @@ public static List CombineOverlappingEvents(List e return events; } - public static AcousticEvent MergeTwoEvents(AcousticEvent e1, AcousticEvent e2) + public static AcousticEvent MergeTwoEvents(AcousticEvent e1, AcousticEvent e2, TimeSpan segmentStartOffset) { - //e1.EventEndSeconds = Math.Max(e1.EventEndSeconds, e2.EventEndSeconds); - e1.EventStartSeconds = Math.Min(e1.EventStartSeconds, e2.EventStartSeconds); + //segmentStartOffset = TimeSpan.Zero; + var minTime = Math.Min(e1.TimeStart, e2.TimeStart); + var maxTime = Math.Max(e1.TimeEnd, e2.TimeEnd); + e1.SetEventPositionRelative(segmentStartOffset, minTime, maxTime); e1.LowFrequencyHertz = Math.Min(e1.LowFrequencyHertz, e2.LowFrequencyHertz); e1.HighFrequencyHertz = Math.Max(e1.HighFrequencyHertz, e2.HighFrequencyHertz); - - //e1.ResultMinute = (int)e1.ResultStartSeconds.Floor(); + e1.Score = Math.Max(e1.Score, e2.Score); + e1.ScoreNormalised = Math.Max(e1.ScoreNormalised, e2.ScoreNormalised); e1.ResultStartSeconds = e1.EventStartSeconds; return e1; } - //################################################################################################################# - //METHODS TO CONVERT BETWEEN FREQ BIN AND HERZ OR MELS - /// - /// converts frequency bounds of an event to left and right columns of object in sonogram matrix - /// NOTE: binCount is required only if freq is in Mel scale + /// Returns the first event in the passed list which overlaps with this one IN THE SAME RECORDING. + /// If no event overlaps return null. /// - /// mel scale - /// lower freq bound - /// upper freq bound - /// Nyquist freq in Herz - /// frequency scale - /// return bin index for lower freq bound - /// return bin index for upper freq bound - public static void Freq2BinIDs(bool doMelscale, int minFreq, int maxFreq, int nyquist, double binWidth, out int leftCol, out int rightCol) + public AcousticEvent OverlapsEventInList(List events) { - if (doMelscale) - { - Freq2MelsBinIDs(minFreq, maxFreq, binWidth, nyquist, out leftCol, out rightCol); - } - else + foreach (AcousticEvent ae in events) { - Freq2HerzBinIDs(minFreq, maxFreq, binWidth, out leftCol, out rightCol); + if (this.FileName.Equals(ae.FileName) && EventsOverlapInTime(this, ae)) + { + return ae; + } } - } - - public static void Freq2HerzBinIDs(int minFreq, int maxFreq, double binWidth, out int leftCol, out int rightCol) - { - leftCol = (int)Math.Round(minFreq / binWidth); - rightCol = (int)Math.Round(maxFreq / binWidth); - } - - public static void Freq2MelsBinIDs(int minFreq, int maxFreq, double binWidth, int nyquistFrequency, out int leftCol, out int rightCol) - { - int binCount = (int)(nyquistFrequency / binWidth) + 1; - double maxMel = MFCCStuff.Mel(nyquistFrequency); - int melRange = (int)(maxMel - 0 + 1); - double binsPerMel = binCount / (double)melRange; - leftCol = (int)Math.Round(MFCCStuff.Mel(minFreq) * binsPerMel); - rightCol = (int)Math.Round(MFCCStuff.Mel(maxFreq) * binsPerMel); - } - - //################################################################################################################# - //METHODS TO CONVERT BETWEEN TIME BIN AND SECONDS - - public static void Time2RowIDs(double startTime, double duration, double frameOffset, out int topRow, out int bottomRow) - { - topRow = (int)Math.Round(startTime / frameOffset); - bottomRow = (int)Math.Round((startTime + duration) / frameOffset); - } - - public void SetNetIntensityAfterNoiseReduction(double mean, double var) - { - this.I3Mean = mean; - this.I3Var = var; - } - /// - /// returns the frame duration and offset duration in seconds. - /// - /// signal samples per second. - /// number of signal samples in one window or frame. - /// number of signal samples between start of one frame and start of next frame. - /// units = seconds. - /// units = second. - /// number of frames in one second. - public static void CalculateTimeScale(int samplingRate, int windowSize, int windowOffset, out double frameDuration, out double frameOffset, out double framesPerSecond) - { - frameDuration = windowSize / (double)samplingRate; - frameOffset = windowOffset / (double)samplingRate; - framesPerSecond = 1 / frameOffset; + return null; } + /* /// - /// return Nyquist / binCount. + /// This method not currently called but is POTENTIALLY USEFUL. + /// Returns the fractional overlap of two events. + /// Translate time/freq dimensions to coordinates in a matrix. + /// Freq dimension = bins = matrix columns. Origin is top left - as per matrix in the sonogram class. + /// Time dimension = frames = matrix rows. /// - public static void CalculateFreqScale(int samplingRate, int windowSize, out int binCount, out double binWidth) - { - binCount = windowSize / 2; - binWidth = samplingRate / (double)windowSize; - } - - public static void WriteEvents(List eventList, ref StringBuilder sb) + public static double EventFractionalOverlap(AcousticEvent event1, AcousticEvent event2) { - if (eventList.Count == 0) + int timeOverlap = Oblong.RowOverlap(event1.Oblong, event2.Oblong); + if (timeOverlap == 0) { - string line = - $"# Event Name\t{"Start",8:f3}\t{"End",6:f3}\t{"MinF"}\t{"MaxF"}\t{"Score1":f2}\t{"Score2":f1}\t{"SourceFile"}"; - sb.AppendLine(line); - line = $"{"NoEvent"}\t{0.000,8:f3}\t{0.000,8:f3}\t{"N/A"}\t{"N/A"}\t{0.000:f2}\t{0.000:f1}\t{"N/A"}"; - sb.AppendLine(line); + return 0.0; } - else + + int hzOverlap = Oblong.ColumnOverlap(event1.Oblong, event2.Oblong); + if (hzOverlap == 0) { - AcousticEvent ae1 = eventList[0]; - string line = - $"# Event Name\t{"Start",8:f3}\t{"End",6:f3}\t{"MinF"}\t{"MaxF"}\t{"Score":f2}\t{ae1.Score2Name:f1}\t{"SourceFile"}"; - sb.AppendLine(line); - foreach (AcousticEvent ae in eventList) - { - line = - $"{ae.Name}\t{ae.TimeStart,8:f3}\t{ae.TimeEnd,8:f3}\t{ae.LowFrequencyHertz}\t{ae.HighFrequencyHertz}\t{ae.Score:f2}\t{ae.Score2:f1}\t{ae.FileName}"; - sb.AppendLine(line); - } + return 0.0; } - } - /// - /// used to write lists of acousitc event data to an excell spread sheet. - /// - public static StringBuilder WriteEvents(List eventList, string str) - { - StringBuilder sb = new StringBuilder(); - if (eventList.Count == 0) + int overlapArea = timeOverlap * hzOverlap; + double fractionalOverlap1 = overlapArea / (double)event1.Oblong.Area(); + double fractionalOverlap2 = overlapArea / (double)event2.Oblong.Area(); + + if (fractionalOverlap1 > fractionalOverlap2) { - string line = string.Format( - str + "\t{0}\t{1,8:f3}\t{2,8:f3}\t{3}\t{4}\t{5:f2}\t{6:f1}\t{7}", "NoEvent", 0.000, 0.000, "N/A", "N/A", 0.000, 0.000, "N/A"); - sb.AppendLine(line); + return fractionalOverlap1; } else { - foreach (AcousticEvent ae in eventList) - { - string line = string.Format( - str + "\t{0}\t{1,8:f3}\t{2,8:f3}\t{3}\t{4}\t{5:f2}\t{6:f1}\t{7}", - ae.Name, - ae.TimeStart, - ae.TimeEnd, - ae.LowFrequencyHertz, - ae.HighFrequencyHertz, - ae.Score, - ae.Score2, - ae.FileName); - - sb.AppendLine(line); - } + return fractionalOverlap2; } - - return sb; } + */ + + //################################################################################################################# + //METHODS FOR SEGMENTATION OF A FREQ BAND BASED ON ACOUSTIC ENERGY /// /// Segments or not depending value of boolean doSegmentation. @@ -794,16 +704,37 @@ public static Tuple, double, double, double, double[]> GetSe return tuple; } - public static Tuple, double, double, double, double[]> GetSegmentationEvents(SpectrogramStandard sonogram, TimeSpan segmentStartOffset, - int minHz, int maxHz, double smoothWindow, double thresholdSD, double minDuration, double maxDuration) + /// + /// Segments the acoustic energy in the passed frequency band and returns as list of acoustic events. + /// Noise reduction is done first. + /// + /// the full spectrogram. + /// Start of current segment relative to the recording start. + /// Bottom of the required frequency band. + /// Top of the required frequency band. + /// To smooth the amplitude array. + /// Determines the threshold for an acoustic event. + /// Minimum duration of an acceptable acoustic event. + /// Maximum duration of an acceptable acoustic event. + /// a list of acoustic events. + public static Tuple, double, double, double, double[]> GetSegmentationEvents( + SpectrogramStandard sonogram, + TimeSpan segmentStartOffset, + int minHz, + int maxHz, + double smoothWindow, + double thresholdSD, + double minDuration, + double maxDuration) { int nyquist = sonogram.SampleRate / 2; var tuple = SNR.SubbandIntensity_NoiseReduced(sonogram.Data, minHz, maxHz, nyquist, smoothWindow, sonogram.FramesPerSecond); double[] intensity = tuple.Item1; //noise reduced intensity array - double Q = tuple.Item2; //baseline dB in the original scale + double baselineDb = tuple.Item2; //baseline dB in the original scale double oneSD = tuple.Item3; //1 SD in dB around the baseline double dBThreshold = thresholdSD * oneSD; + // get list of acoustic events var segmentEvents = ConvertIntensityArray2Events( intensity, segmentStartOffset, @@ -817,13 +748,16 @@ public static Tuple, double, double, double, double[]> GetSe foreach (AcousticEvent ev in segmentEvents) { ev.FileName = sonogram.Configuration.SourceFName; - - //ev.Name = callName; } - return Tuple.Create(segmentEvents, Q, oneSD, dBThreshold, intensity); + return Tuple.Create(segmentEvents, baselineDb, oneSD, dBThreshold, intensity); } + //############################################################################################################################################## + // THE NEXT FOUR METHODS ARE NOT CURRENTLY CALLED. + // THEY WERE USED FOR COLLECTING EVENTS INTO DATA SETS for Machine Learning purposes. (Kiwi publications) + // MAY BE USEFUL IN FUTURE + /// /// returns all the events in a list that occur in the recording with passed file name. /// @@ -839,7 +773,7 @@ public static List GetEventsInFile(List eventList, } return events; - } // end method GetEventsInFile(List eventList, string fileName) + } public static List GetTaggedEventsInFile(List labeledEvents, string filename) { @@ -919,7 +853,7 @@ public static void CalculateAccuracy(List results, List results, List results, List /// Given two lists of AcousticEvents, one being labelled events and the other being predicted events, @@ -988,8 +927,16 @@ 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) + 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) { //init values tp = 0; @@ -1042,7 +989,12 @@ public static void CalculateAccuracyOnOneRecording(List results, fn++; line = string.Format( "False NEGATIVE: {0,4} {5,15} {1,6:f1} ...{2,6:f1} intensity={3} quality={4}", - count, ae.TimeStart, ae.TimeEnd, ae.Intensity, ae.Quality, ae.Name); + count, + ae.TimeStart, + ae.TimeEnd, + ae.Intensity, + ae.Quality, + ae.Name); sb.Append(line + "\t" + ae.FileName + "\n"); } } @@ -1068,12 +1020,10 @@ public static void CalculateAccuracyOnOneRecording(List results, accuracy = (precision + recall) / 2; resultsText = sb.ToString(); - } //end method + } //############################################################################################################################################## -// THE NEXT THREE METHODS CONVERT BETWEEN SCORE ARRAYS AND ACOUSTIC EVENTS -// THE NEXT TWO METHOD CONVERT AN ARRAY OF SCORE (USUALLY INTENSITY VALUES IN A SUB-BAND) TO ACOUSTIC EVENTS. -// THE THIRD METHOD PRODUCES A SCORE ARRAY GIVEN A LIST OF EVENTS. +// THE NEXT THREE METHODS CONVERT AN ARRAY OF SCORE VALUES (USUALLY INTENSITY VALUES IN A SUB-BAND) TO ACOUSTIC EVENTS. public static List ConvertIntensityArray2Events( double[] values, @@ -1093,17 +1043,20 @@ public static List ConvertIntensityArray2Events( 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 && values[i] > scoreThreshold) //start of an event + //start of an event + if (isHit == false && values[i] > scoreThreshold) { isHit = true; startTime = i * frameOffset; startFrame = i; } else //check for the end of an event - if (isHit && 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; double duration = endTime - startTime; @@ -1114,9 +1067,10 @@ public static List ConvertIntensityArray2Events( continue; //skip events with duration shorter than threshold } - AcousticEvent ev = new AcousticEvent(segmentStartOffset, startTime, duration, minHz, maxHz); - - ev.Name = "Acoustic Segment"; //default name + AcousticEvent ev = new AcousticEvent(segmentStartOffset, startTime, duration, minHz, maxHz) + { + Name = "Acoustic Segment", //default name + }; ev.SetTimeAndFreqScales(framesPerSec, freqBinWidth); //obtain average intensity score. @@ -1135,7 +1089,7 @@ public static List ConvertIntensityArray2Events( } /// - /// Given a time series of acoustic amplitude (typically in decibels), this method finds events that match the passed constraints. + /// Given a time series of acoustic amplitude (typically in decibels), finds events that match the passed constraints. /// /// an array of amplitude values, typically decibel values. /// not sure what this is about!. @@ -1247,9 +1201,9 @@ public static List GetEventsAroundMaxima( /// The method uses the passed scoreThreshold in order to calculate a normalised score. /// Max possible score := threshold * 5. /// normalised score := score / maxPossibleScore. - /// Some analysis techniques (e.g. OD) have their own methods for extracting events from score arrays. + /// Some analysis techniques (e.g. Oscillation Detection) have their own methods for extracting events from score arrays. /// - /// the array of scores + /// 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. @@ -1257,7 +1211,7 @@ public static List GetEventsAroundMaxima( /// 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. + /// offset. /// a list of acoustic events. public static List ConvertScoreArray2Events( double[] scores, @@ -1268,14 +1222,14 @@ public static List ConvertScoreArray2Events( double scoreThreshold, double minDuration, double maxDuration, - TimeSpan segmentStartOffset) + TimeSpan segmentStart) { int count = scores.Length; var events = new List(); 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; + double frameOffset = 1 / framesPerSec; + double startTimeInSegment = 0.0; // units = seconds int startFrame = 0; // pass over all frames @@ -1285,7 +1239,7 @@ public static List ConvertScoreArray2Events( { //start of an event isHit = true; - startTime = i * frameOffset; + startTimeInSegment = i * frameOffset; startFrame = i; } else // check for the end of an event @@ -1294,12 +1248,13 @@ public static List ConvertScoreArray2Events( // this is end of an event, so initialise it isHit = false; double endTime = i * frameOffset; - double duration = endTime - startTime; + double duration = endTime - startTimeInSegment; // if (duration < minDuration) continue; //skip events with duration shorter than threshold if (duration < minDuration || duration > maxDuration) { - continue; //skip events with duration shorter than threshold + //skip events with duration shorter than threshold + continue; } // obtain an average score for the duration of the potential event. @@ -1311,18 +1266,13 @@ public static List ConvertScoreArray2Events( av /= i - startFrame + 1; - //NOTE av cannot be < threhsold because event started and ended based on threhsold. - // Therefore remove the following condition on 04/02/2020 - //if (av < scoreThreshold) - //{ - // continue; //skip events whose score is < the threshold - //} - - AcousticEvent ev = new AcousticEvent(segmentStartOffset, startTime, duration, minHz, maxHz); - + // Initialize the event. + AcousticEvent ev = new AcousticEvent(segmentStart, startTimeInSegment, duration, minHz, maxHz); ev.SetTimeAndFreqScales(framesPerSec, freqBinWidth); ev.Score = av; - ev.ScoreNormalised = ev.Score / maxPossibleScore; // normalised to the user supplied threshold + + // normalised to the user supplied threshold + ev.ScoreNormalised = ev.Score / maxPossibleScore; if (ev.ScoreNormalised > 1.0) { ev.ScoreNormalised = 1.0; @@ -1344,14 +1294,17 @@ public static List ConvertScoreArray2Events( events.Add(ev); } - } //end of pass over all frames + } return events; - } //end method ConvertScoreArray2Events() + } /// - /// Extracts an array of scores from a list of events. - /// The events are required to have the passed name. + /// FOR POSSIBLE DELETION! + /// THis method called only once from a frog recogniser class that is no longer used> LitoriaCaerulea:RecognizerBase. + /// THis method is potentially useful but can be deleted. + /// Attempts to reconstruct an array of scores from a list of acoustic events. + /// The events are required to have the passed name (a filter). /// 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) @@ -1365,12 +1318,12 @@ public static double[] ExtractScoreArrayFromEvents(List events, i double windowOffset = events[0].FrameOffset; double frameRate = 1 / windowOffset; //frames per second - //int count = events.Count; - foreach ( AcousticEvent ae in events) + foreach (AcousticEvent ae in events) { if (!ae.Name.Equals(nameOfTargetEvent)) { - continue; //skip irrelevant events + //skip irrelevant events + continue; } int startFrame = (int)(ae.TimeStart * frameRate); @@ -1386,39 +1339,7 @@ public static double[] ExtractScoreArrayFromEvents(List events, i } //############################################################################################################################################## - - /// - /// TODO: THis should be deprecated! - /// 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}"); - var benchmarkDir = new DirectoryInfo(testDir + "\\ExpectedOutput"); - if (!benchmarkDir.Exists) - { - benchmarkDir.Create(); - } - - var benchmarkFilePath = Path.Combine(benchmarkDir.FullName, fileName + ".TestEvents.csv"); - var eventsFilePath = Path.Combine(testDir.FullName, fileName + ".Events.csv"); - var eventsFile = new FileInfo(eventsFilePath); - Csv.WriteToCsv(eventsFile, events); - - LoggedConsole.WriteLine($"# EVENTS TEST: Comparing List of {testName} events with those in benchmark file:"); - var benchmarkFile = new FileInfo(benchmarkFilePath); - if (!benchmarkFile.Exists) - { - LoggedConsole.WriteWarnLine(" A file of test/benchmark events does not exist. Writing output as future events-test file"); - Csv.WriteToCsv(benchmarkFile, events); - } - else - { - // compare the test events with benchmark - TestTools.FileEqualityTest("Compare acoustic events.", eventsFile, benchmarkFile); - } - } + // METHODS to CLUSTER acoustic events /// /// Although not currently used, this method and following methods could be useful in future for clustering of events. diff --git a/src/AudioAnalysisTools/StandardSpectrograms/Image_MultiTrack.cs b/src/AudioAnalysisTools/StandardSpectrograms/Image_MultiTrack.cs index 1e36e0e25..f3d3bfd7e 100644 --- a/src/AudioAnalysisTools/StandardSpectrograms/Image_MultiTrack.cs +++ b/src/AudioAnalysisTools/StandardSpectrograms/Image_MultiTrack.cs @@ -353,33 +353,42 @@ public Image OverlayRedTransparency(Image bmp) } /// - /// superimposes a matrix of scores on top of a sonogram. - /// TODO: WARNING: THIS METHOD IS YET TO BE DEBUGGED SINCE TRANSITION TO SIX-LABOURS, FEB 2020. + /// It is assumed that the spectrogram image is grey scale. + /// NOTE: The score matrix must consist of reals in [0.0, 1.0]. + /// NOTE: The image and the score matrix must have the same number of rows and columns. + /// In case of a spectrogram, it is assumed that the rows are frequency bins and the columns are individual spectra. /// + /// the spectrogram image. + /// the matrix of scores or hits. public static Image OverlayScoresAsRedTransparency(Image bmp, double[,] hits) { Image newBmp = (Image)bmp.Clone(); int rows = hits.GetLength(0); int cols = hits.GetLength(1); - int imageHt = bmp.Height - 1; //subtract 1 because indices start at zero - //traverse columns - skip DC column - for (int c = 1; c < cols; c++) + if (rows != bmp.Height || cols != bmp.Width) { - for (int r = 0; r < rows; r++) + LoggedConsole.WriteErrorLine("ERROR: Image and hits matrix do not have the same dimensions."); + return bmp; + } + + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) { - if (hits[r, c] == 0.0) + if (hits[r, c] <= 0.0) { continue; } - var pixel = bmp[r, imageHt - c]; - if (pixel.R == 255) + if (hits[r, c] > 1.0) { - continue; // white + hits[r, c] = 1.0; } - newBmp[r, imageHt - c] = Color.FromRgb(255, pixel.G, pixel.B); + var pixel = bmp[r, c]; + byte value = (byte)Math.Floor(hits[r, c] * 255); + newBmp[r, c] = Color.FromRgb(value, pixel.G, pixel.B); } } @@ -387,8 +396,54 @@ public static Image OverlayScoresAsRedTransparency(Image bmp, doub } /// - /// superimposes a matrix of scores on top of a sonogram. USES RAINBOW PALLETTE. - /// ASSUME MATRIX NORMALIZED IN [0,1]. + /// Overlays a matrix of scores on an image, typically a spectrogram image. + /// It is assumed that the spectrogram image is grey scale. + /// NOTE: The score matrix must consist of integers from 0 to 255. + /// NOTE: The image and the score matrix must have the same number of rows and columns. + /// In case of a spectrogram, it is assumed that the rows are frequency bins and the columns are individual spectra. + /// + /// the spectrogram image. + /// the matrix of scores or hits. + /// The new image with overlay of scores as red transparency. + public static Image OverlayScoresAsRedTransparency(Image bmp, int[,] hits) + { + Image newBmp = (Image)bmp.Clone(); + int rows = hits.GetLength(0); + int cols = hits.GetLength(1); + + if (rows != bmp.Height || cols != bmp.Width) + { + LoggedConsole.WriteErrorLine("ERROR: Image and hits matrix do not have the same dimensions."); + return bmp; + } + + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + if (hits[r, c] <= 0) + { + continue; + } + + if (hits[r, c] > 255) + { + hits[r, c] = 255; + } + + var pixel = bmp[c, r]; + newBmp[c, r] = Color.FromRgb((byte)hits[r, c], pixel.G, pixel.B); + } + } + + return newBmp; + } + + /// + /// WARNING: THis method is yet to be debugged and tested following move to SixLabors drawing libraries. + /// superimposes a matrix of scores on top of a sonogram. + /// USES A PALLETTE of ten RAINBOW colours. + /// ASSUME MATRIX is NORMALIZED IN [0,1]. /// public void OverlayRainbowTransparency(IImageProcessingContext g, Image bmp) { diff --git a/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramSettings.cs b/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramSettings.cs index 7d2ce4252..820e0d524 100644 --- a/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramSettings.cs +++ b/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramSettings.cs @@ -5,7 +5,7 @@ namespace AudioAnalysisTools.StandardSpectrograms { using System; - using DSP; + using AudioAnalysisTools.DSP; using TowseyLibrary; public class SpectrogramSettings @@ -16,6 +16,9 @@ public class SpectrogramSettings /// public string SourceFileName { get; set; } + /// + /// Gets or sets the window or frame size. 512 is usually a suitable choice for recordings of the environment. + /// public int WindowSize { get; set; } = 512; public double WindowOverlap { get; set; } = 0.0; @@ -26,8 +29,18 @@ public class SpectrogramSettings /// public int WindowStep { get; set; } = 512; - public string WindowFunction { get; set; } = WindowFunctions.HAMMING.ToString(); + /// + /// Gets or sets the default FFT Window function to the Hanning window. + /// THe Hanning window was made default in March 2020 because it was found to produce better spectrograms + /// in cases where the recording is resampled up or down. + /// + public string WindowFunction { get; set; } = WindowFunctions.HANNING.ToString(); + /// + /// Gets or sets the smoothing window. + /// Following the FFT, each spectrum is smoothed with a moving average filter to reduce its variance. + /// We do the minimum smoothing in order to retain spectral definition. + /// public int SmoothingWindow { get; set; } = 3; public bool DoMelScale { get; set; } = false; @@ -49,6 +62,10 @@ public class SpectrogramAttributes public double MaxAmplitude { get; set; } + /// + /// Gets or sets the maximum frequency that can be represented given the signals sampling rate. + /// The Nyquist is half the SR. + /// public int NyquistFrequency { get; set; } public TimeSpan Duration { get; set; } @@ -56,7 +73,7 @@ public class SpectrogramAttributes public int FrameCount { get; set; } /// - /// Gets or sets duration of full frame or window in seconds + /// Gets or sets duration of full frame or window in seconds. /// public TimeSpan FrameDuration { get; set; } @@ -64,9 +81,15 @@ public class SpectrogramAttributes public double FBinWidth { get; set; } - //this.FBinWidth = this.NyquistFrequency / (double) this.FreqBinCount; + /// + /// Gets or sets the real value difference between two adjacent values of the 16, 24 bit signed integer, + /// used to represent the signal amplitude in the range -1 to +1. + /// public double Epsilon { get; set; } + /// + /// Gets or sets the signal power added by using the chosen FFT window function. + /// public double WindowPower { get; set; } /// diff --git a/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramTools.cs b/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramTools.cs index 8d940389a..b0cd931f4 100644 --- a/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramTools.cs +++ b/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramTools.cs @@ -125,8 +125,6 @@ public static Image GetSonogramPlusCharts( if (hits != null) { spectrogram = Image_MultiTrack.OverlayScoresAsRedTransparency(spectrogram, hits); - //OverlayRedTransparency(bmp, this.SuperimposedRedTransparency); - //this.SonogramImage = this.OverlayRedTransparency((Image)this.SonogramImage); } int pixelWidth = spectrogram.Width; @@ -638,14 +636,14 @@ public static Tuple HistogramOfSpectralPeaks(double[,] spectrogram public static double[,] ExtractFreqSubband(double[,] m, int minHz, int maxHz, bool doMelscale, int binCount, double binWidth) { - AcousticEvent.Freq2BinIDs(doMelscale, minHz, maxHz, binCount, binWidth, out var c1, out var c2); + AcousticEvent.ConvertHertzToFrequencyBin(doMelscale, minHz, maxHz, binCount, binWidth, out var c1, out var c2); return DataTools.Submatrix(m, 0, c1, m.GetLength(0) - 1, c2); } public static double[] ExtractModalNoiseSubband(double[] modalNoise, int minHz, int maxHz, bool doMelScale, int nyquist, double binWidth) { //extract subband modal noise profile - AcousticEvent.Freq2BinIDs(doMelScale, minHz, maxHz, nyquist, binWidth, out var c1, out var c2); + AcousticEvent.ConvertHertzToFrequencyBin(doMelScale, minHz, maxHz, nyquist, binWidth, out var c1, out var c2); int subbandCount = c2 - c1 + 1; var subband = new double[subbandCount]; for (int i = 0; i < subbandCount; i++) diff --git a/tests/Acoustics.Test/AudioAnalysisTools/AcousticEventTests.cs b/tests/Acoustics.Test/AudioAnalysisTools/AcousticEventTests.cs new file mode 100644 index 000000000..ed2c2c573 --- /dev/null +++ b/tests/Acoustics.Test/AudioAnalysisTools/AcousticEventTests.cs @@ -0,0 +1,117 @@ +// +// 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.AudioAnalysisTools +{ + using System; + using System.Collections.Generic; + using Acoustics.Shared.ImageSharp; + using Acoustics.Test.TestHelpers; + using global::AudioAnalysisTools; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using SixLabors.ImageSharp; + using SixLabors.ImageSharp.PixelFormats; + + [TestClass] + public class AcousticEventTests : GeneratedImageTest + { + [TestMethod] + public void TestEventMerging() + { + // make a list of events + var events = new List(); + + double maxPossibleScore = 10.0; + var event1 = new AcousticEvent(segmentStartOffset: TimeSpan.Zero, eventStartSegmentRelative: 1.0, eventDuration: 5.0, minFreq: 1000, maxFreq: 8000) + { + Name = "Event1", + Score = 1.0, + ScoreNormalised = 1.0 / maxPossibleScore, + }; + + events.Add(event1); + + var event2 = new AcousticEvent(segmentStartOffset: TimeSpan.Zero, eventStartSegmentRelative: 4.5, eventDuration: 2.0, minFreq: 1500, maxFreq: 6000) + { + Name = "Event2", + Score = 5.0, + ScoreNormalised = 5.0 / maxPossibleScore, + }; + events.Add(event2); + + var event3 = new AcousticEvent(segmentStartOffset: TimeSpan.Zero, eventStartSegmentRelative: 7.0, eventDuration: 2.0, minFreq: 1000, maxFreq: 8000) + { + Name = "Event3", + Score = 9.0, + ScoreNormalised = 9.0 / maxPossibleScore, + }; + events.Add(event3); + + // combine adjacent acoustic events + events = AcousticEvent.CombineOverlappingEvents(events: events, segmentStartOffset: TimeSpan.Zero); + + Assert.AreEqual(2, events.Count); + Assert.AreEqual(1.0, events[0].EventStartSeconds, 1E-4); + Assert.AreEqual(6.5, events[0].EventEndSeconds, 1E-4); + Assert.AreEqual(7.0, events[1].EventStartSeconds, 1E-4); + Assert.AreEqual(9.0, events[1].EventEndSeconds, 1E-4); + + Assert.AreEqual(1000, events[0].LowFrequencyHertz); + Assert.AreEqual(8000, events[0].HighFrequencyHertz); + Assert.AreEqual(1000, events[1].LowFrequencyHertz); + Assert.AreEqual(8000, events[1].HighFrequencyHertz); + + Assert.AreEqual(5.0, events[0].Score, 1E-4); + Assert.AreEqual(9.0, events[1].Score, 1E-4); + Assert.AreEqual(0.5, events[0].ScoreNormalised, 1E-4); + Assert.AreEqual(0.9, events[1].ScoreNormalised, 1E-4); + } + + [TestMethod] + public void TestSonogramWithEventsOverlay() + { + // make a substitute sonogram image + var substituteSonogram = Drawing.NewImage(100, 256, Color.Black); + + // make a list of events + var framesPerSecond = 10.0; + var freqBinWidth = 43.0664; + double maxPossibleScore = 10.0; + + var events = new List(); + var event1 = new AcousticEvent(segmentStartOffset: TimeSpan.Zero, eventStartSegmentRelative: 1.0, eventDuration: 5.0, minFreq: 1000, maxFreq: 8000) + { + Score = 10.0, + Name = "Event1", + ScoreNormalised = 10.0 / maxPossibleScore, + }; + + events.Add(event1); + var event2 = new AcousticEvent(segmentStartOffset: TimeSpan.Zero, eventStartSegmentRelative: 7.0, eventDuration: 2.0, minFreq: 1000, maxFreq: 8000) + { + Score = 1.0, + Name = "Event2", + ScoreNormalised = 1.0 / maxPossibleScore, + }; + events.Add(event2); + + // now add events into the spectrogram image. + // set color for the events + foreach (AcousticEvent ev in events) + { + // because we are testing placement of box not text. + ev.Name = string.Empty; + ev.BorderColour = AcousticEvent.DefaultBorderColor; + ev.ScoreColour = AcousticEvent.DefaultScoreColor; + ev.DrawEvent(substituteSonogram, framesPerSecond, freqBinWidth, 256); + } + + this.Actual = substituteSonogram; + + // BUG: this asset is faulty. See https://github.com/QutEcoacoustics/audio-analysis/issues/300#issuecomment-601537263 + this.Expected = Image.Load(PathHelper.ResolveAssetPath("EventTests_SuperimposeEventsOnImage.png")); + this.AssertImagesEqual(); + } + } +} diff --git a/tests/Acoustics.Test/AudioAnalysisTools/EventStatistics/EventStatisticsCalculateTests.cs b/tests/Acoustics.Test/AudioAnalysisTools/EventStatistics/EventStatisticsCalculateTests.cs index f43c73762..b0cad3c33 100644 --- a/tests/Acoustics.Test/AudioAnalysisTools/EventStatistics/EventStatisticsCalculateTests.cs +++ b/tests/Acoustics.Test/AudioAnalysisTools/EventStatistics/EventStatisticsCalculateTests.cs @@ -16,11 +16,10 @@ namespace Acoustics.Test.AudioAnalysisTools.EventStatistics [TestClass] public class EventStatisticsCalculateTests { - [TestMethod] public void TestCalculateEventStatistics() { - int sampleRate = 22050; + var sampleRate = 22050; double duration = 28; int[] harmonics1 = { 500 }; int[] harmonics2 = { 500, 1000, 2000, 4000, 8000 }; @@ -37,8 +36,8 @@ public void TestCalculateEventStatistics() var start = TimeSpan.FromSeconds(28) + segmentOffset; var end = TimeSpan.FromSeconds(32) + segmentOffset; - double lowFreq = 1500.0; - double topFreq = 8500.0; + var lowFreq = 1500.0; + var topFreq = 8500.0; var statsConfig = new EventStatisticsConfiguration() { @@ -46,7 +45,7 @@ public void TestCalculateEventStatistics() FrameStep = 512, }; - EventStatistics stats = + var stats = EventStatisticsCalculate.AnalyzeAudioEvent( recording, (start, end).AsRange(), diff --git a/tests/Acoustics.Test/AudioAnalysisTools/StandardSpectrograms/SonogramTests.cs b/tests/Acoustics.Test/AudioAnalysisTools/StandardSpectrograms/SonogramTests.cs index 2986aab4d..b35ee9f63 100644 --- a/tests/Acoustics.Test/AudioAnalysisTools/StandardSpectrograms/SonogramTests.cs +++ b/tests/Acoustics.Test/AudioAnalysisTools/StandardSpectrograms/SonogramTests.cs @@ -17,6 +17,8 @@ namespace Acoustics.Test.AudioAnalysisTools.StandardSpectrograms using global::TowseyLibrary; using Microsoft.VisualStudio.TestTools.UnitTesting; using SixLabors.ImageSharp; + using SixLabors.ImageSharp.PixelFormats; + using SixLabors.ImageSharp.Processing; using Path = System.IO.Path; /// @@ -90,7 +92,7 @@ public void Cleanup() /// /// METHOD TO CHECK that averaging of decibel values is working. /// var array = new[] { 96.0, 100.0, 90.0, 97.0 }; - /// The return value should = 96.98816759 dB + /// The return value should = 96.98816759 dB. /// [TestMethod] public void TestAverageOfDecibelValues() @@ -219,5 +221,47 @@ public void TestAnnotatedSonogramWithPlots() Assert.AreEqual(1621, image.Width); Assert.AreEqual(656, image.Height); } + + [TestMethod] + public void TestSonogramHitsOverlay() + { + int width = 100; + int height = 256; + + // make a substitute sonogram image + var pretendSonogram = new Image(width, height); + + // make a hits matrix with crossed diagonals + var hitsMatrix = new int[height, width]; + for (int i = 0; i < height; i++) + { + int col = (int)Math.Floor(width * i / (double)height); + int intensity = col; + hitsMatrix[i, col] = i; + hitsMatrix[i, width - col - 1] = i; + } + + // now add in hits to the spectrogram image. + if (hitsMatrix != null) + { + pretendSonogram = Image_MultiTrack.OverlayScoresAsRedTransparency(pretendSonogram, hitsMatrix); + } + + //pretendSonogram.Save("C:\\temp\\image.png"); + var pixel = new Argb32(255, 0, 0); + var expectedColor = new Color(pixel); + var actualColor = pretendSonogram[0, height - 1]; + Assert.AreEqual(expectedColor, actualColor); + + pixel = new Argb32(128, 0, 0); + expectedColor = new Color(pixel); + actualColor = pretendSonogram[width / 2, height / 2]; + Assert.AreEqual(expectedColor, actualColor); + + pixel = new Argb32(0, 0, 0); + expectedColor = new Color(pixel); + actualColor = pretendSonogram[0, 0]; + Assert.AreEqual(expectedColor, actualColor); + } } } diff --git a/tests/Fixtures/EventTests_SuperimposeEventsOnImage.png b/tests/Fixtures/EventTests_SuperimposeEventsOnImage.png new file mode 100644 index 000000000..b45eba20e --- /dev/null +++ b/tests/Fixtures/EventTests_SuperimposeEventsOnImage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fde5d7e3427a88eae8774bebb0b986b301edee6808d37bc41a3479d7320cc6e +size 699