Skip to content

Commit

Permalink
Fixes bug in multi recogniser (#496)
Browse files Browse the repository at this point in the history
* Fixes bug in multi recogniser

The multirecogniser was not outputting any events after the component recognizers detected events.  The fix was simply to ensure NewEvents were aggregated as well as old style AcousticEvents.

Also:
- wrote tests for the multi recognizers
- ensured that multiple different ways of specifying sub-recognizers would work (name, soft-resolved config, absolute reference to files)
- also ensured configs were loaded before the main analysis starts - this should produce much nicer error messages and avoid cutting audio before telling the user about the inevitable failure.
- fixed the path to analysis programs in the test metdadata helper

Fixes #97

* Added some validation to MultiRecogniser AnalysisNames array

* Reverts change to MultiRecognizerConfig base class

It acts like a Recognizer and thus must be of type RecognizerConfig.

My attempt to generalize the concept in 9c95fa5 was misguided
  • Loading branch information
atruskie authored Jun 9, 2021
1 parent 2099caf commit 13a6960
Show file tree
Hide file tree
Showing 13 changed files with 253 additions and 44 deletions.
2 changes: 1 addition & 1 deletion src/AP.VersionBuild.targets
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<!-- Register our task that as something to run before standard build target -->
<Target Name="APVersionBeforeBuild" BeforeTargets="PrepareForBuild">
<!-- <Message Text="[APVersionBeforeBuild] Debug: $(RuntimeIdentifier)" Importance="High" /> -->
<Exec Command="pwsh –NonInteractive -noprofile $(ProjectDir)../git_version.ps1 -configuration $(Configuration) -self_contained $(SelfContained) -runtime_identifier '$(RuntimeIdentifier)'" ConsoleToMSBuild="True" EchoOff="false" StandardOutputImportance="low">
<Exec Command="pwsh –NonInteractive -noprofile $(ProjectDir)../git_version.ps1 -configuration $(Configuration) -self_contained $(SelfContained) -runtime_identifier &quot;$(RuntimeIdentifier)&quot; -target_framework &quot;$(TargetFramework.ToLowerInvariant())&quot;" ConsoleToMSBuild="True" EchoOff="false" StandardOutputImportance="low">
<Output TaskParameter="ConsoleOutput" ItemName="VersionMetadata" />
</Exec>
<!-- <Message Text="[APVersionBeforeBuild] %(VersionMetadata.Identity)" Importance="High" /> -->
Expand Down
3 changes: 3 additions & 0 deletions src/Acoustics.Shared/ConfigFile/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Acoustics.Shared.ConfigFile
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Acoustics.Shared.Contracts;

public class Config : IConfig
Expand Down Expand Up @@ -47,6 +48,8 @@ private enum MatchType

public string ConfigPath { get; set; }

public string ConfigDirectory => Path.GetDirectoryName(this.ConfigPath);

/// <summary>
/// Gets or sets the generic object graph that mirrors the configuration.
/// This object is ignored for JSON serialization in log dumping if
Expand Down
10 changes: 10 additions & 0 deletions src/Acoustics.Shared/Extensions/ExtensionsString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,16 @@ public static bool IsNotWhitespace(this string str)
return !string.IsNullOrWhiteSpace(str);
}

public static string StripEnd(this string str, string suffix, StringComparison comparison = default)
{
if (suffix != null && str.EndsWith(suffix, comparison))
{
return str.Substring(0, str.Length - suffix.Length);
}

return str;
}

public static string NormalizeToCrLf(this string str)
{
string normalized = Regex.Replace(str, @"\r\n|\n\r|\n|\r", "\r\n");
Expand Down
7 changes: 7 additions & 0 deletions src/Acoustics.Shared/Extensions/FileInfoExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ public static T RefreshInfo<T>(this T info)
return info;
}

public static FileInfo CopyTo(this FileInfo source, DirectoryInfo dest)
{
var result = Path.Combine(dest.FullName, source.Name);
source.CopyTo(result);
return result.ToFileInfo();
}

