Exemple #1
0
        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 void DrawStackOfZoomedSpectrograms(
            DirectoryInfo inputDirectory,
            DirectoryInfo outputDirectory,
            AnalysisIoInputDirectory io,
            ZoomParameters common,
            string analysisTag,
            TimeSpan focalTime,
            int imageWidth)
        {
            var zoomConfig = common.SpectrogramZoomingConfig;
            LdSpectrogramConfig ldsConfig = common.SpectrogramZoomingConfig.LdSpectrogramConfig;

            //var distributions = common.IndexDistributions;
            string   fileStem        = common.OriginalBasename;
            var      indexGeneration = common.IndexGenerationData;
            TimeSpan dataScale       = indexGeneration.IndexCalculationDuration;

            // ####################### DERIVE ZOOMED OUT SPECTROGRAMS FROM SPECTRAL INDICES
            //var indexGenerationData = common.IndexGenerationData;
            var indexProperties = zoomConfig.IndexProperties;

            var(spectra, filteredIndexProperties) = ZoomCommon.LoadSpectra(io, analysisTag, fileStem, zoomConfig.LdSpectrogramConfig, indexProperties);

            Stopwatch sw = Stopwatch.StartNew();

            // Set the default time-scales in seconds per pixel.
            // These were changed on 3rd April 2019 to better match those in the current zooming config file.
            double[] imageScales = { 60, 30, 15, 7.5, 3.2, 1.6, 0.8, 0.4, 0.2 };
            if (zoomConfig.SpectralIndexScale != null)
            {
                imageScales = zoomConfig.SpectralIndexScale;
            }

            sw = Stopwatch.StartNew();
            int scaleCount = imageScales.Length;
            var imageList  = new List <Image <Rgb24> >();

            for (int i = 0; i < scaleCount; i++)
            {
                var imageScale = TimeSpan.FromSeconds(imageScales[i]);
                var image      = DrawIndexSpectrogramAtScale(ldsConfig, indexGeneration, filteredIndexProperties, focalTime, dataScale, imageScale, imageWidth, spectra, fileStem);
                if (image != null)
                {
                    imageList.Add(image);
                    string name = $"{fileStem}_FocalZoom_min{focalTime.TotalMinutes:f1}_scale{imageScales[i]}.png";
                    image.Save(Path.Combine(outputDirectory.FullName, name));
                }
            }

            sw.Stop();
            LoggedConsole.WriteLine("Finished spectrograms derived from spectral indices. Elapsed time = " + sw.Elapsed.TotalSeconds + " seconds");

            // NOTE: The following code is deprecated. It was originally developed to provide some intermediate steps between the hi-resolution false-colour spectrograms
            // and the standard grey scale spectrograms.
            // ####################### DERIVE ZOOMED IN SPECTROGRAMS FROM STANDARD SPECTRAL FRAMES

            /*
             * int[] compressionFactor = { 8, 4, 2, 1 };
             * int compressionCount = compressionFactor.Length;
             * sw = Stopwatch.StartNew();
             * double frameStepInSeconds = indexGeneration.FrameStep / (double)indexGeneration.SampleRateResampled;
             * TimeSpan frameScale = TimeSpan.FromTicks((long)Math.Round(frameStepInSeconds * 10000000));
             * if (zoomConfig.SpectralFrameScale != null)
             * {
             *  imageScales = zoomConfig.SpectralFrameScale;
             *
             *  // TODO: CONVERT IMAGE scales into Compression factors.
             *  compressionCount = imageScales.Length;
             *  compressionFactor = new int[compressionCount];
             *  compressionFactor[compressionCount - 1] = 1;
             *  double denom = imageScales[compressionCount - 1];
             *
             *  for (int i = 0; i < compressionCount - 1; i++)
             *  {
             *      compressionFactor[i] = (int)Math.Round(imageScales[i] / denom);
             *  }
             * }
             *
             * int maxCompression = compressionFactor[0];
             * TimeSpan maxImageDuration = TimeSpan.FromTicks(maxCompression * imageWidth * frameScale.Ticks);
             *
             * TimeSpan halfMaxImageDuration = TimeSpan.FromMilliseconds(maxImageDuration.TotalMilliseconds / 2);
             * TimeSpan startTimeOfMaxImage = TimeSpan.Zero;
             * if (focalTime != TimeSpan.Zero)
             * {
             *  startTimeOfMaxImage = focalTime - halfMaxImageDuration;
             * }
             *
             * TimeSpan startTimeOfData = TimeSpan.FromMinutes(Math.Floor(startTimeOfMaxImage.TotalMinutes));
             *
             * List<double[]> frameData = ReadFrameData(inputDirectory, fileStem, startTimeOfMaxImage, maxImageDuration, zoomConfig, indexGeneration.MaximumSegmentDuration.Value);
             *
             * // get the index data to add into the
             * // TimeSpan imageScale1 = TimeSpan.FromSeconds(0.1);
             * double[,] indexData = spectra["PMN"];
             *
             * // make the images
             * for (int i = 0; i < compressionCount; i++)
             * {
             *  int factor = compressionFactor[i];
             *  var image = DrawFrameSpectrogramAtScale(ldsConfig, indexGeneration, startTimeOfData, factor, frameData, indexData, focalTime, frameScale, imageWidth);
             *  if (image != null)
             *  {
             *      imageList.Add(image);
             *  }
             * }
             *
             * sw.Stop();
             * LoggedConsole.WriteLine("Finished spectrograms derived from standard frames. Elapsed time = " + sw.Elapsed.TotalSeconds + " seconds");
             */

            // combine the images into a stack
            var    combinedImage = ImageTools.CombineImagesVertically(imageList);
            string fileName      = $"{fileStem}_FocalZOOM_min{focalTime.TotalMinutes:f1}.png";

            combinedImage.Save(Path.Combine(outputDirectory.FullName, fileName));
        }
        /// <summary>
        /// HERVE GLOTIN
        /// Combined audio2csv + zooming spectrogram task.
        /// This is used to analyse Herve Glotin's BIRD50 data set.
        /// ############################# IMPORTANT ########################################
        /// In order to analyse the short recordings in BIRD50 dataset, need following change to code:
        /// need to modify    AudioAnalysis.AnalysisPrograms.AcousticIndices.cs #line648
        /// need to change    AnalysisMinSegmentDuration = TimeSpan.FromSeconds(20),
        /// to                AnalysisMinSegmentDuration = TimeSpan.FromSeconds(1),
        /// THIS iS to analyse BIRD50 short recordings.
        /// </summary>
        public static void HiRes1()
        {
            string recordingPath = @"C:\SensorNetworks\WavFiles\TestRecordings\TEST_7min_artificial.wav";

            //// HERVE GLOTIN BIRD50 TRAINING RECORDINGS
            //DirectoryInfo dataDir = new DirectoryInfo(@"D:\SensorNetworks\WavFiles\Glotin\Bird50\AmazonBird50_training_input");
            //string parentDir = @"C:\SensorNetworks\Output\BIRD50";
            //string speciesLabelsFile = parentDir + @"\AmazonBird50_training_output.csv";
            //int speciesCount = 50;
            //////set file name format -depends on train or test. E.g.  "ID0003";
            //string fileStemFormatString = "ID{0:d4}";   // for training files
            //string indexPropertiesConfig = @"C:\Work\GitHub\audio-analysis\AudioAnalysis\AnalysisConfigFiles\IndexPropertiesConfigHiRes.yml";
            //string learningMode = "Train";

            //// HERVE GLOTIN BIRD50 TESTING RECORDINGS
            DirectoryInfo dataDir           = new DirectoryInfo(@"D:\SensorNetworks\WavFiles\Glotin\Bird50\AmazonBird50_testing_input");
            string        parentDir         = @"C:\SensorNetworks\Output\BIRD50";
            string        speciesLabelsFile = null;
            int           speciesCount      = 50;
            ////set file name format -depends on train or test. E.g.  "ID0003";
            string fileStemFormatString  = "ID1{0:d3}"; // for testing files
            string indexPropertiesConfig = @"C:\Work\GitHub\audio-analysis\AudioAnalysis\AnalysisConfigFiles\IndexPropertiesConfigHiRes.yml";
            string learningMode          = "Test";

            // HERVE GLOTIN BOMBYX WHALE RECORDINGS
            //DirectoryInfo dataDir = new DirectoryInfo(@"C:\SensorNetworks\WavFiles\WhaleFromGlotin");
            //string parentDir = @"C:\SensorNetworks\Output\Glotin\Bombyx_SpermWhales";
            //string speciesLabelsFile = null;
            //int speciesCount = 0;
            //////set file name format -depends on train or test. E.g.  "ID0003";
            //string fileStemFormatString = null;
            ////string fileStemFormatString = "ID1{0:d3}"; // for testing files
            //string indexPropertiesConfig = @"C:\Work\GitHub\audio-analysis\AudioAnalysis\AnalysisConfigFiles\IndexPropertiesConfigHiResGianniPavan.yml";
            //string learningMode = "Train";

            // GIANNI PAVAN SASSAFRAS RECORDINGS
            //DirectoryInfo dataDir = new DirectoryInfo(@"C:\SensorNetworks\WavFiles\GianniPavan\SABIOD - TEST SASSOFRATINO");
            //string parentDir = @"C:\SensorNetworks\Output\GianniPavan";
            //string speciesLabelsFile = null;
            //int speciesCount = 0;
            //string fileStemFormatString = null;
            //string indexPropertiesConfig = @"C:\Work\GitHub\audio-analysis\AudioAnalysis\AnalysisConfigFiles\IndexPropertiesConfigHiResGianniPavan.yml";
            //string learningMode = "Train";

            // ######################################################################

            string outputDir      = parentDir + @"\" + learningMode;
            string imageOutputDir = parentDir + @"\" + learningMode + "Images";
            string csvDir         = outputDir + @"\Towsey.Acoustic";
            string zoomOutputDir  = outputDir;

            string audio2csvConfigPath = @"C:\Work\GitHub\audio-analysis\AudioAnalysis\AnalysisConfigFiles\Towsey.AcousticHiRes.yml";
            string hiResZoomConfigPath = @"C:\Work\GitHub\audio-analysis\AudioAnalysis\AnalysisConfigFiles\SpectrogramHiResConfig.yml";

            FileInfo[] wavFiles = { new FileInfo(recordingPath) };

            // comment next two lines when debugging a single recording file
            string match = @"*.wav";

            wavFiles = dataDir.GetFiles(match, SearchOption.AllDirectories);

            // READ IN THE SPECIES LABELS FILE AND SET UP THE DATA
            string[] fileID    = new string[wavFiles.Length];
            int[]    speciesID = new int[speciesCount];
            if (speciesLabelsFile != null)
            {
                BirdClefExperiment1.ReadGlotinsSpeciesLabelFile(speciesLabelsFile, wavFiles.Length, out fileID, out speciesID);
            }
            else // make seperate species name for each file
            {
                speciesID = new int[wavFiles.Length];
            }

            //LOOP THROUGH ALL WAV FILES
            //for (int i = 538; i < 539; i++)
            //for (int i = 0; i < 8; i++)
            for (int i = 0; i < wavFiles.Length; i++)
            {
                FileInfo file = wavFiles[i];
                recordingPath = file.FullName;
                string idName = Path.GetFileNameWithoutExtension(file.FullName);
                string name   = string.Format("{0}_Species{1:d2}", idName, speciesID[i]);
                outputDir     = parentDir + @"\" + learningMode + @"\" + name;
                csvDir        = parentDir + @"\" + learningMode + @"\" + name + @"\Towsey.Acoustic";
                zoomOutputDir = outputDir;
                Console.WriteLine("\n\n");
                Console.WriteLine($@">>>>{i}: File<{name}>");

                try
                {
                    // A: analyse the recording files == audio2csv.
                    var audio2csvArguments = new AnalyseLongRecordings.AnalyseLongRecording.Arguments
                    {
                        Source = recordingPath.ToFileInfo(),
                        Config = audio2csvConfigPath,
                        Output = outputDir.ToDirectoryInfo(),
                    };

                    if (!audio2csvArguments.Source.Exists)
                    {
                        LoggedConsole.WriteWarnLine(" >>>>>>>>>>>> WARNING! The Source Recording file cannot be found! This will cause an exception.");
                    }

                    if (!File.Exists(audio2csvArguments.Config))
                    {
                        LoggedConsole.WriteWarnLine(" >>>>>>>>>>>> WARNING! The Configuration file cannot be found! This will cause an exception.");
                    }

                    AnalyseLongRecordings.AnalyseLongRecording.Execute(audio2csvArguments);

                    // B: Concatenate the summary indices and produce images
                    // Use the Zoomingspectrograms action.

                    // need to find out how long the recording is.
                    string        fileName     = audio2csvArguments.Source.BaseName();
                    string        testFileName = fileName + @"__Towsey.Acoustic.ACI.csv";
                    List <string> data         = FileTools.ReadTextFile(Path.Combine(csvDir, testFileName));
                    int           lineCount    = data.Count - 1; // -1 for header.
                    int           imageWidth   = lineCount;

                    //assume scale is index calculation duration = 0.1s
                    // i.e. image resolution  0.1s/px. or 600px/min
                    double focalMinute = (double)lineCount / 600 / 2;
                    if (focalMinute < 0.016666)
                    {
                        focalMinute = 0.016666; // shortest recording = 1 second.
                    }

                    var zoomingArguments = new DrawZoomingSpectrograms.Arguments
                    {
                        // use the default set of index properties in the AnalysisConfig directory.
                        SourceDirectory          = csvDir,
                        Output                   = zoomOutputDir,
                        SpectrogramZoomingConfig = hiResZoomConfigPath,

                        // draw a focused multi-resolution pyramid of images
                        ZoomAction = DrawZoomingSpectrograms.Arguments.ZoomActionType.Focused,
                    };

                    LoggedConsole.WriteLine("# Spectrogram Zooming config  : " + zoomingArguments.SpectrogramZoomingConfig);
                    LoggedConsole.WriteLine("# Input Directory             : " + zoomingArguments.SourceDirectory);
                    LoggedConsole.WriteLine("# Output Directory            : " + zoomingArguments.Output);

                    var common = new ZoomParameters(zoomingArguments.SourceDirectory.ToDirectoryEntry(), zoomingArguments.SpectrogramZoomingConfig.ToFileEntry(), false);

                    // Create directory if not exists
                    if (!Directory.Exists(zoomingArguments.Output))
                    {
                        Directory.CreateDirectory(zoomingArguments.Output);
                    }

                    ZoomFocusedSpectrograms.DrawStackOfZoomedSpectrograms(
                        zoomingArguments.SourceDirectory.ToDirectoryInfo(),
                        zoomingArguments.Output.ToDirectoryInfo(),
                        common,
                        TimeSpan.FromMinutes(focalMinute),
                        imageWidth,
                        AcousticIndices.TowseyAcoustic);

                    // DRAW THE VARIOUS IMAGES
                    string fileStem = fileName;
                    if (fileStemFormatString != null)
                    {
                        fileStem = string.Format(fileStemFormatString, i + 1); // training images
                    }

                    var ldfcSpectrogramArguments = new DrawLongDurationSpectrograms.Arguments
                    {
                        // use the default set of index properties in the AnalysisConfig directory.
                        InputDataDirectory    = csvDir,
                        OutputDirectory       = imageOutputDir,
                        IndexPropertiesConfig = indexPropertiesConfig,
                    };

                    // there are two possible tasks
                    // 1: draw the aggregated grey scale spectrograms
                    int secDuration = DrawLongDurationSpectrograms.DrawAggregatedSpectrograms(ldfcSpectrogramArguments, fileStem);

                    // 2: draw the coloured ridge spectrograms
                    secDuration = DrawLongDurationSpectrograms.DrawRidgeSpectrograms(ldfcSpectrogramArguments, fileStem);

                    // copy files
                    // POW, EVN, SPT, RHZ, RVT, RPS, RNG
                    string[]      copyArray            = { "POW", "EVN", "SPT", "RHZ", "RVT", "RPS", "RNG" };
                    DirectoryInfo sourceDirectory      = new DirectoryInfo(csvDir);
                    string        destinationDirectory = parentDir + @"\TrainingClassifier";
                    foreach (string key in copyArray)
                    {
                        // ID0002__Towsey.Acoustic.BGN.csv    fileName += @"__Towsey.Acoustic.ACI.csv";
                        string sourceFileName        = string.Format(idName + "__Towsey.Acoustic." + key + ".csv");
                        string sourcePath            = Path.Combine(sourceDirectory.FullName, sourceFileName);
                        string nameOfParentDirectory = sourceDirectory.Parent.Name;
                        string destinationFileName   = string.Format(nameOfParentDirectory + "." + key + ".csv");
                        string destinationPath       = Path.Combine(destinationDirectory, destinationFileName);
                        File.Copy(sourcePath, destinationPath, true);
                    }
                } // try block
                catch (Exception e)
                {
                    LoggedConsole.WriteErrorLine(string.Format("ERROR!!!!! RECORDING {0}   FILE {1}", i, name));
                    LoggedConsole.WriteErrorLine(string.Format(e.ToString()));
                }
            } // end loop through all wav files
        }     // HiRes1()
