diff --git a/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.CisticolaExilis.yml b/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.CisticolaExilis.yml new file mode 100644 index 000000000..a2dcd592e --- /dev/null +++ b/src/AnalysisConfigFiles/RecognizerConfigFiles/Towsey.CisticolaExilis.yml @@ -0,0 +1,71 @@ +--- + +# Golden-headed cisticola = Cisticola exilis +# Resample rate must be 2 X the desired Nyquist +ResampleRate: 8000 +# 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: + CisticolaSyllable: !UpwardTrackParameters + ComponentName: Whip + SpeciesName: CisticolaExilis + FrameSize: 512 + FrameStep: 256 + WindowFunction: HANNING + #BgNoiseThreshold: 0.0 + # min and max of the freq band to search + MinHertz: 100 + MaxHertz: 200 + MinDuration: 0.3 + MaxDuration: 1.0 + DecibelThreshold: 6.0 + +#################### POST-PROCESSING of EVENTS ################### + +# A: First post-processing steps are to combine overlapping/proximal/sequential events +# 1: Combine overlapping events +#CombineOverlappingEvents: false + +# 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: true +SyllableStartDifference: 3.0 +SyllableHertzGap: 35 + +# B: Filter the events for excess activity in their upper and lower buffer zones +LowerHertzBuffer: 150 +UpperHertzBuffer: 400 + +# C: Options to save results files +# 4: Available options for saving data files (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. + +# 5: Available options for saving +#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" + +################################################################################ +# 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 +... \ No newline at end of file diff --git a/src/AnalysisPrograms/Recognizers/Birds/CisticolaExilis.cs b/src/AnalysisPrograms/Recognizers/Birds/CisticolaExilis.cs new file mode 100644 index 000000000..f92e48767 --- /dev/null +++ b/src/AnalysisPrograms/Recognizers/Birds/CisticolaExilis.cs @@ -0,0 +1,219 @@ +// +// 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.Events.Types; + using AudioAnalysisTools.Indices; + using AudioAnalysisTools.WavTools; + using log4net; + using SixLabors.ImageSharp; + using TowseyLibrary; + using static AnalysisPrograms.Recognizers.GenericRecognizer; + using Path = System.IO.Path; + + /// + /// A recognizer for calls of the golden-headed cisticola (Cisticola exilis), https://en.wikipedia.org/wiki/Golden-headed_cisticola . + /// It is a species of warbler in the family Cisticolidae, found in Australia and 13 Asian countries. + /// Grows to 9–11.5 centimetres (3.5–4.5 in) long, it is usually brown and cream in colour. + /// It produces a variety of calls distinct from other birds, which, according to the Sunshine Coast Council, range from a "teewip" to a "wheezz, whit-whit". + /// It has a very large range and population, which is thought to be increasing. It is not a threatened species. + /// + internal class CisticolaExilis : RecognizerBase + { + private static readonly ILog CisticolaLog = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + public override string Author => "Towsey"; + + public override string SpeciesName => "CisticolaExilis"; + + public override string Description => "[ALPHA] Detects acoustic events for the golden-headed cisticola."; + + public override AnalyzerConfig ParseConfig(FileInfo file) + { + RuntimeHelpers.RunClassConstructor(typeof(CisticolaExilisConfig).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("CisticolaExilis 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 CisticolaExilisConfig is defined at bottom of this file. + var genericConfig = (CisticolaExilisConfig)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(); + + // 1: Pull out the chirp events and calculate their frequency profiles. + var (chirpEvents, others) = combinedResults.NewEvents.FilterForEventType(); + + if (combinedResults.NewEvents.Count == 0) + { + CisticolaLog.Debug($"Return zero events."); + return combinedResults; + } + + // 2: Combine overlapping events. If the dB threshold is set low, may get lots of little events. + combinedResults.NewEvents = CompositeEvent.CombineOverlappingEvents(chirpEvents.Cast().ToList()); + CisticolaLog.Debug($"Event count after combining overlaps = {combinedResults.NewEvents.Count}"); + + // 3: Combine proximal events. If the dB threshold is set low, may get lots of little events. + if (genericConfig.CombinePossibleSyllableSequence) + { + // Convert events to spectral events for combining of possible sequences. + // Can also use this parameter to combine events that are in the upper or lower neighbourhood. + // Such combinations will increase bandwidth of the event and this property can be used later to weed out unlikely events. + var spectralEvents1 = combinedResults.NewEvents.Cast().ToList(); + var startDiff = genericConfig.SyllableStartDifference; + var hertzDiff = genericConfig.SyllableHertzGap; + combinedResults.NewEvents = CompositeEvent.CombineProximalEvents(spectralEvents1, TimeSpan.FromSeconds(startDiff), (int)hertzDiff); + CisticolaLog.Debug($"Event count after combining proximals = {combinedResults.NewEvents.Count}"); + } + + // Get the CisticolaSyllable config. + const string profileName = "CisticolaSyllable"; + var configuration = (CisticolaExilisConfig)genericConfig; + var chirpConfig = (ForwardTrackParameters)configuration.Profiles[profileName]; + + // 4: Filter events on the amount of acoustic activity in their upper and lower neighbourhoods - their buffer zone. + // The idea is that an unambiguous event should have some acoustic space above and below. + // The filter requires that the average acoustic activity in each frame and bin of the upper and lower buffer zones should not exceed the user specified decibel threshold. + // The bandwidth of these two neighbourhoods is determined by the following parameters. + // ########## These parameters could be specified by user in config.yml file. + var upperHertzBuffer = 400; + var lowerHertzBuffer = 150; + + // The decibel threshold is currently set 5/6ths of the user specified threshold. + // THIS IS TO BE WATCHED. IT MAY PROVE TO BE INAPPROPRIATE TO HARD-CODE. + // Want the activity in buffer zones to be "somewhat" less than the user-defined threshold. + var neighbourhoodDbThreshold = chirpConfig.DecibelThreshold.Value * 0.8333; + + if (upperHertzBuffer > 0 || lowerHertzBuffer > 0) + { + var spectralEvents2 = combinedResults.NewEvents.Cast().ToList(); + combinedResults.NewEvents = EventExtentions.FilterEventsOnNeighbourhood( + spectralEvents2, + combinedResults.Sonogram, + lowerHertzBuffer, + upperHertzBuffer, + segmentStartOffset, + neighbourhoodDbThreshold); + + CisticolaLog.Debug($"Event count after filtering on neighbourhood = {combinedResults.NewEvents.Count}"); + } + + if (combinedResults.NewEvents.Count == 0) + { + CisticolaLog.Debug($"Return zero events."); + return combinedResults; + } + + // 5: Filter on COMPONENT COUNT in Composite events. + int maxComponentCount = 2; + combinedResults.NewEvents = EventExtentions.FilterEventsOnCompositeContent(combinedResults.NewEvents, maxComponentCount); + CisticolaLog.Debug($"Event count after filtering on component count = {combinedResults.NewEvents.Count}"); + + // 6: Filter the events for duration in seconds + 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); + CisticolaLog.Debug($"Event count after filtering on duration = {combinedResults.NewEvents.Count}"); + + // 7: Filter the events for bandwidth in Hertz + double average = 280; + double sd = 40; + double sigmaThreshold = 3.0; + combinedResults.NewEvents = EventExtentions.FilterOnBandwidth(combinedResults.NewEvents, average, sd, sigmaThreshold); + CisticolaLog.Debug($"Event count after filtering on bandwidth = {combinedResults.NewEvents.Count}"); + + //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; + } + + /* + /// + /// 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 CisticolaExilisConfig : GenericRecognizerConfig, INamedProfiles + { + public bool CombinePossibleSyllableSequence { get; set; } = false; + + public double SyllableStartDifference { get; set; } = 0.5; + + public double SyllableHertzGap { get; set; } = 200; + } + } +} diff --git a/tests/Acoustics.Test/AnalysisPrograms/Recognizers/CisticolaTests.cs b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/CisticolaTests.cs new file mode 100644 index 000000000..b956a8444 --- /dev/null +++ b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/CisticolaTests.cs @@ -0,0 +1,72 @@ +// +// 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 = Golden-headed cisticola = Cisticola exilis. + /// + [TestClass] + public class CisticolaTests : OutputDirectoryTest + { + /// + /// The canonical recording used for this recognizer is a 31 second recording . + /// + private static readonly FileInfo TestAsset = PathHelper.ResolveAsset("Recordings", "gympie_np_1192_331618_20150818_054959_31_0.wav"); + private static readonly FileInfo ConfigFile = PathHelper.ResolveConfigFile("RecognizerConfigFiles", "Towsey.CisticolaExilis.yml"); + private static readonly AudioRecording Recording = new AudioRecording(TestAsset); + private static readonly CisticolaExilis Recognizer = new CisticolaExilis(); + + [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(8, events.Count); + Assert.IsNull(scoreTrack); + Assert.AreEqual(1, plots.Count); + Assert.AreEqual(2667, 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