public static string BaseName(this FileInfo file)
{
return Path.GetFileNameWithoutExtension(file.Name);
Expand Down
2 changes: 1 addition & 1 deletion src/AnalysisBase/AnalyzerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class AnalyzerConfig
[Obsolete("The AnalysisName property is no longer used")]
public string AnalysisName { get; set; }

public double EventThreshold { get; set; } = EventThresholdDefault;
public virtual double EventThreshold { get; set; } = EventThresholdDefault;

/// <summary>
/// Gets or sets the length of audio block to process.
Expand Down
3 changes: 2 additions & 1 deletion src/AnalysisConfigFiles/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"cSpell.words": [
"Nyquist",
"Resample"
]
],
"python.pythonPath": "C:\\Python37\\python.exe"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# LIST OF REQUIRED SPECIES RECOGNISERS
# The items in this list need to match the value for `AnalysisName` in each of the individual species config files.
# Note also, that the config file paths are resolved by appending '.yml' to the end of each item in the species list.
SpeciesList:
AnalysisNames:
# - Towsey.CriniaRemota
- Towsey.LimnodynastesConvex
# - Towsey.LitoriaBicolor
Expand All @@ -24,9 +24,6 @@ SpeciesList:
# - Towsey.UperoleiaInundata
# - Towsey.UperoleiaLithomoda

# Standard settings
EventThreshold: 0.2


## Specifically for AnalyzeLongRecording
# SegmentDuration: units=seconds;
Expand All @@ -35,7 +32,7 @@ SegmentDuration: 60
SegmentOverlap: 0
# Available options (case-sensitive): [False/Never | True/Always | WhenEventsDetected]
SaveIntermediateWavFiles: Never
# If `true` saves a data into a seperate file every `SegmentDuration` seconds. Accepts a boolean value: [false|true]
# If `true` saves a data into a separate file every `SegmentDuration` seconds. Accepts a boolean value: [false|true]
SaveIntermediateCsvFiles: false
# Available options (case-sensitive): [False/Never | True/Always | WhenEventsDetected]
SaveSonogramImages: True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public static void Execute(Arguments arguments)

bool filenameDate = configuration.RequireDateInFilename;

if (configuration[AnalysisKeys.AnalysisName].IsNotWhitespace())
if (configuration.AnalysisName.IsNotWhitespace())
{
Log.Warn("Your config file has `AnalysisName` set - this property is deprecated and ignored");
}
Expand Down
100 changes: 67 additions & 33 deletions src/AnalysisPrograms/Recognizers/Base/MultiRecognizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace AnalysisPrograms.Recognizers.Base
using AnalysisPrograms.AnalyseLongRecordings;
using AudioAnalysisTools;
using AudioAnalysisTools.DSP;
using AudioAnalysisTools.Events;
using AudioAnalysisTools.Indices;
using AudioAnalysisTools.StandardSpectrograms;
using AudioAnalysisTools.WavTools;
Expand All @@ -39,26 +40,22 @@ namespace AnalysisPrograms.Recognizers.Base