Exemple #4
0
        public static void Execute(Arguments arguments)
        {
            if (arguments == null)
            {
                throw new NoDeveloperMethodException();
            }

            string description;

            switch (arguments.ZoomAction)
            {
            case Arguments.ZoomActionType.Focused:
                description =
                    "# DRAW STACK OF FOCUSED MULTI-SCALE LONG DURATION SPECTROGRAMS DERIVED FROM SPECTRAL INDICES.";
                break;

            case Arguments.ZoomActionType.Tile:
                description =
                    "# DRAW ZOOMING SPECTROGRAMS DERIVED FROM SPECTRAL INDICES OBTAINED FROM AN AUDIO RECORDING";
                break;

            default:
                throw new ArgumentOutOfRangeException();
            }

            LoggedConsole.WriteLine(description);

            LoggedConsole.WriteLine("# Spectrogram Zooming config  : " + arguments.SpectrogramZoomingConfig);
            LoggedConsole.WriteLine("# Input Directory             : " + arguments.SourceDirectory);
            LoggedConsole.WriteLine("# Output Directory            : " + arguments.Output);

            var common = new ZoomParameters(
                arguments.SourceDirectory.ToDirectoryEntry(),
                arguments.SpectrogramZoomingConfig.ToFileEntry(),
                !string.IsNullOrEmpty(arguments.OutputFormat));

            LoggedConsole.WriteLine("# File name of recording      : " + common.OriginalBasename);

            // create file systems for reading input and writing output
            var io = FileSystemProvider.GetInputOutputFileSystems(
                arguments.SourceDirectory,
                FileSystemProvider.MakePath(arguments.Output, common.OriginalBasename, arguments.OutputFormat, "Tiles"))
                     .EnsureInputIsDirectory();

            switch (arguments.ZoomAction)
            {
            case Arguments.ZoomActionType.Focused:
                // draw a focused multi-resolution pyramid of images
                TimeSpan focalTime;
                if (arguments.FocusMinute.HasValue)
                {
                    focalTime = TimeSpan.FromMinutes(arguments.FocusMinute.Value);
                }
                else
                {
                    throw new ArgumentException("FocusMinute is null, cannot proceed");
                }

                const int ImageWidth = 1500;
                ZoomFocusedSpectrograms.DrawStackOfZoomedSpectrograms(
                    arguments.SourceDirectory.ToDirectoryInfo(),
                    arguments.Output.ToDirectoryInfo(),
                    common,
                    focalTime,
                    ImageWidth,
                    AcousticIndices.TowseyAcoustic);
                break;

            case Arguments.ZoomActionType.Tile:
                // Create the super tiles for a full set of recordings
                ZoomTiledSpectrograms.DrawTiles(
                    io,
                    common,
                    AcousticIndices.TowseyAcoustic);

                break;

            default:
                Log.Warn("Other ZoomAction results in standard LD Spectrogram to be drawn");

                // draw standard false color spectrograms - useful to check what spectrograms of the individual
                // indices are like.

                throw new NotImplementedException();

                /*LDSpectrogramRGB.DrawSpectrogramsFromSpectralIndices(
                 * arguments.SourceDirectory,
                 * arguments.Output,
                 * arguments.SpectrogramConfigPath,
                 * arguments.IndexPropertiesConfig);*/
                break;
            }
        }
