/// <summary> /// Derives false colour varying-resolution multi-index spectrograms from provided spectral indices. /// </summary> private static void GenerateIndexSpectrogramTiles(double[] indexScales, LdSpectrogramConfig ldsConfig, Dictionary <string, IndexProperties> filteredIndexProperties, SpectrogramZoomingConfig zoomConfig, Dictionary <string, double[, ]> spectra, IndexGenerationData indexGenerationData, string fileStem, TilingProfile tilingProfile, Tiler tiler, TimeSpan alignmentPadding) { // TOP MOST ZOOMED-OUT IMAGES Log.Info("Begin drawwing zooming indec spectrograms"); foreach (double scale in indexScales) { Log.Info("Starting scale: " + scale); if (indexGenerationData.RecordingDuration.TotalSeconds < scale) { Log.Warn($"Skipping scale step {scale} because recording duration ({indexGenerationData.RecordingDuration}) is less than 1px of scale"); continue; } // TODO: eventual optimisation, remove concept of super tiles // TODO: optimisation, cache aggregation layers (currently every layer is reaggregated from base level) TimeSpan imageScale = TimeSpan.FromSeconds(scale); var superTiles = DrawSuperTilesAtScaleFromIndexSpectrograms( ldsConfig, filteredIndexProperties, zoomConfig, imageScale, spectra, indexGenerationData, fileStem, tilingProfile.ChromeOption, alignmentPadding); // tile images as we go Log.Debug("Writing index tiles for " + scale); tiler.TileMany(superTiles); Log.Debug("Completed writing index tiles for " + scale); } }
/// <summary> /// This is cut down version of the method of same name in LDSpectrogramRGB.cs. /// </summary> /// <param name="ldSpectrogramConfig">config for ldfc spectrogram.</param> /// <param name="outputDirectory">outputDirectory.</param> /// <param name="indexGenerationData">indexGenerationData.</param> /// <param name="basename">stem name of the original recording.</param> /// <param name="indexSpectrograms">Optional spectra to pass in. If specified the spectra will not be loaded from disk!.</param> private static string DrawSpectrogramsFromSpectralIndices( LdSpectrogramConfig ldSpectrogramConfig, DirectoryInfo outputDirectory, IndexGenerationData indexGenerationData, string basename, Dictionary <string, double[, ]> indexSpectrograms = null) { string colorMap1 = ldSpectrogramConfig.ColorMap1; // SpectrogramConstants.RGBMap_ACI_ENT_EVN; string colorMap2 = ldSpectrogramConfig.ColorMap2; // SpectrogramConstants.RGBMap_BGN_PMN_OSC; double blueEnhanceParameter = ldSpectrogramConfig.BlueEnhanceParameter.Value; var cs1 = new LDSpectrogramRGB(ldSpectrogramConfig, indexGenerationData, colorMap1); string fileStem = basename; cs1.FileName = fileStem; // calculate start time by combining DatetimeOffset with minute offset. cs1.StartOffset = indexGenerationData.AnalysisStartOffset; if (indexGenerationData.RecordingStartDate.HasValue) { DateTimeOffset dto = (DateTimeOffset)indexGenerationData.RecordingStartDate; cs1.RecordingStartDate = dto; if (dto != null) { cs1.StartOffset = dto.TimeOfDay + cs1.StartOffset; } } var indexProperties = IndexCalculateSixOnly.GetIndexProperties(); cs1.SetSpectralIndexProperties(indexProperties); // Load the Index Spectrograms into a Dictionary cs1.LoadSpectrogramDictionary(indexSpectrograms); if (cs1.GetCountOfSpectrogramMatrices() == 0) { Log.Error("No spectrogram matrices in the dictionary. Spectrogram files do not exist?"); throw new InvalidOperationException("Cannot find spectrogram matrix files"); } // draw all available gray scale index spectrograms. var keys = indexProperties.Keys.ToArray(); cs1.DrawGreyScaleSpectrograms(outputDirectory, fileStem, keys); // create two false-color spectrogram images var image1NoChrome = cs1.DrawFalseColorSpectrogramChromeless(cs1.ColorMode, colorMap1, blueEnhanceParameter); var image2NoChrome = cs1.DrawFalseColorSpectrogramChromeless(cs1.ColorMode, colorMap2, blueEnhanceParameter); var spacer = new Image <Rgb24>(image1NoChrome.Width, 10); var imageList = new[] { image1NoChrome, spacer, image2NoChrome, spacer }; Image image3 = ImageTools.CombineImagesVertically(imageList); var outputPath = FilenameHelpers.AnalysisResultPath(outputDirectory, fileStem, "2Maps", "png"); image3.Save(outputPath); return(outputPath); }
public static Image DrawFalseColorSpectrograms(Arguments args, string fileStem, Dictionary <string, IndexProperties> indexProperties, Dictionary <string, double[, ]> spectra = null) { // note: the spectra are oriented as per visual orientation, i.e. xAxis = time framesDictionary<string, Int16>.KeyCollection keys = AuthorList.Keys // string[] keys = spectra.Keys.ToCommaSeparatedList().Split(','); // int frameCount = spectra[keys[0]].GetLength(1); int sampleRate = 22050; int frameWidth = 512; double backgroundFilter = 0.75; // 0.75 means small values are accentuated. var minuteOffset = TimeSpan.Zero; var dataScale = args.TemporalScale; string colorMap = args.ColourMap1 ?? LDSpectrogramRGB.DefaultColorMap1; var cs1 = new LDSpectrogramRGB(minuteOffset, dataScale, sampleRate, frameWidth, colorMap) { FileName = fileStem, BackgroundFilter = backgroundFilter, IndexCalculationDuration = dataScale, }; // set the relevant dictionary of index properties cs1.SetSpectralIndexProperties(indexProperties); cs1.SpectrogramMatrices = spectra; // get parameter from the config file. var configFile = args.FalseColourSpectrogramConfig.ToFileInfo(); var config = LdSpectrogramConfig.ReadYamlToConfig(configFile); var blueEnhanceParameter = config.BlueEnhanceParameter ?? 0.0; var image1 = cs1.DrawFalseColorSpectrogramChromeless("NEGATIVE", colorMap, blueEnhanceParameter); var fullDuration = TimeSpan.FromSeconds(image1.Width * dataScale.TotalSeconds); string title = fileStem; var titleImage = LDSpectrogramRGB.DrawTitleBarOfFalseColourSpectrogram(title, image1.Width); int trackHeight = 20; var timeScale = ImageTrack.DrawTimeRelativeTrack(fullDuration, image1.Width, trackHeight); colorMap = args.ColourMap2 ?? LDSpectrogramRGB.DefaultColorMap2; var image2 = cs1.DrawFalseColorSpectrogramChromeless("NEGATIVE", colorMap, blueEnhanceParameter); var list = new List <Image> { titleImage, image1, timeScale, image2 }; var combinedImage = ImageTools.CombineImagesVertically(list.ToArray()); return(combinedImage); }
public static (Dictionary <string, double[, ]>, Dictionary <string, IndexProperties>) LoadSpectra( AnalysisIoInputDirectory io, string analysisTag, string fileStem, LdSpectrogramConfig config, Dictionary <string, IndexProperties> indexProperties) { var keys = config.GetKeys().Distinct(); // add two necessary keys for zooming keys = keys.ToList().Append("SUM"); keys = keys.ToList().Append("DIF"); //add following matrix for possible subsequent BNG combination matrix. string comboIndexID = "RHZ"; keys = keys.ToList().Append(comboIndexID); var relevantIndexProperties = keys.ToDictionary(x => x, x => indexProperties[x]); Dictionary <string, double[, ]> spectra = IndexMatrices.ReadSpectralIndices( io.InputBase, fileStem, analysisTag, keys.ToArray()); /* * // THE FOLLOWING IDEA TO MAKE A COMBINED MATRIX OF BGN and RHZ was rejected. * // Anthony was concerned that the BGN matrix alone was not conveying much information at high resolutions. * // The idea was to combine another matrix with the BGN matrix. * // I tried three combinations, BGN-RHZ, BGN-OSC and BGN-SPT. None of them appeard to provide additional useful information at high resolution. * // The problem is that at high resolution, i.e. approaching 0.1s for an analysis unit, there are not many orthogonal features in a single frequency bin. * // Make a BNG COMBINATION Spectral matrix. * //var comboMatrix = MatrixTools.MaxOfTwoMatrices(spectra["BNG"], spectra["RHZ"]); * var comboMatrix = MatrixTools.AddMatricesWeightedSum(spectra["BGN"], 1.0, spectra[comboIndexID], 10.0); * spectra["BGN"] = comboMatrix; */ return(spectra, relevantIndexProperties); }
public static Image <Rgb24> DrawFrameSpectrogramAtScale( LdSpectrogramConfig config, SpectrogramZoomingConfig zoomingConfig, TimeSpan startTimeOfData, TimeSpan frameScale, double[,] frameData, IndexGenerationData indexGeneration, ImageChrome chromeOption) { // TODO: the following normalisation bounds could be passed instead of using hard coded. double min = zoomingConfig.LowerNormalizationBoundForDecibelSpectrograms; double max = zoomingConfig.UpperNormalizationBoundForDecibelSpectrograms; //need to correctly orient the matrix for this method frameData = MatrixTools.MatrixRotate90Clockwise(frameData); // Get an unchromed image var spectrogramImage = ZoomFocusedSpectrograms.DrawStandardSpectrogramInFalseColour(frameData); if (chromeOption == ImageChrome.Without) { return(spectrogramImage); } int nyquist = indexGeneration.SampleRateResampled / 2; int herzInterval = 1000; string title = $"ZOOM SCALE={frameScale.TotalMilliseconds}ms/pixel "; var titleBar = ZoomFocusedSpectrograms.DrawTitleBarOfZoomSpectrogram(title, spectrogramImage.Width); spectrogramImage = ZoomFocusedSpectrograms.FrameZoomSpectrogram( spectrogramImage, titleBar, startTimeOfData, frameScale, config.XAxisTicInterval, nyquist, herzInterval); return(spectrogramImage); }
public void TestDeserializationOfTimespan() { // Michael had a comment somewhere that derserialization of TimeSpans // does not work properly. This test ensures it does. // Michael was correct kind of - so I changed XAxisTicInterval to an int to make serialization simpler var file = PathHelper.ResolveConfigFile("SpectrogramFalseColourConfig.yml"); var config = LdSpectrogramConfig.ReadYamlToConfig(file); Assert.AreEqual <TimeSpan>(TimeSpan.FromHours(1), config.XAxisTicInterval); Assert.AreEqual <double>(3600, config.XAxisTicIntervalSeconds); var lines = File.ReadAllLines(file.FullName); int index = lines.IndexOf(x => x.Contains("XAxisTicIntervalSeconds")); lines[index] = "XAxisTicIntervalSeconds: 127347.567"; File.WriteAllLines("SpectrogramFalseColourConfig.yml", lines); var modifiedConfig = LdSpectrogramConfig.ReadYamlToConfig("SpectrogramFalseColourConfig.yml".ToFileInfo()); Assert.AreEqual <TimeSpan>(TimeSpan.FromSeconds(127347.567), modifiedConfig.XAxisTicInterval); Assert.AreEqual <double>(127347.567, modifiedConfig.XAxisTicIntervalSeconds); }
public void SummariseResults(AnalysisSettings settings, FileSegment inputFileSegment, EventBase[] events, SummaryIndexBase[] indices, SpectralIndexBase[] spectralIndices, AnalysisResult2[] results) { var acousticIndicesConfig = (AcousticIndicesConfig)settings.AnalysisAnalyzerSpecificConfiguration; var sourceAudio = inputFileSegment.Source; var resultsDirectory = AnalysisCoordinator.GetNamedDirectory(settings.AnalysisOutputDirectory, this); bool tileOutput = acousticIndicesConfig.TileOutput; var frameWidth = acousticIndicesConfig.FrameLength; int sampleRate = AppConfigHelper.DefaultTargetSampleRate; sampleRate = acousticIndicesConfig.ResampleRate ?? sampleRate; // Gather settings for rendering false color spectrograms var ldSpectrogramConfig = LdSpectrogramConfig.GetDefaultConfig(); string basename = Path.GetFileNameWithoutExtension(sourceAudio.Name); // output to disk (so other analysers can use the data, // only data - configuration settings that generated these indices // this data can then be used by post-process analyses /* NOTE: The value for FrameStep is used only when calculating a standard spectrogram * FrameStep is NOT used when calculating Summary and Spectral indices. */ var indexConfigData = new IndexGenerationData() { RecordingType = inputFileSegment.Source.Extension, RecordingBasename = basename, RecordingStartDate = inputFileSegment.TargetFileStartDate, RecordingDuration = inputFileSegment.TargetFileDuration.Value, SampleRateOriginal = inputFileSegment.TargetFileSampleRate.Value, SampleRateResampled = sampleRate, FrameLength = frameWidth, FrameStep = settings.Configuration.GetIntOrNull(AnalysisKeys.FrameStep) ?? frameWidth, IndexCalculationDuration = acousticIndicesConfig.IndexCalculationDurationTimeSpan, BgNoiseNeighbourhood = acousticIndicesConfig.BgNoiseBuffer, AnalysisStartOffset = inputFileSegment.SegmentStartOffset ?? TimeSpan.Zero, MaximumSegmentDuration = settings.AnalysisMaxSegmentDuration, BackgroundFilterCoeff = SpectrogramConstants.BACKGROUND_FILTER_COEFF, LongDurationSpectrogramConfig = ldSpectrogramConfig, }; var icdPath = FilenameHelpers.AnalysisResultPath( resultsDirectory, basename, IndexGenerationData.FileNameFragment, "json"); Json.Serialise(icdPath.ToFileInfo(), indexConfigData); // gather spectra to form spectrograms. Assume same spectra in all analyzer results // this is the most efficient way to do this // gather up numbers and strings store in memory, write to disk one time // this method also AUTOMATICALLY SORTS because it uses array indexing var dictionaryOfSpectra = spectralIndices.ToTwoDimensionalArray(SpectralIndexValues.CachedSelectors, TwoDimensionalArray.Rotate90ClockWise); // Calculate the index distribution statistics and write to a json file. Also save as png image var indexDistributions = IndexDistributions.WriteSpectralIndexDistributionStatistics(dictionaryOfSpectra, resultsDirectory, basename); // HACK: do not render false color spectrograms unless IndexCalculationDuration = 60.0 (the normal resolution) if (acousticIndicesConfig.IndexCalculationDurationTimeSpan != 60.0.Seconds()) { Log.Warn("False color spectrograms were not rendered"); } else { FileInfo indicesPropertiesConfig = acousticIndicesConfig.IndexPropertiesConfig.ToFileInfo(); // Actually draw false color / long duration spectrograms Tuple <Image, string>[] images = LDSpectrogramRGB.DrawSpectrogramsFromSpectralIndices( inputDirectory: resultsDirectory, outputDirectory: resultsDirectory, ldSpectrogramConfig: ldSpectrogramConfig, indexPropertiesConfigPath: indicesPropertiesConfig, indexGenerationData: indexConfigData, basename: basename, analysisType: this.Identifier, indexSpectrograms: dictionaryOfSpectra, indexStatistics: indexDistributions, imageChrome: (!tileOutput).ToImageChrome()); if (tileOutput) { Debug.Assert(images.Length == 2); Log.Info("Tiling output at scale: " + acousticIndicesConfig.IndexCalculationDuration); foreach (var image in images) { TileOutput(resultsDirectory, Path.GetFileNameWithoutExtension(sourceAudio.Name), image.Item2 + ".Tile", inputFileSegment, image.Item1); } } } }
[DataRow(0.1, 35990)] // at this scale 10 pixels are missing... image is padded by 0 px, and misisng 10px public void TestImagesHaveCorrectLength(double renderScale, int expectedWidth) { // this value just represents a 'render as much as you can until this limit' threshold const int ImageWidth = 1024; var dataScale = 0.1.Seconds(); var imageScale = renderScale.Seconds(); var recordingDuration = 3599.Seconds(); // we simulate rendering the last tile at each resolution var tileDuration = imageScale.Multiply(ImageWidth); var numberOfTiles = (int)Math.Floor(recordingDuration.TotalSeconds / tileDuration.TotalSeconds); var startTime = tileDuration.Multiply(numberOfTiles); // 1 second short of an hour - this is the test case var endTime = recordingDuration; var config = new LdSpectrogramConfig() { // same color map to reduce memory stress for test ColorMap1 = SpectrogramConstants.RGBMap_BGN_PMN_EVN, ColorMap2 = SpectrogramConstants.RGBMap_BGN_PMN_EVN, }; var generationData = new IndexGenerationData() { IndexCalculationDuration = dataScale, FrameLength = 512, FrameStep = 0, RecordingDuration = recordingDuration, }; var indexProperties = IndexProperties.GetIndexProperties(PathHelper.ResolveConfigFile("IndexPropertiesConfig.yml")); // create some fake spectra int duration = (int)(recordingDuration.TotalSeconds / dataScale.TotalSeconds); var spectra = new Dictionary <string, double[, ]>(); foreach (var key in config.GetKeys()) { var values = new double[256, duration].Fill(indexProperties[key].DefaultValue); spectra.Add(key, values); } string basename = "abc"; var image = ZoomCommon.DrawIndexSpectrogramCommon( config, generationData, indexProperties, startTime, endTime, dataScale, imageScale, ImageWidth, spectra, basename); // since we just asked for a fragment of the image at the end, // the expected width is just for that last fragment var lastWidth = expectedWidth - (numberOfTiles * ImageWidth); Assert.That.ImageIsSize(lastWidth, 256, image); Image <Rgb24> bitmap = (Image <Rgb24>)image; // test output for debugging //image.Save("./" + renderScale + ".png"); Assert.That.ImageRegionIsColor(new Rectangle(0, 0, lastWidth, 256), Color.Black, bitmap, 0); }
/// <summary> /// Assume that we are processing data for one minute only. /// From this one minute of data, we produce images at three scales. /// A one minute recording framed at 20ms should yield 3000 frames. /// But to achieve this where sr= 22050 and frameSize=512, we need an overlap of 71 samples. /// Consequently only 2999 frames returned per minute. /// Therefore have to pad end to get 3000 frames. /// </summary> public static TimeOffsetSingleLayerSuperTile[] DrawSuperTilesFromSingleFrameSpectrogram(DirectoryInfo dataDir, LdSpectrogramConfig analysisConfig, Dictionary <string, IndexProperties> indexProperties, SpectrogramZoomingConfig zoomingConfig, int minute, double[] imageScales, string basename, IndexGenerationData indexGeneration, ImageChrome chromeOption, TimeSpan alignmentPadding) { string fileStem = basename; // string analysisType = analysisConfig.AnalysisType; TimeSpan indexScale = indexGeneration.IndexCalculationDuration; TimeSpan frameScale = TimeSpan.FromSeconds(zoomingConfig.SpectralFrameDuration); var expectedDataDurationInSeconds = (int)indexGeneration.MaximumSegmentDuration.Value.TotalSeconds; var expectedFrameCount = (int)Math.Round(expectedDataDurationInSeconds / zoomingConfig.SpectralFrameDuration); string fileName = fileStem + "_" + minute + "min.csv"; string csvPath = Path.Combine(dataDir.FullName, fileName); bool skipHeader = true; bool skipFirstColumn = true; // read spectrogram into a list of frames List <double[]> frameList = CsvTools.ReadCSVFileOfDoubles(csvPath, skipHeader, skipFirstColumn); if (frameList == null) { LoggedConsole.WriteErrorLine( "WARNING: METHOD DrawSuperTilesFromSingleFrameSpectrogram(): NO SPECTRAL DATA SUPPLIED"); return(null); } PadEndOfListOfFrames(frameList, expectedFrameCount); TrimEndOfListOfFrames(frameList, expectedFrameCount); //// frame count will be one less than expected for the recording segment because of frame overlap //// Therefore pad the end of the list of frames with the last frame. // int frameDiscrepancy = expectedFrameCount - frameList.Count; // if (frameDiscrepancy > 0) // { // double[] frame = frameList[frameList.Count - 1]; // for (int d = 0; d < frameDiscrepancy; d++) // { // frameList.Add(frame); // } // } var frameData = new TemporalMatrix("rows", MatrixTools.ConvertList2Matrix(frameList), frameScale); frameData.SwapTemporalDimension(); // so the two data matrices have the same temporal dimension TimeSpan startTime = indexGeneration.AnalysisStartOffset; // default = zero minute of day i.e. midnight TimeSpan startTimeOfData = startTime + TimeSpan.FromMinutes(minute); var str = new TimeOffsetSingleLayerSuperTile[imageScales.Length]; // make the images for (int scale = 0; scale < imageScales.Length; scale++) { TimeSpan imageScale = TimeSpan.FromSeconds(imageScales[scale]); var compressionFactor = (int)Math.Round(imageScale.TotalMilliseconds / frameData.DataScale.TotalMilliseconds); double columnDuration = imageScale.TotalSeconds; // int expectedFrameCount = (int)Math.Round(expectedDataDurationInSeconds / columnDuration); // ############## RESEARCH CHOICE HERE >>>> compress spectrograms to correct scale using either max or average // Average appears to offer better contrast. // double[,] data = frameData.CompressMatrixInTemporalDirectionByTakingMax(imageScale); double[,] data = frameData.CompressMatrixInTemporalDirectionByTakingAverage(imageScale); var spectrogramImage = DrawFrameSpectrogramAtScale( analysisConfig, zoomingConfig, startTimeOfData, imageScale, data, indexGeneration, chromeOption); str[scale] = new TimeOffsetSingleLayerSuperTile( alignmentPadding, SpectrogramType.Frame, imageScale, spectrogramImage.CloneAs <Rgba32>(), startTimeOfData); } return(str); }
public void TestAnalyzeSr64000Recording() { int sampleRate = 64000; double duration = 420; // signal duration in seconds = 7 minutes int[] harmonics = { 500, 1000, 2000, 4000, 8000 }; var recording = DspFilters.GenerateTestRecording(sampleRate, duration, harmonics, WaveType.Cosine); string recordingName = "TemporaryRecording2"; var recordingPath = this.outputDirectory.CombineFile(recordingName + ".wav"); WavWriter.WriteWavFileViaFfmpeg(recordingPath, recording.WavReader); var fst = FreqScaleType.Linear125Octaves7Tones28Nyquist32000; var freqScale = new FrequencyScale(fst); /* * // draw the signal as spectrogram just for debugging purposes * // but can only draw a two minute spectrogram when sr=64000 - change duration above. * duration = 120; // if drawing sonogram, then set signal duration = 2 minutes * var sonogram = OctaveFreqScale.ConvertRecordingToOctaveScaleSonogram(recording, fst); * var sonogramImage = sonogram.GetImageFullyAnnotated(sonogram.GetImage(), "SPECTROGRAM", freqScale.GridLineLocations); * var outputImagePath = this.outputDirectory.CombineFile("SignalSpectrogram_OctaveFreqScale.png"); * sonogramImage.Save(outputImagePath.FullName); */ // Now need to rewrite the config file with new parameter settings var configPath = PathHelper.ResolveConfigFile("Towsey.Acoustic.yml"); // Convert the Config config to IndexCalculateConfig class and merge in the unnecesary parameters. //Config configuration = Yaml.Deserialise(configPath); //IndexCalculateConfig config = IndexCalculateConfig.GetConfig(configuration, false); // because of difficulties in dealing with Config config files, just edit the text file!!!!! var configLines = File.ReadAllLines(configPath.FullName); configLines[configLines.IndexOf(x => x.StartsWith("IndexCalculationDuration: "))] = "IndexCalculationDuration: 15.0"; //configLines[configLines.IndexOf(x => x.StartsWith("BgNoiseBuffer: "))] = "BgNoiseBuffer: 5.0"; configLines[configLines.IndexOf(x => x.StartsWith("FrequencyScale: Linear"))] = "FrequencyScale: " + fst; // the is the only octave scale currently functioning for IndexCalculate class configLines[configLines.IndexOf(x => x.StartsWith("FrameLength"))] = $"FrameLength: {freqScale.WindowSize}"; configLines[configLines.IndexOf(x => x.StartsWith("ResampleRate: "))] = "ResampleRate: 64000"; // write the edited Config file to temporary output directory var newConfigPath = this.outputDirectory.CombineFile("Towsey.Acoustic.yml"); File.WriteAllLines(newConfigPath.FullName, configLines); PathHelper.ResolveConfigFile("IndexPropertiesConfig.yml").CopyTo(this.outputDirectory.CombineFile("IndexPropertiesConfig.yml").FullName); var arguments = new AnalyseLongRecording.Arguments { Source = recordingPath, Config = newConfigPath.FullName, Output = this.outputDirectory, MixDownToMono = true, Parallel = !Debugger.IsAttached, }; AnalyseLongRecording.Execute(arguments); var resultsDirectory = this.outputDirectory.Combine("Towsey.Acoustic"); var listOfFiles = resultsDirectory.EnumerateFiles().ToArray(); Assert.AreEqual(19, listOfFiles.Length); var csvCount = listOfFiles.Count(f => f.Name.EndsWith(".csv")); Assert.AreEqual(15, csvCount); var jsonCount = listOfFiles.Count(f => f.Name.EndsWith(".json")); Assert.AreEqual(2, jsonCount); var pngCount = listOfFiles.Count(f => f.Name.EndsWith(".png")); Assert.AreEqual(2, pngCount); var bgnFile = resultsDirectory.CombineFile(recordingName + "__Towsey.Acoustic.BGN.csv"); double[,] actualBgn = Csv.ReadMatrixFromCsv <double>(bgnFile, TwoDimensionalArray.None); var expectedSpectrumFile = PathHelper.ResolveAsset("LongDuration", "BgnMatrix.OctaveScale.csv"); // uncomment the following line when first produce the array // bgnFile.CopyTo(expectedSpectrumFile.FullName); // compare actual BGN file with expected file. var expectedBgn = Csv.ReadMatrixFromCsv <double>(expectedSpectrumFile, TwoDimensionalArray.None); CollectionAssert.That.AreEqual(expectedBgn, actualBgn, 0.000_000_001); var array = MatrixTools.GetRow(actualBgn, 0); Assert.AreEqual(28, actualBgn.RowLength()); Assert.AreEqual(256, array.Length); // draw array just to check peaks are in correct places - just for debugging purposes var ldsBgnSpectrumFile = this.outputDirectory.CombineFile("Spectrum2.png"); GraphsAndCharts.DrawGraph(array, "LD BGN SPECTRUM Octave", ldsBgnSpectrumFile); // ########################################## // SECOND part of test is to create the LD spectrograms because they are not created when IndexCalcDuration < 60 seconds // first read in the index generation data var icdPath = resultsDirectory.CombineFile(recordingName + "__IndexGenerationData.json"); var indexConfigData = Json.Deserialize <IndexGenerationData>(icdPath); var indexPropertiesConfig = PathHelper.ResolveConfigFile("IndexPropertiesConfig.yml"); var ldSpectrogramConfigFile = PathHelper.ResolveConfigFile("SpectrogramFalseColourConfig.yml"); var ldSpectrogramConfig = LdSpectrogramConfig.ReadYamlToConfig(ldSpectrogramConfigFile); ldSpectrogramConfig.FreqScale = fst.ToString(); // finally read in the dictionary of spectra string analysisType = "Towsey.Acoustic"; var keys = LDSpectrogramRGB.GetArrayOfAvailableKeys(); var dictionaryOfSpectra = IndexMatrices.ReadSpectralIndices(resultsDirectory, recordingName, analysisType, keys); LDSpectrogramRGB.DrawSpectrogramsFromSpectralIndices( inputDirectory: resultsDirectory, outputDirectory: resultsDirectory, ldSpectrogramConfig: ldSpectrogramConfig, indexPropertiesConfigPath: indexPropertiesConfig, indexGenerationData: indexConfigData, basename: recordingName, analysisType: analysisType, indexSpectrograms: dictionaryOfSpectra); // test number of images - should now be 23 listOfFiles = resultsDirectory.EnumerateFiles().ToArray(); pngCount = listOfFiles.Count(f => f.Name.EndsWith(".png")); Assert.AreEqual(22, pngCount); var twoMapsImagePath = resultsDirectory.CombineFile(recordingName + "__2Maps.png"); var twoMapsImage = Image.Load <Rgb24>(twoMapsImagePath.FullName); // image is (7*4) * 652 Assert.AreEqual(28, twoMapsImage.Width); Assert.AreEqual(652, twoMapsImage.Height); }
/// <summary> /// Draws FC index spectrograms for an entire row. /// </summary> public static TimeOffsetSingleLayerSuperTile[] DrawSuperTilesAtScaleFromIndexSpectrograms( LdSpectrogramConfig analysisConfig, Dictionary <string, IndexProperties> indexProperties, SpectrogramZoomingConfig zoomingConfig, TimeSpan imageScale, Dictionary <string, double[, ]> spectra, IndexGenerationData indexGeneration, string basename, ImageChrome chromeOption, TimeSpan alignmentPadding) { Contract.Requires(!spectra.IsNullOrEmpty(), "ERROR: NO SPECTRAL DATA SUPPLIED"); // calculate source data duration from column count of arbitrary matrix TimeSpan dataScale = indexGeneration.IndexCalculationDuration; double[,] matrix = spectra.First().Value; TimeSpan sourceDataDuration = TimeSpan.FromSeconds(matrix.GetLength(1) * dataScale.TotalSeconds); int tileWidth = zoomingConfig.TileWidth; int superTileWidth = zoomingConfig.SuperTileWidthDefault(); var superTileCount = (int)Math.Ceiling(zoomingConfig.SuperTileCount(sourceDataDuration, imageScale.TotalSeconds)); TimeSpan superTileDuration = TimeSpan.FromTicks(superTileWidth * imageScale.Ticks); // initialize the image array to return var superTiles = new TimeOffsetSingleLayerSuperTile[superTileCount]; // sometimes the whole recording is not analyzed. In this case, jump the time index forward. TimeSpan startTime = indexGeneration.AnalysisStartOffset; // start the loop for (int t = 0; t < superTileCount; t++) { var image = DrawOneScaledIndexSpectrogramTile( analysisConfig, indexGeneration, indexProperties, startTime, dataScale, imageScale, superTileWidth, spectra, basename, chromeOption); superTiles[t] = new TimeOffsetSingleLayerSuperTile( durationToPreviousTileBoundaryAtUnitScale: alignmentPadding, spectrogramType: SpectrogramType.Index, scale: imageScale, image: image.CloneAs <Rgba32>(), timeOffset: startTime); startTime += superTileDuration; if (startTime > sourceDataDuration) { break; } } return(superTiles); }
/// <summary> /// THIS IS ENTRY METHOD FOR TILING SPECTROGRAMS. /// </summary> public static void DrawTiles( AnalysisIoInputDirectory io, ZoomParameters common, string analysisTag) { Log.Info("Begin Draw Super Tiles"); Contract.Requires(common != null, "common can not be null"); Contract.Requires(common.SpectrogramZoomingConfig != null, "SpectrogramZoomingConfig can not be null"); var zoomConfig = common.SpectrogramZoomingConfig; LdSpectrogramConfig ldsConfig = common.SpectrogramZoomingConfig.LdSpectrogramConfig; var distributions = common.IndexDistributions; var indexGenerationData = common.IndexGenerationData; var indexProperties = zoomConfig.IndexProperties; string fileStem = common.OriginalBasename; // scales for false color index spectrograms images in seconds per pixel. double[] indexScales = zoomConfig.SpectralIndexScale; // default scales for standard (FFT) spectrograms in seconds per pixel. double[] standardScales = zoomConfig.SpectralFrameScale; ValidateScales(indexScales, indexGenerationData.IndexCalculationDuration.TotalSeconds); var shouldRenderStandardScale = !standardScales.IsNullOrEmpty(); if (!shouldRenderStandardScale) { Log.Warn("Standard spectrograms will not be rendered"); } var allImageScales = indexScales.Concat(standardScales).ToArray(); Log.Info("Tiling at scales: " + allImageScales.ToCommaSeparatedList()); // determine what naming format to use for tiles var(namingPattern, alignmentPadding) = GetTilingProfile( common, zoomConfig, indexGenerationData, indexScales.Max()); // pad out image so it produces a whole number of tiles // this solves the asymmetric right padding of short audio files // var paddedWidth = (int)(Math.Ceiling(zoomConfig.TileWidth / xNominalUnitScale) * xNominalUnitScale); // create a new tiler // pass it scales for x and y-axis // also pass it unit scale relations (between unit scale and unit height/width) to use as a reference point var tiler = new Tiler( new DirectoryInfo(io.OutputBase.FullName), namingPattern, xScales: new SortedSet <double>(allImageScales), xUnitScale: XNominalUnitScale, unitWidth: 1440, yScales: new SortedSet <double>(allImageScales.Select(x => 1.0)), yUnitScale: 1.0, unitHeight: namingPattern.TileHeight); var(spectra, filteredIndexProperties) = ZoomCommon.LoadSpectra(io, analysisTag, fileStem, zoomConfig.LdSpectrogramConfig, indexProperties); // false color index tiles GenerateIndexSpectrogramTiles( indexScales, ldsConfig, filteredIndexProperties, zoomConfig, spectra, indexGenerationData, fileStem, namingPattern, tiler, alignmentPadding); // standard fft frame spectrograms if (shouldRenderStandardScale) { GenerateStandardSpectrogramTiles( spectra, indexGenerationData, ldsConfig, filteredIndexProperties, zoomConfig, standardScales, fileStem, namingPattern, tiler, alignmentPadding); } Log.Success("Tiling complete"); }
private static void GenerateStandardSpectrogramTiles( Dictionary <string, double[, ]> spectra, IndexGenerationData indexGeneration, LdSpectrogramConfig ldsConfig, Dictionary <string, IndexProperties> filteredIndexProperties, SpectrogramZoomingConfig zoomConfig, double[] standardScales, string fileStem, TilingProfile namingPattern, Tiler tiler, TimeSpan alignmentPadding) { Log.Info("START DRAWING ZOOMED-IN FRAME SPECTROGRAMS"); TimeSpan dataDuration = TimeSpan.FromTicks(spectra["PMN"].GetLength(1) * indexGeneration.IndexCalculationDuration.Ticks); TimeSpan duration = indexGeneration.RecordingDuration; Contract.Requires( (dataDuration - duration).Absolute() < indexGeneration.IndexCalculationDuration, "The expected amount of data was not supplied"); var minuteCount = (int)Math.Ceiling(dataDuration.TotalMinutes); // window the standard spectrogram generation so that we can provide adjacent supertiles to the // tiler, so that bordering / overlapping tiles (for cases where tile size != multiple of supertile size) // don't render partial tiles (i.e. bad/partial rendering of image) // this is the function generator // use of Lazy means results will only be evaluated once // and only when needed. This is useful for sliding window. Lazy <TimeOffsetSingleLayerSuperTile[]> GenerateStandardSpectrogramGenerator(int minuteToLoad) { return(new Lazy <TimeOffsetSingleLayerSuperTile[]>( () => { Log.Info("Starting generation for minute: " + minuteToLoad); var superTilingResults = DrawSuperTilesFromSingleFrameSpectrogram( null /*inputDirectory*/, //TODO:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ldsConfig, filteredIndexProperties, zoomConfig, minuteToLoad, standardScales, fileStem, indexGeneration, namingPattern.ChromeOption, alignmentPadding); return superTilingResults; })); } Lazy <TimeOffsetSingleLayerSuperTile[]> previous = null; Lazy <TimeOffsetSingleLayerSuperTile[]> current = null; Lazy <TimeOffsetSingleLayerSuperTile[]> next = null; for (int minute = 0; minute < minuteCount; minute++) { Log.Trace("Starting loop for minute" + minute); // shift each value back previous = current; current = next ?? GenerateStandardSpectrogramGenerator(minute); next = minute + 1 < minuteCount?GenerateStandardSpectrogramGenerator(minute + 1) : null; // for each scale level of the results for (int i = 0; i < current.Value.Length; i++) { // finally tile the output Log.Debug("Begin tile production for minute: " + minute); tiler.Tile(previous?.Value[i], current.Value[i], next?.Value[i]); Log.Debug("Begin tile production for minute: " + minute); } } }
[DataRow(0.1, 35990)] // at this scale 10 pixels are missing... image is padded by 0 px, and misisng 10px public void TestImagesHaveCorrectLength(double renderScale, int expectedWidth) { // this value just represents a 'render as much as you can until this limit' threshold const int ImageWidth = 1024; var dataScale = 0.1.Seconds(); var imageScale = renderScale.Seconds(); var recordingDuration = 3599.Seconds(); // we simulate rendering the last tile at each resolution var tileDuration = imageScale.Multiply(ImageWidth); var numberOfTiles = (int)Math.Floor(recordingDuration.TotalSeconds / tileDuration.TotalSeconds); var startTime = tileDuration.Multiply(numberOfTiles); // 1 second short of an hour - this is the test case var endTime = recordingDuration; var config = new LdSpectrogramConfig(); var generationData = new IndexGenerationData() { IndexCalculationDuration = dataScale, FrameLength = 512, FrameStep = 0, RecordingDuration = recordingDuration, }; var indexProperties = IndexProperties.GetIndexProperties(PathHelper.ResolveConfigFile("IndexPropertiesConfig.yml")); // create some fake spectra int duration = (int)(recordingDuration.TotalSeconds / dataScale.TotalSeconds); var spectra = new Dictionary <string, double[, ]>(); foreach (var key in SpectralIndexValues.GetKeys()) { spectra.Add(key, new double[256, duration]); var bgnKey = nameof(SpectralIndexValues.BGN); if (key == bgnKey) { spectra[bgnKey].Fill(indexProperties[bgnKey].DefaultValue); } // due to a bug in DrawRgbColourMatrix the ACI calculation ends up being NaN. // This we fill one of our other spectra to a non-zero value to get the black colour we desire // Bug reference: https://github.com/QutEcoacoustics/audio-analysis/issues/154 var sumKey = nameof(SpectralIndexValues.SUM); if (key == sumKey) { // this forces ACI calculation to 0 / 1 instead of 0 / 0 spectra[sumKey].Fill(1.0); } } string basename = "abc"; var image = ZoomCommon.DrawIndexSpectrogramCommon( config, generationData, indexProperties, startTime, endTime, dataScale, imageScale, ImageWidth, spectra, basename); // since we just asked for a fragment of the image at the end, // the expected width is just for that last fragment var lastWidth = expectedWidth - (numberOfTiles * ImageWidth); Assert.That.ImageIsSize(lastWidth, 256, image); Bitmap bitmap = (Bitmap)image; // test output for debugging //image.Save("./" + renderScale + ".png"); Assert.That.ImageRegionIsColor(new Rectangle(0, 0, lastWidth, 256), Color.Black, bitmap, 0); }
public static Image DrawIndexSpectrogramCommon( LdSpectrogramConfig config, IndexGenerationData indexGenerationData, Dictionary <string, IndexProperties> indexProperties, TimeSpan startTime, TimeSpan endTime, TimeSpan dataScale, TimeSpan imageScale, int imageWidth, Dictionary <string, double[, ]> spectra, string basename) { double scalingFactor = Math.Round(imageScale.TotalMilliseconds / dataScale.TotalMilliseconds); Contract.Requires(scalingFactor >= 1.0, $"Compression scale `{scalingFactor}`is invalid"); // calculate data duration from column count of abitrary matrix //TimeSpan dataDuration = TimeSpan.FromSeconds(matrix.GetLength(1) * dataScale.TotalSeconds); int columnCount = spectra.FirstValue().GetLength(1); var startIndex = (int)(startTime.Ticks / dataScale.Ticks); var endIndex = (int)(endTime.Ticks / dataScale.Ticks); Contract.Ensures(endIndex <= columnCount); // extract subset of target data var spectralSelection = new Dictionary <string, double[, ]>(); foreach (string key in spectra.Keys) { var matrix = spectra[key]; int rowCount = matrix.GetLength(0); spectralSelection[key] = MatrixTools.Submatrix(matrix, 0, startIndex, rowCount - 1, endIndex - 1); Contract.Ensures( spectralSelection[key].GetLength(1) == (endTime - startTime).Ticks / dataScale.Ticks, "The expected number of frames should be extracted."); } // compress spectrograms to correct scale if (scalingFactor > 1) { // we add rounding to the compression so that fractional pixels get rendered spectralSelection = IndexMatrices.CompressIndexSpectrograms( spectralSelection, imageScale, dataScale, d => Math.Round(d, MidpointRounding.AwayFromZero)); } else { // this else is unnecessary - completely defensive code Contract.Ensures(scalingFactor == 1); } // check that have not compressed matrices to zero length if (spectralSelection.FirstValue().GetLength(0) == 0 || spectralSelection.FirstValue().GetLength(1) == 0) { throw new InvalidOperationException("Spectral matrices compressed to zero size"); } // DEFINE the DEFAULT colour maps for the false-colour spectrograms // Then obtain values from spectrogramDrawingConfig. NOTE: WE REQUIRE LENGTH = 11 chars. string colorMap1 = "ACI-ENT-EVN"; if (config.ColorMap1 != null && config.ColorMap1.Length == 11) { colorMap1 = config.ColorMap1; } string colorMap2 = "BGN-PMN-EVN"; if (config.ColorMap2 != null && config.ColorMap2.Length == 11) { colorMap2 = config.ColorMap2; } double backgroundFilterCoeff = indexGenerationData.BackgroundFilterCoeff; // double colourGain = (double?)configuration.ColourGain ?? SpectrogramConstants.COLOUR_GAIN; // determines colour saturation var cs1 = new LDSpectrogramRGB(config, indexGenerationData, colorMap1) { FileName = basename, BackgroundFilter = backgroundFilterCoeff, }; cs1.SetSpectralIndexProperties(indexProperties); // set the relevant dictionary of index properties cs1.LoadSpectrogramDictionary(spectralSelection); // set up piecewise linear function to determine colour weights var logResolution = Math.Log(imageScale.TotalMilliseconds, 2); double upperResolution = Math.Log(32768, 2); double lowerResolution = Math.Log(256, 2); double range = upperResolution - lowerResolution; double blendWeight1; if (logResolution >= upperResolution) { blendWeight1 = 1.0; } else if (logResolution <= lowerResolution) { blendWeight1 = 0.0; } else { blendWeight1 = (logResolution - lowerResolution) / range; } double blendWeight2 = 1 - blendWeight1; //else if (imageScaleInMsPerPixel > 2000) //{ // blendWeight1 = 0.7; // blendWeight2 = 0.3; //} //else if (imageScaleInMsPerPixel > 1000) //{ // blendWeight1 = 0.3; // blendWeight2 = 0.7; //} //else if (imageScaleInMsPerPixel > 500) //{ // // > 0.5 seconds // blendWeight1 = 0.2; // blendWeight2 = 0.8; //} //else if (imageScaleInMsPerPixel > 300) //{ // // > 0.5 seconds // blendWeight1 = 0.1; // blendWeight2 = 0.9; //} var ldfcSpectrogram = cs1.DrawBlendedFalseColourSpectrogram(colorMap1, colorMap2, blendWeight1, blendWeight2); if (ldfcSpectrogram == null) { throw new InvalidOperationException("Null Image returned from DrawBlendedFalseColourSpectrogram"); } return(ldfcSpectrogram); }
public static void Execute(Arguments arguments) { if (arguments == null) { throw new NoDeveloperMethodException(); } string date = "# DATE AND TIME: " + DateTime.Now; LoggedConsole.WriteLine("# DRAW LONG DURATION SPECTROGRAMS DERIVED FROM CSV FILES OF SPECTRAL INDICES OBTAINED FROM AN AUDIO RECORDING"); LoggedConsole.WriteLine(date); LoggedConsole.WriteLine("# Spectrogram Config file: " + arguments.FalseColourSpectrogramConfig); LoggedConsole.WriteLine("# Index Properties Config file: " + arguments.IndexPropertiesConfig); LoggedConsole.WriteLine(); (FileInfo indexGenerationDataFile, FileInfo indexDistributionsFile) = ZoomParameters.CheckNeededFilesExist(arguments.InputDataDirectory.ToDirectoryInfo()); var indexGenerationData = Json.Deserialize <IndexGenerationData>(indexGenerationDataFile); // spectral distribution statistics is required only when calcualting difference spectrograms. Dictionary <string, IndexDistributions.SpectralStats> indexDistributionsData = null; if (indexDistributionsFile != null && indexDistributionsFile.Exists) { indexDistributionsData = IndexDistributions.Deserialize(indexDistributionsFile); } // this config can be found in IndexGenerationData. If config argument not specified, simply take it from icd file LdSpectrogramConfig config; if (arguments.FalseColourSpectrogramConfig == null) { config = indexGenerationData.LongDurationSpectrogramConfig; } else { config = LdSpectrogramConfig.ReadYamlToConfig(arguments.FalseColourSpectrogramConfig.ToFileInfo()); } FilenameHelpers.ParseAnalysisFileName(indexGenerationDataFile, out var originalBaseName, out var _, out var _); // CHECK FOR ERROR SEGMENTS - get zero signal array var input = arguments.InputDataDirectory.ToDirectoryInfo(); var csvFile = new FileInfo(Path.Combine(input.FullName, originalBaseName + "__Towsey.Acoustic.Indices.csv")); //Dictionary<string, double[]> summaryIndices = CsvTools.ReadCSVFile2Dictionary(csvFile.FullName); //var summaryIndices = Csv.ReadFromCsv<Dictionary<string, double[]>>(csvFile); var summaryIndices = Csv.ReadFromCsv <SummaryIndexValues>(csvFile); var indexErrors = GapsAndJoins.DataIntegrityCheckForZeroSignal(summaryIndices); //config.IndexCalculationDuration = TimeSpan.FromSeconds(1.0); //config.XAxisTicInterval = TimeSpan.FromSeconds(60.0); //config.IndexCalculationDuration = TimeSpan.FromSeconds(60.0); //config.XAxisTicInterval = TimeSpan.FromSeconds(3600.0); LDSpectrogramRGB.DrawSpectrogramsFromSpectralIndices( inputDirectory: input, outputDirectory: arguments.OutputDirectory.ToDirectoryInfo(), ldSpectrogramConfig: config, indexPropertiesConfigPath: arguments.IndexPropertiesConfig.ToFileInfo(), indexGenerationData: indexGenerationData, basename: originalBaseName, analysisType: AcousticIndices.TowseyAcoustic, indexSpectrograms: null, indexStatistics: indexDistributionsData, segmentErrors: indexErrors, imageChrome: false.ToImageChrome()); Log.Success("Draw Long Duration Spectrograms complete!"); }
public static Image <Rgb24> DrawOneScaledIndexSpectrogramTile( LdSpectrogramConfig config, IndexGenerationData indexGenerationData, Dictionary <string, IndexProperties> indexProperties, TimeSpan startTime, TimeSpan dataScale, TimeSpan imageScale, int superTileImageWidth, Dictionary <string, double[, ]> spectra, string basename, ImageChrome chromeOption) { Contract.Requires(!spectra.IsNullOrEmpty()); // calculate data duration from column count of abitrary matrix var matrix = spectra.First().Value; int columnCount = matrix.GetLength(1); TimeSpan dataDuration = TimeSpan.FromSeconds(columnCount * dataScale.TotalSeconds); var analysisStartTime = indexGenerationData.RecordingStartDate.Value.TimeOfDay.Add(indexGenerationData.AnalysisStartOffset); TimeSpan offsetTime = TimeSpan.Zero; TimeSpan imageDuration = TimeSpan.FromTicks(superTileImageWidth * imageScale.Ticks); TimeSpan halfImageDuration = TimeSpan.FromTicks(superTileImageWidth * imageScale.Ticks / 2); if (startTime < TimeSpan.Zero) { offsetTime = TimeSpan.Zero - startTime; startTime = TimeSpan.Zero; } TimeSpan endTime = startTime + imageDuration; if (endTime > dataDuration) { endTime = dataDuration; } // get the plain unchromed spectrogram var ldSpectrogram = ZoomCommon.DrawIndexSpectrogramCommon( config, indexGenerationData, indexProperties, startTime, endTime, dataScale, imageScale, superTileImageWidth, spectra, basename); if (chromeOption == ImageChrome.Without) { return(ldSpectrogram); } int nyquist = 22050 / 2; if (indexGenerationData.SampleRateResampled > 0) { nyquist = indexGenerationData.SampleRateResampled / 2; } int hertzInterval = 1000; if (config != null) { hertzInterval = config.YAxisTicInterval; } string title = $"ZOOM SCALE={imageScale.TotalSeconds}s/pixel"; var titleBar = ZoomFocusedSpectrograms.DrawTitleBarOfZoomSpectrogram(title, ldSpectrogram.Width); startTime += analysisStartTime; ldSpectrogram = ZoomFocusedSpectrograms.FrameZoomSpectrogram( ldSpectrogram, titleBar, startTime, imageScale, config.XAxisTicInterval, nyquist, hertzInterval); // create the base image var image = Drawing.NewImage(ldSpectrogram.Width, ldSpectrogram.Height, Color.DarkGray); var xOffset = (int)(offsetTime.Ticks / imageScale.Ticks); image.Mutate(g1 => { g1.DrawImage(ldSpectrogram, new Point(xOffset, 0), 1); }); return(image); }