public class MultiRecognizer : RecognizerBase
{
public class MultiRecognizerConfig : RecognizerConfig
{
public string[] SpeciesList { get; set; }
}
private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

public override string Description => "[BETA] A method to run multiple event/species recognisers, depending on entries in config file.";

public override string Author => "Ecosounds";
public override string Author => "QUT";

public override string SpeciesName => "MultiRecognizer";

public override string CommonName => string.Empty;

public override Status Status => Status.Unmaintained;

private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

public override AnalyzerConfig ParseConfig(FileInfo file)
{
return ConfigFile.Deserialize<MultiRecognizerConfig>(file);
var config = ConfigFile.Deserialize<MultiRecognizerConfig>(file);
return config;
}

public override RecognizerResults Recognize(AudioRecording audioRecording, Config configuration, TimeSpan segmentStartOffset, Lazy<IndexCalculateResult[]> getSpectralIndexes, DirectoryInfo outputDirectory, int? imageWidth)
Expand All @@ -79,24 +76,17 @@ public override RecognizerResults Recognize(AudioRecording audioRecording, Confi
var scoreTracks = new List<Image<Rgb24>>();
var plots = new List<Plot>();
var events = new List<AcousticEvent>();
var newEvents = new List<EventCommon>();

// Get list of ID names from config file
// Loop through recognizers and accumulate the output
foreach (string name in multiRecognizerConfig.SpeciesList)
foreach (var pair in multiRecognizerConfig.Analyses)
{
// AT: Fixed this... the following should not be needed. If it happens, let me know.
// SEEM TO HAVE LOST SAMPLES
//if (audioRecording.WavReader.Samples == null)
//{
// Log.Error("audioRecording's samples are null - an inefficient disk operation is now needed");
// audioRecording = new AudioRecording(audioRecording.FilePath);
//}

var output = DoCallRecognition(name, segmentStartOffset, audioRecording, getSpectralIndexes, outputDirectory, imageWidth.Value);
var output = DoCallRecognition(pair.Recognizer, pair.Configuration, segmentStartOffset, audioRecording, getSpectralIndexes, outputDirectory, imageWidth);

if (output == null)
{
Log.Warn($"Recognizer for {name} returned a null output");
Log.Warn($"Recognizer for {pair.Recognizer.DisplayName} returned a null output");
}
else
{
Expand All @@ -111,6 +101,8 @@ public override RecognizerResults Recognize(AudioRecording audioRecording, Confi
events.AddRange(output.Events);
}

newEvents.AddRange(output.NewEvents);

// rescale scale of plots
output.Plots.ForEach(p => p.ScaleDataArray(sonogram.FrameCount));

Expand All @@ -123,6 +115,7 @@ public override RecognizerResults Recognize(AudioRecording audioRecording, Confi
return new RecognizerResults()
{
Events = events,
NewEvents = newEvents,
ScoreTrack = scoreTrackImage,
Sonogram = sonogram,
Plots = plots,
Expand All @@ -141,17 +134,8 @@ public override void SummariseResults(
// no-op
}

public static RecognizerResults DoCallRecognition(string name, TimeSpan segmentStartOffset, AudioRecording recording, Lazy<IndexCalculateResult[]> indices, DirectoryInfo outputDirectory, int imageWidth)
private static RecognizerResults DoCallRecognition(IEventRecognizer recognizer, RecognizerConfig configuration, TimeSpan segmentStartOffset, AudioRecording recording, Lazy<IndexCalculateResult[]> indices, DirectoryInfo outputDirectory, int? imageWidth)
{
Log.Debug("Looking for recognizer and config files for " + name);

// find an appropriate event recognizer
var recognizer = AnalyseLongRecording.FindAndCheckAnalyzer<IEventRecognizer>(name, name + ".yml");

// load up the standard config file for this species
var configurationFile = ConfigFile.Resolve(name + ".yml");
var configuration = recognizer.ParseConfig(configurationFile);

// TODO: adapt sample rate to required rate
int? resampleRate = configuration.ResampleRate;
if (resampleRate.HasValue && recording.WavReader.SampleRate != resampleRate.Value)
Expand All @@ -160,17 +144,17 @@ public static RecognizerResults DoCallRecognition(string name, TimeSpan segmentS
}

// execute it
Log.Info("MultiRecognizer: Executing single recognizer " + name);
Log.Info("MultiRecognizer: Executing single recognizer " + recognizer.DisplayName);
RecognizerResults result = recognizer.Recognize(
recording,
configuration,
segmentStartOffset,
indices,
outputDirectory,
imageWidth);
Log.Debug("MultiRecognizer: Completed single recognizer" + name);
Log.Debug("MultiRecognizer: Completed single recognizer" + recognizer.DisplayName);

var scoreTracks = result.Plots.Select(p => GenerateScoreTrackImage(name, p?.data, imageWidth)).ToList();
var scoreTracks = result.Plots.Select(p => GenerateScoreTrackImage(recognizer.DisplayName, p?.data, imageWidth ?? result.Sonogram.FrameCount)).ToList();
if (scoreTracks.Count != 0)
{
result.ScoreTrack = ImageTools.CombineImagesVertically(scoreTracks);
Expand All @@ -179,7 +163,7 @@ public static RecognizerResults DoCallRecognition(string name, TimeSpan segmentS
return result;
}

public static Image<Rgb24> GenerateScoreTrackImage(string name, double[] scores, int imageWidth)
private static Image<Rgb24> GenerateScoreTrackImage(string name, double[] scores, int imageWidth)
{
Log.Info("MultiRecognizer.GenerateScoreTrackImage(): " + name);
if (scores == null)
Expand Down Expand Up @@ -223,5 +207,55 @@ public static Image<Rgb24> GenerateScoreTrackImage(string name, double[] scores,
});
return trackImage;
}

public class MultiRecognizerConfig : RecognizerConfig
{
public MultiRecognizerConfig()
{
void OnLoaded(IConfig eventConfig)
{
MultiRecognizerConfig config = (MultiRecognizerConfig)eventConfig;

if (config.AnalysisNames is null or { Length: 0 })
{
throw new ConfigFileException(
$"{ nameof(this.AnalysisNames)} cannot be null or empty. It should be a list with at least one config file in it.",
config.ConfigPath);
}

// load the other config files
this.Analyses =
config
.AnalysisNames
.Select(x => x.EndsWith(".yml") ? x : x + ".yml")
.Select(lookup =>
{
Log.Debug("Looking for config files for " + lookup);
var configurationFile = ConfigFile.Resolve(lookup, config.ConfigDirectory.ToDirectoryInfo());

// find an appropriate event recognizer
var recognizer = AnalyseLongRecording.FindAndCheckAnalyzer<IEventRecognizer>(null, configurationFile.Name);

// load up the standard config file for this species
var configuration = (RecognizerConfig)recognizer.ParseConfig(configurationFile);

return (recognizer, configuration);
}).ToArray();
}

this.Loaded += OnLoaded;
}

public string[] AnalysisNames { get; set; }

/// <summary>
/// Gets or sets the threshold for which to filter events.
/// Defaults to 0.0 for the multi recogniser as we want the base recogniser's filters to be used.
/// </summary>
public override double EventThreshold { get; set; } = 0.0;

internal (IEventRecognizer Recognizer, RecognizerConfig Configuration)[] Analyses { get; set; }
}
}

}
1 change: 1 addition & 0 deletions src/AssemblyMetadata.cs.template
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ namespace AnalysisPrograms
public const bool CompiledAsSelfContained = ${self_contained};
public const string CompiledRuntimeIdentifer = "${runtime_identifier}";
public const string CompiledConfiguration = "${configuration}";
public const string CompiledTargetFramework = "${target_framework}";

public static readonly Version Version = new Version("${version}");
}
Expand Down
3 changes: 2 additions & 1 deletion src/git_version.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ param(
[string]$configuration,
[string]$self_contained,
[string]$runtime_identifier,
[string]$target_framework,
[switch]$set_ci = $false,
[switch]$json = $false,
[switch]$env_vars = $false,
Expand Down Expand Up @@ -144,7 +145,7 @@ elseif ($json) {
}
elseif ($env_vars) {
# https://github.com/PowerShell/PowerShell/issues/5543
# Due to a bug with set-content, it cna't bind to the property for a value in
# Due to a bug with set-content, it can't bind to the property for a value in
# an object, which makes the pipeline version rather intolerable.
# Hence we're adding a toString() to our pscustomobject that ensures only
# the value is represented when Set-Content calls toString
Expand Down
Loading

0 comments on commit 13a6960

Please sign in to comment.