Exemple #5
0
        public static void DrawStackOfZoomedSpectrograms(DirectoryInfo inputDirectory, DirectoryInfo outputDirectory, ZoomParameters common, TimeSpan focalTime, int imageWidth, string analysisType)
        {
            var zoomConfig = common.SpectrogramZoomingConfig;
            LdSpectrogramConfig ldsConfig = common.SpectrogramZoomingConfig.LdSpectrogramConfig;

            var distributions   = common.IndexDistributions;
            var indexGeneration = common.IndexGenerationData;

            string fileStem = common.OriginalBasename;

            TimeSpan dataScale = indexGeneration.IndexCalculationDuration;

            // ####################### DERIVE ZOOMED OUT SPECTROGRAMS FROM SPECTRAL INDICES

            string[] keys            = { "ACI", "BGN", "CVR", "DIF", "ENT", "EVN", "PMN", "POW", "RHZ", "RVT", "RPS", "RNG", "SUM", "SPT" };
            var      indexProperties = InitialiseIndexProperties.FilterIndexPropertiesForSpectralOnly(zoomConfig.IndexProperties);
            Dictionary <string, double[, ]> spectra = IndexMatrices.ReadSpectralIndices(inputDirectory, fileStem, analysisType, keys);

            Stopwatch sw = Stopwatch.StartNew();

            // standard scales in seconds per pixel.
            double[] imageScales = { 60, 24, 12, 6, 2, 1, 0.6, 0.2 };
            if (zoomConfig.SpectralIndexScale != null)
            {
                imageScales = zoomConfig.SpectralIndexScale;
            }

            sw = Stopwatch.StartNew();
            int scaleCount = imageScales.Length;
            var imageList  = new List <Image>();

            for (int i = 0; i < scaleCount; i++)
            {
                var imageScale = TimeSpan.FromSeconds(imageScales[i]);
                var image      = DrawIndexSpectrogramAtScale(ldsConfig, indexGeneration, indexProperties, focalTime, dataScale, imageScale, imageWidth, spectra, fileStem);
                if (image != null)
                {
                    imageList.Add(image);
                    string name = $"{fileStem}_FocalZoom_min{focalTime.TotalMinutes:f1}_scale{imageScales[i]}.png";
                    image.Save(Path.Combine(outputDirectory.FullName, name));
                }
            }

            sw.Stop();
            LoggedConsole.WriteLine("Finished spectrograms derived from spectral indices. Elapsed time = " + sw.Elapsed.TotalSeconds + " seconds");

            // ####################### DERIVE ZOOMED IN SPECTROGRAMS FROM STANDARD SPECTRAL FRAMES
            int[] compressionFactor = { 8, 4, 2, 1 };
            int   compressionCount  = compressionFactor.Length;

            sw = Stopwatch.StartNew();
            double   frameStepInSeconds = indexGeneration.FrameStep / (double)indexGeneration.SampleRateResampled;
            TimeSpan frameScale         = TimeSpan.FromTicks((long)Math.Round(frameStepInSeconds * 10000000));

            if (zoomConfig.SpectralFrameScale != null)
            {
                imageScales = zoomConfig.SpectralFrameScale;

                // TODO: CONVERT IMAGE scales into Compression factors.
                compressionCount  = imageScales.Length;
                compressionFactor = new int[compressionCount];
                compressionFactor[compressionCount - 1] = 1;
                double denom = imageScales[compressionCount - 1];

                for (int i = 0; i < compressionCount - 1; i++)
                {
                    compressionFactor[i] = (int)Math.Round(imageScales[i] / denom);
                }
            }

            int      maxCompression   = compressionFactor[0];
            TimeSpan maxImageDuration = TimeSpan.FromTicks(maxCompression * imageWidth * frameScale.Ticks);

            TimeSpan halfMaxImageDuration = TimeSpan.FromMilliseconds(maxImageDuration.TotalMilliseconds / 2);
            TimeSpan startTimeOfMaxImage  = TimeSpan.Zero;

            if (focalTime != TimeSpan.Zero)
            {
                startTimeOfMaxImage = focalTime - halfMaxImageDuration;
            }

            TimeSpan startTimeOfData = TimeSpan.FromMinutes(Math.Floor(startTimeOfMaxImage.TotalMinutes));

            List <double[]> frameData = ReadFrameData(inputDirectory, fileStem, startTimeOfMaxImage, maxImageDuration, zoomConfig, indexGeneration.MaximumSegmentDuration.Value);

            // get the index data to add into the
            // TimeSpan imageScale1 = TimeSpan.FromSeconds(0.1);
            double[,] indexData = spectra["POW"];

            // make the images
            for (int i = 0; i < compressionCount; i++)
            {
                int factor = compressionFactor[i];
                var image  = DrawFrameSpectrogramAtScale(ldsConfig, indexGeneration, startTimeOfData, factor, frameData, indexData, focalTime, frameScale, imageWidth);
                if (image != null)
                {
                    imageList.Add(image);
                }
            }

            sw.Stop();
            LoggedConsole.WriteLine("Finished spectrograms derived from standard frames. Elapsed time = " + sw.Elapsed.TotalSeconds + " seconds");

            // combine the images into a stack
            Image  combinedImage = ImageTools.CombineImagesVertically(imageList);
            string fileName      = $"{fileStem}_FocalZOOM_min{focalTime.TotalMinutes:f1}.png";

            combinedImage.Save(Path.Combine(outputDirectory.FullName, fileName));
        }