diff --git a/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.NinoxBoobook.yml b/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.NinoxBoobook.yml new file mode 100644 index 000000000..fd4da317c --- /dev/null +++ b/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.NinoxBoobook.yml @@ -0,0 +1,58 @@ +--- + +# Resample rate must be 2 X the desired Nyquist +#ResampleRate: 22050 +#ResampleRate: 16000 +# SegmentDuration: units=seconds; +SegmentDuration: 60 +# SegmentOverlap: units=seconds; +SegmentOverlap: 0 + +# Each of these profiles will be analyzed +Profiles: + PeakTrackSyllable: !ForwardTrackParameters + ComponentName: RidgeTrack + SpeciesName: NinoxBoobook + FrameSize: 1024 + FrameStep: 256 + WindowFunction: HANNING + BgNoiseThreshold: 0.0 + # min and max of the freq band to search + MinHertz: 300 + MaxHertz: 1000 + MinDuration: 0.15 + MaxDuration: 0.5 + DecibelThreshold: 6.0 + +#Combine each pair of Boobook syllables as one event +#CombineProximalSimilarEvents: false +CombinePossibleSyllableSequence: true +SyllableStartDifference: 0.6 +SyllableHertzGap: 200 + +#CombineOverlappedEvents: false +# Common settings +#Standard: &STANDARD +#EventThreshold: 0.2 +#BgNoiseThreshold: 3.0 + +# This notation means the a profile has all of the settings that the Standard profile has, +# however, the DctDuration parameter has been overridden. +# <<: *STANDARD +# DctDuration: 0.3 + +# Available options (case-sensitive): [False/Never | True/Always | WhenEventsDetected] +SaveIntermediateWavFiles: Never +SaveIntermediateCsvFiles: false +# Available options (case-sensitive): [False/Never | True/Always | WhenEventsDetected] +# "True" is useful when debugging but "WhenEventsDetected" is required for operational use. +SaveSonogramImages: True +#SaveSonogramImages: WhenEventsDetected +# DisplayCsvImage is obsolete - ensure it remains set to: false +DisplayCsvImage: false +## End section for AnalyzeLongRecording + +# Other config files to reference + +HighResolutionIndicesConfig: "../Towsey.Acoustic.HiResIndicesForRecognisers.yml" +... \ No newline at end of file diff --git a/src/AnalysisPrograms/Recognizers/Birds/NinoxBoobook.cs b/src/AnalysisPrograms/Recognizers/Birds/NinoxBoobook.cs index e27efbd72..7af75b689 100644 --- a/src/AnalysisPrograms/Recognizers/Birds/NinoxBoobook.cs +++ b/src/AnalysisPrograms/Recognizers/Birds/NinoxBoobook.cs @@ -2,14 +2,6 @@ // 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). // -/// -/// A recognizer for the Australian Boobook Owl, /// https://en.wikipedia.org/wiki/Australian_boobook . -/// Eight subspecies of the Australian boobook are recognized, -/// with three further subspecies being reclassified as separate species in 2019 due to their distinctive calls and genetics. -/// THis recognizer has been trained on good quality calls from the Gympie recordings obtained by Yvonne Phillips. -/// The recognizer has also been run across several recordings of Boobook from NZ (recordings obtained from Stuart Parsons. -/// The NZ Boobook calls were of poor quality (distant and echo) and were 200 Hertz higher and performance was not good. -/// namespace AnalysisPrograms.Recognizers { using System; @@ -17,10 +9,13 @@ namespace AnalysisPrograms.Recognizers using System.IO; using System.Linq; using System.Reflection; + using System.Runtime.CompilerServices; using Acoustics.Shared.ConfigFile; + using AnalysisBase; using AnalysisPrograms.Recognizers.Base; using AudioAnalysisTools; using AudioAnalysisTools.DSP; + using AudioAnalysisTools.Events; using AudioAnalysisTools.Events.Types; using AudioAnalysisTools.Indices; using AudioAnalysisTools.StandardSpectrograms; @@ -28,8 +23,17 @@ namespace AnalysisPrograms.Recognizers using log4net; using SixLabors.ImageSharp; using TowseyLibrary; + using static AnalysisPrograms.Recognizers.GenericRecognizer; using Path = System.IO.Path; + /// + /// A recognizer for the Australian Boobook Owl, /// https://en.wikipedia.org/wiki/Australian_boobook . + /// Eight subspecies of the Australian boobook are recognized, + /// with three further subspecies being reclassified as separate species in 2019 due to their distinctive calls and genetics. + /// THis recognizer has been trained on good quality calls from the Gympie recordings obtained by Yvonne Phillips. + /// The recognizer has also been run across several recordings of Boobook from NZ (recordings obtained from Stuart Parsons. + /// The NZ Boobook calls were of poor quality (distant and echo) and were 200 Hertz higher and performance was not good. + /// internal class NinoxBoobook : RecognizerBase { private static readonly ILog BoobookLog = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); @@ -40,113 +44,108 @@ internal class NinoxBoobook : RecognizerBase public override string Description => "[ALPHA] Detects acoustic events for the Australian Boobook owl."; - /* - /// - /// Summarize your results. This method is invoked exactly once per original file. - /// - public override void SummariseResults( - AnalysisSettings settings, - FileSegment inputFileSegment, - EventBase[] events, - SummaryIndexBase[] indices, - SpectralIndexBase[] spectralIndices, - AnalysisResult2[] results) + public override AnalyzerConfig ParseConfig(FileInfo file) { - // No operation - do nothing. Feel free to add your own logic. - base.SummariseResults(settings, inputFileSegment, events, indices, spectralIndices, results); + RuntimeHelpers.RunClassConstructor(typeof(NinoxBoobookConfig).TypeHandle); + var config = ConfigFile.Deserialize(file); + + // validation of configs can be done here + GenericRecognizer.ValidateProfileTagsMatchAlgorithms(config.Profiles, file); + + // This call sets a restriction so that only one generic algorithm is used. + // CHANGE this to accept multiple generic algorithms as required. + //if (result.Profiles.SingleOrDefault() is ForwardTrackParameters) + if (config.Profiles?.Count == 1 && config.Profiles.First().Value is ForwardTrackParameters) + { + return config; + } + + throw new ConfigFileException("NinoxBoobook expects one and only one ForwardTrack algorithm.", file); } - */ /// /// This method is called once per segment (typically one-minute segments). /// /// one minute of audio recording. - /// config file that contains parameters used by all profiles. + /// config file that contains parameters used by all profiles. /// when recording starts. /// not sure what this is. /// where the recognizer results can be found. /// assuming ????. /// recognizer results. - public override RecognizerResults Recognize(AudioRecording audioRecording, Config genericConfig, TimeSpan segmentStartOffset, Lazy getSpectralIndexes, DirectoryInfo outputDirectory, int? imageWidth) + public override RecognizerResults Recognize( + AudioRecording audioRecording, + Config config, + TimeSpan segmentStartOffset, + Lazy getSpectralIndexes, + DirectoryInfo outputDirectory, + int? imageWidth) { + //class NinoxBoobookConfig is define at bottom of this file. + var genericConfig = (NinoxBoobookConfig)config; var recognizer = new GenericRecognizer(); - var config = recognizer.ParseConfig(FileInfo file); - /* - string[] profileNames = null; - if (ConfigFile.HasProfiles(genericConfig)) - { - profileNames = ConfigFile.GetProfileNames(genericConfig); - int count = profileNames.Length; - var message = $"Found {count} config profile(s): "; - foreach (string s in profileNames) - { - message = message + (s + ", "); - } - - BoobookLog.Debug(message); - } - else - { - BoobookLog.Warn("No configuration profiles found."); - } + RecognizerResults combinedResults = recognizer.Recognize( + audioRecording, + genericConfig, + segmentStartOffset, + getSpectralIndexes, + outputDirectory, + imageWidth); + + // DO POST-PROCESSING of EVENTS - var combinedResults = new RecognizerResults(); + // Convert events to spectral events for possible combining. + // Combine overlapping events. If the dB threshold is set low, may get lots of little events. + // + var events = combinedResults.NewEvents; + var spectralEvents = events.Select(x => (SpectralEvent)x).ToList(); + var newEvents = CompositeEvent.CombineOverlappingEvents(spectralEvents); - foreach (var profileName in profileNames) + if (genericConfig.CombinePossibleSyllableSequence) { - var results = new RecognizerResults(); - - if (ConfigFile.TryGetProfile(genericConfig, profileName, out var profile)) - { - results = recognizer.(audioRecording, genericConfig, "Territorial", segmentStartOffset); - BoobookLog.Debug("Boobook event count = " + results.Events.Count); - } - else - { - BoobookLog.Warn($"Could not access {profileName} configuration parameters"); - } - - // combine the results i.e. add wing-beat events to the list of territorial call events. - //NOTE: The returned territorialResults and wingbeatResults will never be null. - combinedResults.Events.AddRange(results.Events); - combinedResults.Plots.AddRange(results.Plots); + // convert events to spectral events for possible combining. + //var spectralEvents = events.Select(x => (SpectralEvent)x).ToList(); + spectralEvents = newEvents.Cast().ToList(); + var startDiff = genericConfig.SyllableStartDifference; + var hertzDiff = genericConfig.SyllableHertzGap; + newEvents = CompositeEvent.CombineSimilarProximalEvents(spectralEvents, TimeSpan.FromSeconds(startDiff), (int)hertzDiff); } + combinedResults.NewEvents = newEvents; + //UNCOMMENT following line if you want special debug spectrogram, i.e. with special plots. // NOTE: Standard spectrograms are produced by setting SaveSonogramImages: "True" or "WhenEventsDetected" in config file. - //SaveDebugSpectrogram(territorialResults, genericConfig, outputDirectory, audioRecording.BaseName); - */ + //GenericRecognizer.SaveDebugSpectrogram(territorialResults, genericConfig, outputDirectory, audioRecording.BaseName); return combinedResults; } + /* /// - /// returns a base sonogram type from which spectrogram images are prepared. + /// Summarize your results. This method is invoked exactly once per original file. /// - internal static BaseSonogram GetSonogram(Config configuration, AudioRecording audioRecording) + public override void SummariseResults( + AnalysisSettings settings, + FileSegment inputFileSegment, + EventBase[] events, + SummaryIndexBase[] indices, + SpectralIndexBase[] spectralIndices, + AnalysisResult2[] results) { - var sonoConfig = new SonogramConfig - { - WindowSize = 512, - NoiseReductionType = NoiseReductionType.Standard, - NoiseReductionParameter = configuration.GetDoubleOrNull(AnalysisKeys.NoiseBgThreshold) ?? 0.0, - WindowOverlap = 0.0, - }; - - // now construct the standard decibel spectrogram WITH noise removal - // get frame parameters for the analysis - var sonogram = (BaseSonogram)new SpectrogramStandard(sonoConfig, audioRecording.WavReader); - return sonogram; + // No operation - do nothing. Feel free to add your own logic. + base.SummariseResults(settings, inputFileSegment, events, indices, spectralIndices, results); } + */ - /// - /// THis method can be modified if want to do something non-standard with the output spectrogram. - /// - internal static void SaveDebugSpectrogram(RecognizerResults results, Config genericConfig, DirectoryInfo outputDirectory, string baseName) + /// /> + public class NinoxBoobookConfig : GenericRecognizerConfig, INamedProfiles { - //var image = sonogram.GetImageFullyAnnotated("Test"); - var image = SpectrogramTools.GetSonogramPlusCharts(results.Sonogram, results.Events, results.Plots, null); - image.Save(Path.Combine(outputDirectory.FullName, baseName + ".profile.png")); + //public bool CombineOverlappedEvents { get; set; } + public bool CombinePossibleSyllableSequence { get; set; } = false; + + public double SyllableStartDifference { get; set; } = 0.5; + + public double SyllableHertzGap { get; set; } = 200; } } } \ No newline at end of file diff --git a/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs b/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs index 9dd409d8d..44b2ba486 100644 --- a/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs +++ b/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs @@ -51,11 +51,17 @@ public override AnalyzerConfig ParseConfig(FileInfo file) RuntimeHelpers.RunClassConstructor(typeof(GenericRecognizerConfig).TypeHandle); var result = ConfigFile.Deserialize(file); - this.combineOverlappedEvents = result.CombineOverlappedEvents; + // validation of configs can be done here + ValidateProfileTagsMatchAlgorithms(result.Profiles, file); + + return result; + } + public static void ValidateProfileTagsMatchAlgorithms(Dictionary profiles, FileInfo file) + { // validation of configs can be done here // sanity check the algorithm - foreach (var (profileName, profile) in result.Profiles) + foreach (var (profileName, profile) in profiles) { if (profile is CommonParameters c) { @@ -103,8 +109,6 @@ public override AnalyzerConfig ParseConfig(FileInfo file) throw new ConfigFileException($"The algorithm type in profile {profileName} is not recognized. It must be one of {allowedAlgorithms}"); } } - - return result; } /// @@ -118,7 +122,7 @@ public override RecognizerResults Recognize( { var configuration = (GenericRecognizerConfig)genericConfig; - if (configuration.Profiles.NotNull() && configuration.Profiles.Count == 0) + if (configuration.Profiles?.Count < 1) { throw new ConfigFileException( "The generic recognizer needs at least one profile set. 0 were found."); @@ -358,19 +362,14 @@ public override void SummariseResults( } */ - private static SonogramConfig ParametersToSonogramConfig(CommonParameters common) + /// + /// THis method can be modified if want to do something non-standard with the output spectrogram. + /// + public static void SaveDebugSpectrogram(RecognizerResults results, Config genericConfig, DirectoryInfo outputDirectory, string baseName) { - int windowSize = (int)common.FrameSize; - int windowStep = (int)common.FrameStep; - return new SonogramConfig() - { - WindowSize = windowSize, - WindowStep = windowStep, - WindowOverlap = (windowSize - windowStep) / (double)windowSize, - WindowFunction = (string)common.WindowFunction, - NoiseReductionType = NoiseReductionType.Standard, - NoiseReductionParameter = common.BgNoiseThreshold ?? 0.0, - }; + var image3 = SpectrogramTools.GetSonogramPlusCharts(results.Sonogram, results.NewEvents, results.Plots, null); + + image3.Save(Path.Combine(outputDirectory.FullName, baseName + ".profile.png")); } /// @@ -390,21 +389,24 @@ private static Plot PreparePlot(double[] array, string title, double threshold) return plot; } - /// - /// THis method can be modified if want to do something non-standard with the output spectrogram. - /// - public static void SaveDebugSpectrogram(RecognizerResults results, Config genericConfig, DirectoryInfo outputDirectory, string baseName) + private static SonogramConfig ParametersToSonogramConfig(CommonParameters common) { - var image3 = SpectrogramTools.GetSonogramPlusCharts(results.Sonogram, results.NewEvents, results.Plots, null); - - image3.Save(Path.Combine(outputDirectory.FullName, baseName + ".profile.png")); + int windowSize = (int)common.FrameSize; + int windowStep = (int)common.FrameStep; + return new SonogramConfig() + { + WindowSize = windowSize, + WindowStep = windowStep, + WindowOverlap = (windowSize - windowStep) / (double)windowSize, + WindowFunction = (string)common.WindowFunction, + NoiseReductionType = NoiseReductionType.Standard, + NoiseReductionParameter = common.BgNoiseThreshold ?? 0.0, + }; } - /// /> + /// /> public class GenericRecognizerConfig : RecognizerConfig, INamedProfiles { - public bool CombineOverlappedEvents { get; set; } - /// public Dictionary Profiles { get; set; } } diff --git a/src/AudioAnalysisTools/Events/EventCommon.cs b/src/AudioAnalysisTools/Events/EventCommon.cs index 2274cf360..b3e75f5a9 100644 --- a/src/AudioAnalysisTools/Events/EventCommon.cs +++ b/src/AudioAnalysisTools/Events/EventCommon.cs @@ -33,6 +33,8 @@ public abstract class EventCommon : EventBase, IDrawableEvent /// public string ComponentName => this.GetType().Name; + public override double ResultStartSeconds => this.EventStartSeconds; + /// /// Gets or sets the event score. /// This is a score in absolute units as determined by context. diff --git a/src/AudioAnalysisTools/Events/Types/CompositeEvent.cs b/src/AudioAnalysisTools/Events/Types/CompositeEvent.cs index 9fd2b397b..77c62ed57 100644 --- a/src/AudioAnalysisTools/Events/Types/CompositeEvent.cs +++ b/src/AudioAnalysisTools/Events/Types/CompositeEvent.cs @@ -28,8 +28,6 @@ public CompositeEvent(List events) public override double EventStartSeconds => this.ComponentEvents.Min(x => x.EventStartSeconds); - public override double ResultStartSeconds => this.EventStartSeconds; - public override double EventEndSeconds => this.ComponentEvents.Max(x => (x as ITemporalEvent)?.EventEndSeconds) ?? double.PositiveInfinity; @@ -155,11 +153,11 @@ public static bool EventsOverlapInTime(SpectralEvent event1, SpectralEvent event /// /// Combines overlapping events in the passed List of events and returns a reduced list. /// - public static List CombineOverlappingEvents(List events) + public static List CombineOverlappingEvents(List events) { if (events.Count < 2) { - return events; + return events.Cast().ToList(); } for (int i = events.Count - 1; i >= 0; i--) @@ -176,7 +174,7 @@ public static List CombineOverlappingEvents(List e } } - return events; + return events.Cast().ToList(); } /// diff --git a/src/AudioAnalysisTools/Tracks/ForwardTrackAlgorithm.cs b/src/AudioAnalysisTools/Tracks/ForwardTrackAlgorithm.cs index a5afbc93d..1995ca45c 100644 --- a/src/AudioAnalysisTools/Tracks/ForwardTrackAlgorithm.cs +++ b/src/AudioAnalysisTools/Tracks/ForwardTrackAlgorithm.cs @@ -10,6 +10,7 @@ namespace AudioAnalysisTools.Tracks using System.Text; using AnalysisPrograms.Recognizers.Base; using AudioAnalysisTools.Events; + using AudioAnalysisTools.Events.Drawing; using AudioAnalysisTools.Events.Tracks; using AudioAnalysisTools.Events.Types; using AudioAnalysisTools.StandardSpectrograms; @@ -71,8 +72,11 @@ public static (List Events, double[] CombinedIntensity) GetForwardT // list of accumulated acoustic events var events = new List(); var combinedIntensityArray = new double[frameCount]; + //IImageProcessingContext graphics, + //EventRenderingOptions options foreach (var track in tracks) - { + { + //track.Draw(imageProcessingContest, EventRenderingOptions options); var maxScore = decibelThreshold * 5; var ae = new ChirpEvent(track, maxScore) { diff --git a/tests/Acoustics.Test/AudioAnalysisTools/AcousticEventTests.cs b/tests/Acoustics.Test/AudioAnalysisTools/AcousticEventTests.cs index c664c1d21..24addd8f7 100644 --- a/tests/Acoustics.Test/AudioAnalysisTools/AcousticEventTests.cs +++ b/tests/Acoustics.Test/AudioAnalysisTools/AcousticEventTests.cs @@ -6,6 +6,7 @@ namespace Acoustics.Test.AudioAnalysisTools { using System; using System.Collections.Generic; + using System.Linq; using Acoustics.Shared.ImageSharp; using Acoustics.Test.TestHelpers; using global::AudioAnalysisTools; @@ -49,7 +50,8 @@ public void TestEventMerging() events.Add(event3); // combine Overlapping acoustic events - events = CompositeEvent.CombineOverlappingEvents(events: events); + var newEvents = CompositeEvent.CombineOverlappingEvents(events: events); + events = newEvents.Cast().ToList(); //require two events, the first being a composite of two events. Assert.AreEqual(2, events.Count);