diff --git a/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.NinoxStrenua.yml b/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.NinoxStrenua.yml new file mode 100644 index 000000000..6165a1fc7 --- /dev/null +++ b/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.NinoxStrenua.yml @@ -0,0 +1,60 @@ +--- + +# Powerful Owl = Towsey.NinoxStrenua +# Resample rate must be 2 X the desired Nyquist +ResampleRate: 16000 +# SegmentDuration: units=seconds; +SegmentDuration: 60 +# SegmentOverlap: units=seconds; +SegmentOverlap: 0 + +# Each of these profiles will be analyzed +# This profile is required for the species-specific recogniser and must have the current name. +Profiles: + StrenuaSyllable: !ForwardTrackParameters + ComponentName: RidgeTrack + SpeciesName: NinoxStrenua + FrameSize: 1024 + FrameStep: 256 + WindowFunction: HANNING + # min and max of the freq band to search + MinHertz: 300 + MaxHertz: 600 + MinDuration: 0.3 + MaxDuration: 1.5 + DecibelThreshold: 12.0 + +#################### POST-PROCESSING of EVENTS ################### + +# A: First post-processing steps are to combine overlapping/proximal/sequential events +# 1: Combine overlapping events +CombineOverlappingEvents: true + +# 2: Combine each pair of Boobook syllables as one event +# Can also use this to "mop up" events in neighbourhood - these can be removed later. +CombinePossibleSyllableSequence: false +SyllableStartDifference: 1.5 +SyllableHertzGap: 300 + +# B: Filter the events for excess activity in their upper and lower buffer zones +NeighbourhoodLowerHertzBuffer: 100 +NeighbourhoodUpperHertzBuffer: 300 +NeighbourhoodDbThreshold: 12.0 + +# C: Options to save results files +# 4: Available options for saving spectrograms (case-sensitive): [False/Never | True/Always | WhenEventsDetected] +# "True" is useful when debugging but "WhenEventsDetected" is required for operational use. +#SaveSonogramImages: True +SaveSonogramImages: WhenEventsDetected + +# 5: Available options for saving data files (case-sensitive): [False/Never | True/Always | WhenEventsDetected] +SaveIntermediateWavFiles: Never +SaveIntermediateCsvFiles: false + +# 6: 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/NinoxStrenua.cs b/src/AnalysisPrograms/Recognizers/Birds/NinoxStrenua.cs new file mode 100644 index 000000000..3fc0c6e3e --- /dev/null +++ b/src/AnalysisPrograms/Recognizers/Birds/NinoxStrenua.cs @@ -0,0 +1,270 @@ +// +// 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 AnalysisPrograms.Recognizers +{ + using System; + using System.Collections.Generic; + 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.Events; + using AudioAnalysisTools.Indices; + using AudioAnalysisTools.WavTools; + using log4net; + using TowseyLibrary; + using static AnalysisPrograms.Recognizers.GenericRecognizer; + using Path = System.IO.Path; + + /// + /// A recognizer for the Australian Powerful Owl, https://en.wikipedia.org/wiki/Powerful_owl. + /// The owl is so named because it is the largest of the Australian owls and it preys on large marsupials such as possums. + /// Its range is confined to the East and SE coast of Australia. + /// Its conservation status is "threatened". + /// This recognizer has been trained on good quality calls provided by NSW DPI by Brad Law and Kristen Thompson. + /// + internal class NinoxStrenua : RecognizerBase + { + private static readonly ILog PowerfulOwlLog = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + public override string Author => "Towsey"; + + public override string SpeciesName => "NinoxStrenua"; + + public override string Description => "[ALPHA] Detects acoustic events for the Australian Powerful Owl."; + + public override AnalyzerConfig ParseConfig(FileInfo file) + { + RuntimeHelpers.RunClassConstructor(typeof(NinoxStrenuaConfig).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("NinoxStrenua 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. + /// 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 config, + TimeSpan segmentStartOffset, + Lazy getSpectralIndexes, + DirectoryInfo outputDirectory, + int? imageWidth) + { + //class NinoxStrenuaConfig is defined at bottom of this file. + var genericConfig = (NinoxStrenuaConfig)config; + var recognizer = new GenericRecognizer(); + + RecognizerResults combinedResults = recognizer.Recognize( + audioRecording, + genericConfig, + segmentStartOffset, + getSpectralIndexes, + outputDirectory, + imageWidth); + + // ################### POST-PROCESSING of EVENTS ################### + // Following two commented lines are different ways of casting lists. + //var newEvents = spectralEvents.Cast().ToList(); + //var spectralEvents = events.Select(x => (SpectralEvent)x).ToList(); + + if (combinedResults.NewEvents.Count == 0) + { + PowerfulOwlLog.Debug($"Return zero events."); + return combinedResults; + } + + // 1: Filter the events for duration in seconds + // Get the PowerfulOwl Syllable config. + const string profileName = "StrenuaSyllable"; + var configuration = (NinoxStrenuaConfig)genericConfig; + var chirpConfig = (ForwardTrackParameters)configuration.Profiles[profileName]; + var minimumEventDuration = chirpConfig.MinDuration; + var maximumEventDuration = chirpConfig.MaxDuration; + if (genericConfig.CombinePossibleSyllableSequence) + { + minimumEventDuration *= 2.0; + maximumEventDuration *= 1.5; + } + + combinedResults.NewEvents = EventExtentions.FilterOnDuration(combinedResults.NewEvents, minimumEventDuration.Value, maximumEventDuration.Value); + PowerfulOwlLog.Debug($"Event count after filtering on duration = {combinedResults.NewEvents.Count}"); + + // 2: Filter the events for bandwidth in Hertz + double average = 400; + double sd = 50; + double sigmaThreshold = 3.0; + //combinedResults.NewEvents = EventExtentions.FilterOnBandwidth(combinedResults.NewEvents, average, sd, sigmaThreshold); + PowerfulOwlLog.Debug($"Event count after filtering on bandwidth = {combinedResults.NewEvents.Count}"); + + // 3: Filter on COMPONENT COUNT in Composite events. + int maxComponentCount = 5; + combinedResults.NewEvents = EventExtentions.FilterEventsOnCompositeContent(combinedResults.NewEvents, maxComponentCount); + PowerfulOwlLog.Debug($"Event count after filtering on component count = {combinedResults.NewEvents.Count}"); + + // 4: Pull out the chirp events and calculate their frequency profiles. + var (chirpEvents, others) = combinedResults.NewEvents.FilterForEventType(); + + // Uncomment the next line when want to obtain the event frequency profiles. + // WriteFrequencyProfiles(chirpEvents); + foreach (var ev in chirpEvents) + { + // Calculate frequency profile score for event + SetFrequencyProfileScore((ChirpEvent)ev); + } + + if (combinedResults.NewEvents.Count == 0) + { + PowerfulOwlLog.Debug($"Return zero events."); + return combinedResults; + } + + //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 UserName.SpeciesName.yml config file. + //GenericRecognizer.SaveDebugSpectrogram(territorialResults, genericConfig, outputDirectory, audioRecording.BaseName); + return combinedResults; + } + + /// + /// The Powerful Owl call syllable is shaped like an inverted "U". Its total duration is close to 0.15 seconds. + /// The rising portion lasts for 0.06s, followed by a turning portion, 0.03s, followed by the decending portion of 0.06s. + /// The constants for this method were obtained from the calls in a Gympie recording obtained by Yvonne Phillips. + /// + /// An event containing at least one forward track i.e. a chirp. + public static void SetFrequencyProfileScore(ChirpEvent ev) + { + const double risingDuration = 0.06; + const double gapDuration = 0.03; + const double fallingDuration = 0.06; + + var track = ev.Tracks.First(); + var profile = track.GetTrackFrequencyProfile().ToArray(); + + // get the first point + var firstPoint = track.Points.First(); + var frameDuration = firstPoint.Seconds.Maximum - firstPoint.Seconds.Minimum; + var risingFrameCount = (int)Math.Floor(risingDuration / frameDuration); + var gapFrameCount = (int)Math.Floor(gapDuration / frameDuration); + var fallingFrameCount = (int)Math.Floor(fallingDuration / frameDuration); + + var startSum = 0.0; + if (profile.Length >= risingFrameCount) + { + for (var i = 0; i <= risingFrameCount; i++) + { + startSum += profile[i]; + } + } + + int startFrame = risingFrameCount + gapFrameCount; + int endFrame = startFrame + fallingFrameCount; + var endSum = 0.0; + if (profile.Length >= endFrame) + { + for (var i = startFrame; i <= endFrame; i++) + { + endSum += profile[i]; + } + } + + // set score to 1.0 if the profile has inverted U shape. + double score = 0.0; + if (startSum > 0.0 && endSum < 0.0) + { + score = 1.0; + } + + ev.FrequencyProfileScore = score; + } + + /// + /// WARNING - this method assumes that the rising and falling parts of a Powerful Owl call syllable last for 5 frames. + /// + /// List of spectral events. + public static void WriteFrequencyProfiles(List events) + { + /* Here are the frequency profiles of some events. + * Note that the first five frames (0.057 seconds) have positive slope and subsequent frames have negative slope. + * The final frames are likely to be echo and to be avoided. + * Therefore take the first 0.6s to calculate the positive slope, leave a gap of 0.025 seconds and then get negative slope from the next 0.6 seconds. +42,21,21,42,21, 00, 21,-21,-21,-21, 00,-21,-42 +42,42,21,21,42,-21, 21, 00,-21,-21,-21,-21, 00,-21,21,-21 +42,42,21,21,42, 00, 00, 00,-21,-21,-21,-21,-21 +21,21,00,00,21, 21,-21, 00, 00,-21, 00,-21,-21,21,-21,42 +42,42,21,00,42, 00, 00,-21,-21,-21,-21, 00,-21, +21,42,21,21,21, 00,-21,-21,-21, 00,-21,-21 +42,21,21,42,21, 21, 00,-21,-21,-21,-21 +42,42,21,42,00, 00,-21, 00,-21,-21, 00,-21,-21 +*/ + + var spectralEvents = events.Select(x => (ChirpEvent)x).ToList(); + foreach (var ev in spectralEvents) + { + foreach (var track in ev.Tracks) + { + var profile = track.GetTrackFrequencyProfile().ToArray(); + var startSum = 0.0; + if (profile.Length >= 5) + { + startSum = profile[0] + profile[1] + profile[2] + profile[3] + profile[4]; + } + + var endSum = 0.0; + if (profile.Length >= 11) + { + endSum = profile[6] + profile[7] + profile[8] + profile[9] + profile[10]; + } + + LoggedConsole.WriteLine($"{startSum} {endSum}"); + LoggedConsole.WriteLine(DataTools.WriteArrayAsCsvLine(profile, "F0")); + } + } + } + +/* +/// +/// 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) +{ + // No operation - do nothing. Feel free to add your own logic. + base.SummariseResults(settings, inputFileSegment, events, indices, spectralIndices, results); +} +*/ + + public class NinoxStrenuaConfig : GenericRecognizerConfig, INamedProfiles + { + } + } +} diff --git a/tests/Acoustics.Test/AnalysisPrograms/Recognizers/PowerfulOwlTests.cs b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/PowerfulOwlTests.cs new file mode 100644 index 000000000..c24438129 --- /dev/null +++ b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/PowerfulOwlTests.cs @@ -0,0 +1,74 @@ +// +// All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). +// + +namespace Acoustics.Test.AnalysisPrograms.Recognizers +{ + using System; + using System.Collections.Generic; + using System.IO; + using Acoustics.Test.TestHelpers; + using Acoustics.Tools.Wav; + using global::AnalysisPrograms.Recognizers; + using global::AnalysisPrograms.SourcePreparers; + using global::AudioAnalysisTools.Events; + using global::AudioAnalysisTools.Events.Types; + using global::AudioAnalysisTools.WavTools; + using global::TowseyLibrary; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// Species name = Powerful Owl = Ninox strenua. + /// + [TestClass] + public class PowerfulOwlTests : OutputDirectoryTest + { + /// + /// The canonical recording used for this recognizer is at: + /// "C:\Ecoacoustics\WavFiles\PowerfulOwl_NinoxStrenua\XC269666_PowerfulOwl_NinoxStrenua.mp3". + /// + //private static readonly FileInfo TestAsset = PathHelper.ResolveAsset("Recordings", "gympie_np_1192_331618_20150818_054959_31_0.wav"); + private static readonly FileInfo TestAsset = new FileInfo(@"C:\Ecoacoustics\WavFiles\PowerfulOwl_NinoxStrenua\XC269666_PowerfulOwl_NinoxStrenua.wav"); + private static readonly FileInfo ConfigFile = PathHelper.ResolveConfigFile("RecognizerConfigFiles", "Towsey.NinoxStrenua.yml"); + private static readonly AudioRecording Recording = new AudioRecording(TestAsset); + private static readonly NinoxStrenua Recognizer = new NinoxStrenua(); + + [TestMethod] + public void TestRecognizer() + { + var config = Recognizer.ParseConfig(ConfigFile); + + var results = Recognizer.Recognize( + audioRecording: Recording, + config: config, + segmentStartOffset: TimeSpan.Zero, + getSpectralIndexes: null, + outputDirectory: this.TestOutputDirectory, + imageWidth: null); + + var events = results.NewEvents; + var scoreTrack = results.ScoreTrack; + var plots = results.Plots; + var sonogram = results.Sonogram; + + this.SaveTestOutput( + outputDirectory => GenericRecognizer.SaveDebugSpectrogram(results, null, outputDirectory, Recognizer.SpeciesName)); + + Assert.AreEqual(14, events.Count); + Assert.IsNull(scoreTrack); + Assert.AreEqual(1, plots.Count); + Assert.AreEqual(4380, sonogram.FrameCount); + + Assert.IsInstanceOfType(events[1], typeof(CompositeEvent)); + + var secondEvent = (CompositeEvent)events[1]; + + Assert.AreEqual(5.375419501133787, secondEvent.EventStartSeconds); + Assert.AreEqual(6.0720181405895692, secondEvent.EventEndSeconds); + Assert.AreEqual(483, secondEvent.LowFrequencyHertz); + Assert.AreEqual(735, secondEvent.HighFrequencyHertz); + Assert.AreEqual(20.901882476071698, secondEvent.Score, TestHelper.AllowedDelta); + Assert.AreEqual(0.20786700431266195, secondEvent.ScoreNormalized, TestHelper.AllowedDelta); + } + } +} \ No newline at end of file