/// <summary>
        /// Joins summary indices csv files together.
        /// This method merges ALL the passed files of acoustic indices
        /// It is assumed you are concatenating a sequence of consecutive short recordings.
        /// </summary>
        public static List <SummaryIndexValues> ConcatenateAllSummaryIndexFiles(
            FileInfo[] summaryIndexFiles,
            DirectoryInfo opDir,
            IndexGenerationData indexGenerationData,
            string outputFileBaseName)
        {
            var indexResolution = indexGenerationData.IndexCalculationDuration;

            var summaryIndices = IndexMatrices.ConcatenateSummaryIndexFilesWithTimeCheck(summaryIndexFiles, indexResolution);

            if (summaryIndices.Count == 0)
            {
                LoggedConsole.WriteErrorLine("WARNING: LDSpectrogramStitching.ConcatenateAllSummaryIndexFiles(): Empty List of SUMMARY indices returned!");
                return(null);
            }

            // check length of data and make adjustments if required.
            // NOTHING done with this info at the moment. Could be used to truncate data to 24 hours.
            //int totalRowMinutes = (int)Math.Round(summaryIndices.Count() * indexResolution.TotalMinutes);

            // write out the list of data file names to JSON file.
            var arrayOfFileNames = summaryIndices.Select(x => x.FileName).ToArray();
            var path             = FilenameHelpers.AnalysisResultPath(opDir, outputFileBaseName, "FileNames", "json");

            Json.Serialise(new FileInfo(path), arrayOfFileNames);

            return(summaryIndices);
        }
        /// <summary>
        /// Loads a csv file of summary indices, normalises the values for visualisation and displays a TracksImage
        /// </summary>
        /// <param name="arguments"></param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <returns></returns>
        public static void Main(Arguments arguments)
        {
            bool verbose = true;

            if (verbose)
            {
                string date = "# DATE AND TIME: " + DateTime.Now;
                LoggedConsole.WriteLine("# MAKE AN IMAGE FROM A CSV FILE OF SUMMARY INDICES DERIVED FROM AN AUDIO RECORDING");
                LoggedConsole.WriteLine(date);
                LoggedConsole.WriteLine("# Input  .csv   file: " + arguments.InputCsv);
                LoggedConsole.WriteLine("# Output image  file: " + arguments.Output);
                LoggedConsole.WriteLine();
            }

            var input  = arguments.InputCsv.ToFileInfo();
            var output = arguments.Output.ToFileInfo();

            output.CreateParentDirectories();

            // Find required index generation data
            var igd = IndexGenerationData.GetIndexGenerationData(input.Directory.ToDirectoryEntry());

            // Convert summary indices to image
            string        fileName    = input.BaseName();
            string        title       = $"SOURCE:{fileName},   {Meta.OrganizationTag};  ";
            Image <Rgb24> tracksImage = IndexDisplay.DrawImageOfSummaryIndexTracks(
                input,
                arguments.IndexPropertiesConfig.ToFileInfo(),
                title,
                igd.IndexCalculationDuration,
                igd.RecordingStartDate);

            tracksImage.Save(output.FullName);
        }
        /// <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);
        }
 /// <summary>
 /// Read in required files.
 /// We expect a valid indices output directory (the input directory in this action)
 /// to contain a SpectralIndexStatistics.json and a IndexGenerationData.json file.
 /// </summary>
 public static (FileInfo indexGenerationDataFile, FileInfo indexDistributionsFile) CheckNeededFilesExist(
     DirectoryInfo indicesDirectory)
 {
     // MT NOTE: This file (IndexDistributions.json) should not be compulsory requirement for this activity. At the most a warning could be
     // written to say that file not found.
     // AT NOTE 2017-11-06: Now loads the file as an optional dependency
     return(
         indexGenerationDataFile : IndexGenerationData.FindFile(indicesDirectory),
         indexDistributionsFile : indicesDirectory.EnumerateFiles("*" + Indices.IndexDistributions.SpectralIndexStatisticsFilenameFragment + "*").SingleOrDefault());
 }
        /// <summary>
        /// ONLY Use this concatenation method when you want to concatenate the files for a fixed single day.
        /// The files to be concatenated must be somewhere in the subdirectory structure of the passed list of data directories
        /// Read them into a dictionary
        /// MOST RECENT METHOD TO CONCATENATE Spectral INDEX.CSV FILES - Early September 2015.
        /// It is designed to deal with Yvonne's case where want to concatenate files distributed over arbitrary directories.
        /// It only merges files for the passed fixed date. i.e only 24 hours
        /// </summary>
        public static void DrawSpectralIndexFiles(
            Dictionary <string, double[, ]> dictionary,
            LdSpectrogramConfig sgConfig,
            IndexGenerationData indexGenerationData,
            FileInfo indexPropertiesConfigFileInfo,
            DirectoryInfo opDir,
            SiteDescription siteDescription,
            FileInfo sunriseDataFile          = null,
            List <GapsAndJoins> segmentErrors = null)
        {
            // derive new indices such as sqrt(PMN), NCDI etc -- main reason for this is to view what their distributions look like.
            dictionary = IndexMatrices.AddDerivedIndices(dictionary);

            // Calculate the index distribution statistics and write to a json file. Also save as png image
            if (indexGenerationData.RecordingStartDate != null)
            {
                DateTimeOffset dto        = (DateTimeOffset)indexGenerationData.RecordingStartDate;
                string         dateString = $"{dto.Year}{dto.Month:D2}{dto.Day:D2}";
                string         opFileStem = $"{siteDescription.SiteName}_{dateString}";

                var indexDistributions = IndexDistributions.WriteSpectralIndexDistributionStatistics(dictionary, opDir, opFileStem);

                //SummaryIndexBase[] summaryIndices = null;
                string analysisType = "Towsey.Acoustic";

                Tuple <Image, string>[] tuple = LDSpectrogramRGB.DrawSpectrogramsFromSpectralIndices(
                    opDir, // topLevelDirectories[0], // this should not be required but it is - because things have gotten complicated !
                    opDir,
                    sgConfig,
                    indexPropertiesConfigFileInfo,
                    indexGenerationData,
                    opFileStem,
                    analysisType,
                    dictionary,
                    null, //summaryIndices,
                    indexDistributions,
                    siteDescription,
                    sunriseDataFile,
                    segmentErrors,
                    ImageChrome.With);
            }
        }
Exemple #6
0
        public static void DrawSummaryIndexFiles(
            Dictionary <string, double[]> dictionaryOfCsvColumns,
            IndexGenerationData indexGenerationData,
            FileInfo indexPropertiesConfigFileInfo,
            DirectoryInfo opDir,
            SiteDescription siteDescription,
            FileInfo sunriseDatafile = null,
            List <GapsAndJoins> erroneousSegments = null, // info if have fatal errors i.e. no signal
            bool verbose = false)
        {
            var dto = (DateTimeOffset)indexGenerationData.RecordingStartDate;

            string dateString = $"{dto.Year}{dto.Month:D2}{dto.Day:D2}";
            string opFileStem = $"{siteDescription.SiteName}_{dateString}";

            // Calculate the index distribution statistics and write to a json file. Also save as png image
            var indexDistributions = IndexDistributions.WriteSummaryIndexDistributionStatistics(dictionaryOfCsvColumns, opDir, opFileStem);

            var    start     = ((DateTimeOffset)indexGenerationData.RecordingStartDate).TimeOfDay;
            string startTime = $"{start.Hours:d2}{start.Minutes:d2}h";

            if (start.Hours == 0 && start.Minutes == 0)
            {
                startTime = "midnight";
            }

            string titletext =
                $"SOURCE: \"{opFileStem}\".     Starts at {startTime}                       {Meta.OrganizationTag}";
            Bitmap tracksImage = IndexDisplay.DrawImageOfSummaryIndices(
                IndexProperties.GetIndexProperties(indexPropertiesConfigFileInfo),
                dictionaryOfCsvColumns,
                titletext,
                indexGenerationData.IndexCalculationDuration,
                indexGenerationData.RecordingStartDate,
                sunriseDatafile,
                erroneousSegments,
                verbose);
            var imagePath = FilenameHelpers.AnalysisResultPath(opDir, opFileStem, SummaryIndicesStr, ImgFileExt);

            tracksImage.Save(imagePath);
        }
        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 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);
        }
        // ##############################################################################################################
        // ######################### METHODS FOR STITCHING TNC - EDDIE GAME's DATA
        // ######################### CONCATENATE EVERYTHING

        /// <summary>
        /// RECENT METHOD TO CONCATENATE Spectral INDEX.CSV FILES - August 2015. Revised Septermber 2016
        /// Was written to deal with  EDDIE GAME PNG data where the files to be concatenated are all in one top level directory.
        /// This method merges all files of spectral indices in the passed directories.
        /// The total length of the concatenated files can exceed 24 hours - limited by memory!
        /// </summary>
        public static Dictionary <string, double[, ]> ConcatenateAllSpectralIndexFiles(DirectoryInfo[] directories, string[] keys, IndexGenerationData indexGenerationData)
        {
            string analysisType = "Towsey.Acoustic";
            var    dictionaryOfSpectralIndices = IndexMatrices.GetSpectralIndexFilesAndConcatenate(directories, analysisType, keys, indexGenerationData, true);

            if (dictionaryOfSpectralIndices.Count == 0)
            {
                LoggedConsole.WriteErrorLine("WARNING from method LDSpectrogramStitching.ConcatenateSpectralIndexFiles() !!!");
                LoggedConsole.WriteErrorLine("        An empty dictionary of spectral indices was returned !!! ");
                return(null);
            }

            // now add in derived indices i.e. POW, NCDI etc
            // dictionaryOfSpectralIndices = IndexMatrices.AddDerivedIndices(dictionaryOfSpectralIndices);
            return(dictionaryOfSpectralIndices);
        }
        private static (TilingProfile Profile, TimeSpan padding) GetTilingProfile(
            ZoomParameters common,
            SpectrogramZoomingConfig zoomConfig,
            IndexGenerationData indexGeneration,
            double maxScale)
        {
            TilingProfile namingPattern;
            TimeSpan      padding;

            switch (zoomConfig.TilingProfile)
            {
            case nameof(PanoJsTilingProfile):
                namingPattern = new PanoJsTilingProfile();
                padding       = TimeSpan.Zero;

                if (zoomConfig.TileWidth != namingPattern.TileWidth)
                {
                    throw new ConfigFileException(
                              "TileWidth must match the default PanoJS TileWidth of " + namingPattern.TileWidth);
                }

                break;

            case nameof(AbsoluteDateTilingProfile):
                // Zooming spectrograms use multiple color profiles at different levels
                // therefore unable to set a useful tag (like ACI-ENT-EVN).
                if (indexGeneration.RecordingStartDate != null)
                {
                    var recordingStartDate = (DateTimeOffset)indexGeneration.RecordingStartDate;
                    var tilingStartDate    = GetPreviousTileBoundary(
                        zoomConfig.TileWidth,
                        maxScale,
                        recordingStartDate);
                    padding = recordingStartDate - tilingStartDate;

                    // if we're not writing tiles to disk, omit the basename because whatever the container format is
                    // it will have the base name attached
                    namingPattern = new AbsoluteDateTilingProfile(
                        common.OmitBasename ? string.Empty : common.OriginalBasename,
                        "BLENDED.Tile",
                        tilingStartDate,
                        indexGeneration.FrameLength / 2,
                        zoomConfig.TileWidth);
                }
                else
                {
                    throw new ArgumentNullException(
                              nameof(zoomConfig.TilingProfile),
                              "`RecordingStateDate` from the `IndexGenerationData.json` cannot be null when `AbsoluteDateTilingProfile` specified");
                }

                break;

            default:
                throw new ConfigFileException(
                          $"The {nameof(zoomConfig.TilingProfile)} configuration property was set to an unsupported value - no profile known by that name");
            }

            Log.Info($"Tiling had a left padding duration of {padding} to align tiles");
            Log.Info(
                $"Tiling using {namingPattern.GetType().Name}, Tile Width: {namingPattern.TileWidth}, Height: {namingPattern.TileHeight}");
            return(namingPattern, padding);
        }
        /// <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);
            }
        }
        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);
        }
Exemple #14
0
        public void TestAnalyzeSr22050Recording()
        {
            int    sampleRate = 22050;
            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);
            var   recordingPath = this.outputDirectory.CombineFile("TemporaryRecording1.wav");

            WavWriter.WriteWavFileViaFfmpeg(recordingPath, recording.WavReader);

            // draw the signal as spectrogram just for debugging purposes

            /*
             * var fst = FreqScaleType.Linear;
             * var freqScale = new FrequencyScale(fst);
             * var sonoConfig = new SonogramConfig
             * {
             *  WindowSize = 512,
             *  WindowOverlap = 0.0,
             *  SourceFName = recording.BaseName,
             *  NoiseReductionType = NoiseReductionType.Standard,
             *  NoiseReductionParameter = 2.0,
             * };
             * var sonogram = new SpectrogramStandard(sonoConfig, recording.WavReader);
             * var image = sonogram.GetImageFullyAnnotated(sonogram.GetImage(), "SPECTROGRAM", freqScale.GridLineLocations);
             * var outputImagePath = this.outputDirectory.CombineFile("Signal1_LinearFreqScale.png");
             * image.Save(outputImagePath.FullName);
             */

            var configPath = PathHelper.ResolveConfigFile("Towsey.Acoustic.yml");

            var arguments = new AnalyseLongRecording.Arguments
            {
                Source        = recordingPath,
                Config        = configPath.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(38, 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(21, pngCount);

            var twoMapsImagePath = resultsDirectory.CombineFile("TemporaryRecording1__2Maps.png");
            var twoMapsImage     = Image.Load <Rgb24>(twoMapsImagePath.FullName);

            // image is 7 * 632
            Assert.AreEqual(7, twoMapsImage.Width);
            Assert.AreEqual(632, twoMapsImage.Height);

            var bgnFile = resultsDirectory.CombineFile("TemporaryRecording1__Towsey.Acoustic.BGN.csv");

            double[,] actualBgn = Csv.ReadMatrixFromCsv <double>(bgnFile, TwoDimensionalArray.None);

            var expectedSpectrumFile = PathHelper.ResolveAsset("LongDuration", "BgnMatrix.LinearScale.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(7, expectedBgn.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("Spectrum1.png");

            GraphsAndCharts.DrawGraph(array, "LD BGN SPECTRUM Linear", ldsBgnSpectrumFile);

            var generationData = Json.Deserialize <IndexGenerationData>(IndexGenerationData.FindFile(resultsDirectory));

            Assert.AreEqual("TemporaryRecording1", generationData.RecordingBasename);
        }
        /// <summary>
        /// This method can add in absolute time if you want.
        /// Currently commented out - see below.
        /// </summary>
        public static Image <Rgb24> DrawIndexSpectrogramAtScale(
            LdSpectrogramConfig config,
            IndexGenerationData indexGenerationData,
            Dictionary <string, IndexProperties> indexProperties,
            TimeSpan focalTime,
            TimeSpan dataScale,
            TimeSpan imageScale,
            int imageWidth,
            Dictionary <string, double[, ]> spectra,
            string basename)
        {
            if (spectra == null)
            {
                LoggedConsole.WriteLine("WARNING: NO SPECTRAL DATA SUPPLIED");
                return(null);
            }

            // check that scalingFactor >= 1.0
            double scalingFactor = Math.Round(imageScale.TotalMilliseconds / dataScale.TotalMilliseconds);

            if (scalingFactor < 1.0)
            {
                LoggedConsole.WriteLine("WARNING: Scaling Factor < 1.0");
                return(null);
            }

            Dictionary <string, IndexProperties> dictIp = indexProperties;

            dictIp = InitialiseIndexProperties.FilterIndexPropertiesForSpectralOnly(dictIp);

            // calculate start time by combining DatetimeOffset with minute offset.
            TimeSpan sourceMinuteOffset = indexGenerationData.AnalysisStartOffset;

            if (indexGenerationData.RecordingStartDate.HasValue)
            {
                DateTimeOffset dto = (DateTimeOffset)indexGenerationData.RecordingStartDate;
                sourceMinuteOffset = dto.TimeOfDay + sourceMinuteOffset;
            }

            // calculate data duration from column count of abitrary matrix
            var kvp    = spectra.First();
            var matrix = kvp.Value;

            //var matrix = spectra["ACI"]; // assume this key will always be present!!
            TimeSpan dataDuration = TimeSpan.FromSeconds(matrix.GetLength(1) * dataScale.TotalSeconds);

            TimeSpan recordingStartTime = TimeSpan.Zero; // default = zero minute of day i.e. midnight

            recordingStartTime = indexGenerationData.RecordingStartDate.Value.TimeOfDay.Add(indexGenerationData.AnalysisStartOffset);

            TimeSpan offsetTime        = TimeSpan.Zero;
            TimeSpan imageDuration     = TimeSpan.FromTicks(imageWidth * imageScale.Ticks);
            TimeSpan halfImageDuration = TimeSpan.FromTicks(imageWidth * imageScale.Ticks / 2);
            TimeSpan startTime         = TimeSpan.Zero;

            if (focalTime != TimeSpan.Zero)
            {
                startTime = focalTime - halfImageDuration;
            }

            if (startTime < TimeSpan.Zero)
            {
                offsetTime = TimeSpan.Zero - startTime;
                startTime  = TimeSpan.Zero;
            }

            TimeSpan endTime = imageDuration;

            if (focalTime != TimeSpan.Zero)
            {
                endTime = focalTime + halfImageDuration;
            }

            if (endTime > dataDuration)
            {
                endTime = dataDuration;
            }

            TimeSpan spectrogramDuration = endTime - startTime;
            int      spectrogramWidth    = (int)(spectrogramDuration.Ticks / imageScale.Ticks);

            // get the plain unchromed spectrogram
            var ldfcSpectrogram = ZoomCommon.DrawIndexSpectrogramCommon(
                config,
                indexGenerationData,
                indexProperties,
                startTime,
                endTime,
                dataScale,
                imageScale,
                imageWidth,
                spectra,
                basename);

            if (ldfcSpectrogram == null)
            {
                LoggedConsole.WriteLine("WARNING: NO SPECTROGRAM AT SCALE " + imageScale);
                return(null);
            }

            // now chrome spectrogram
            ldfcSpectrogram.Mutate(g2 =>
            {
                // draw red line at focus time
                if (focalTime != TimeSpan.Zero)
                {
                    Pen pen = new Pen(Color.Red, 1);
                    TimeSpan focalOffset = focalTime - startTime;
                    int x1 = (int)(focalOffset.Ticks / imageScale.Ticks);
                    g2.DrawLine(pen, x1, 0, x1, ldfcSpectrogram.Height);
                }
            });

            // draw the title bar
            int nyquist = 22050 / 2; // default

            if (indexGenerationData.SampleRateResampled > 0)
            {
                nyquist = indexGenerationData.SampleRateResampled / 2;
            }

            int herzInterval = 1000;

            if (config != null)
            {
                herzInterval = config.YAxisTicInterval;
            }

            string title = $"SCALE={imageScale.TotalSeconds}s/px.  Duration={spectrogramDuration} ";

            //add chrome
            // NEXT LINE USED ONLY IF WANT ABSOLUTE TIME
            //startTime += recordingStartTime;
            var titleBar = DrawTitleBarOfZoomSpectrogram(title, ldfcSpectrogram.Width);

            ldfcSpectrogram = FrameZoomSpectrogram(
                ldfcSpectrogram,
                titleBar,
                startTime,
                imageScale,
                config.XAxisTicInterval,
                nyquist,
                herzInterval);

            // create the base canvas image on which to centre the focal image
            var image = Drawing.NewImage(imageWidth, ldfcSpectrogram.Height, Color.DarkGray);

            int xOffset = (int)(offsetTime.Ticks / imageScale.Ticks);

            image.Mutate(g1 => g1.DrawImage(ldfcSpectrogram, new Point(xOffset, 0), 1));
            return(image);
        }
        /// <summary>
        /// This method can add in the absolute recording start time. However currently disabled.
        /// </summary>
        /// <param name="config">v.</param>
        /// <param name="indexGenerationData">indexGenerationData.</param>
        /// <param name="startTimeOfData">startTimeOfData.</param>
        /// <param name="compressionFactor">compressionFactor.</param>
        /// <param name="frameData">frameData.</param>
        /// <param name="indexData">indexData.</param>
        /// <param name="focalTime">focalTime.</param>
        /// <param name="frameScale">frameScale.</param>
        /// <param name="imageWidth">imageWidth.</param>
        public static Image DrawFrameSpectrogramAtScale(
            LdSpectrogramConfig config,
            IndexGenerationData indexGenerationData,
            TimeSpan startTimeOfData,
            int compressionFactor,
            List <double[]> frameData,
            double[,] indexData,
            TimeSpan focalTime,
            TimeSpan frameScale,
            int imageWidth)
        {
            if (frameData == null || frameData.Count == 0)
            {
                LoggedConsole.WriteLine("WARNING: NO SPECTRAL SPECTROGRAM DATA SUPPLIED");
                return(null);
            }

            // var recordingStartTime = TimeSpan.Zero; // default = zero minute of day i.e. midnight
            // var recordingStartTime = TimeTools.DateTimePlusTimeSpan(indexGenerationData.RecordingStartDate, indexGenerationData.AnalysisStartOffset);

            TimeSpan imageScale        = TimeSpan.FromTicks(frameScale.Ticks * compressionFactor);
            TimeSpan imageDuration     = TimeSpan.FromTicks(imageWidth * imageScale.Ticks);
            TimeSpan halfImageDuration = TimeSpan.FromTicks(imageWidth * imageScale.Ticks / 2);
            TimeSpan startTime         = focalTime - halfImageDuration;

            if (startTime < TimeSpan.Zero)
            {
                startTime = TimeSpan.Zero;
            }

            int             startIndex         = (int)((startTime.Ticks - startTimeOfData.Ticks) / frameScale.Ticks);
            int             requiredFrameCount = imageWidth * compressionFactor;
            List <double[]> frameSelection     = frameData.GetRange(startIndex, requiredFrameCount);

            double[,] spectralSelection = MatrixTools.ConvertList2Matrix(frameSelection);

            // compress spectrograms to correct scale
            if (compressionFactor > 1)
            {
                spectralSelection = TemporalMatrix.CompressFrameSpectrograms(spectralSelection, compressionFactor);
            }

            var spectrogramImage = DrawStandardSpectrogramInFalseColour(spectralSelection);

            int x1 = (int)(halfImageDuration.Ticks / imageScale.Ticks);

            spectrogramImage.Mutate(g2 =>
            {
                // draw focus time on image
                if (focalTime != TimeSpan.Zero)
                {
                    Pen pen = new Pen(Color.Red, 1);
                    g2.DrawLine(pen, x1, 0, x1, spectrogramImage.Height);
                }
            });

            int nyquist = 22050 / 2; // default

            if (indexGenerationData.SampleRateResampled > 0)
            {
                nyquist = indexGenerationData.SampleRateResampled / 2;
            }

            int    herzInterval = config.YAxisTicInterval;
            string title        = $"ZOOM SCALE={imageScale.TotalMilliseconds}ms/pixel   Image duration={imageDuration} ";
            var    titleBar     = DrawTitleBarOfZoomSpectrogram(title, spectrogramImage.Width);

            // add the recording start time ONLY IF WANT ABSOLUTE TIME SCALE - obtained from info in file name
            // startTime += recordingStartTime;
            spectrogramImage = FrameZoomSpectrogram(spectrogramImage, titleBar, startTime, imageScale, config.XAxisTicInterval, nyquist, herzInterval);

            // MAY WANT THESE CLIPPING TRACKS AT SOME POINT
            // read high amplitude and clipping info into an image
            //string indicesFile = Path.Combine(configuration.InputDirectoryInfo.FullName, fileStem + ".csv");
            //string indicesFile = Path.Combine(config.InputDirectoryInfo.FullName, fileStem + ".Indices.csv");
            //string indicesFile = Path.Combine(configuration.InputDirectoryInfo.FullName, fileStem + "_" + configuration.AnalysisType + ".csv");
            //Image imageX = DrawSummaryIndices.DrawHighAmplitudeClippingTrack(indicesFile.ToFileInfo());
            //if (null != imageX) imageX.Save(Path.Combine(outputDirectory.FullName, fileStem + ".ClipHiAmpl.png"));

            // create the base image
            Image image = new Image <Rgb24>(imageWidth, spectrogramImage.Height);

            image.Mutate(g1 =>
            {
                g1.Clear(Color.DarkGray);

                //int xOffset = (int)(startTime.Ticks / imageScale.Ticks);
                int xOffset = (imageWidth / 2) - x1;
                g1.DrawImage(spectrogramImage, new Point(xOffset, 0), 1);
            });

            return(image);
        }
        /// <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);
        }
        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);
        }
        /// <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);
        }
        private DirectoryInfo CreateTestData(DirectoryInfo output)
        {
            // create 11 "day"s of data, with three spectral ribbon variants
            var sourceDirectory = output.CreateSubdirectory("FakeIndices");

            var firstDate = new DateTimeOffset(2019, 4, 18, 0, 0, 0, TimeSpan.FromHours(10));

            CreateDay(Increment(0), "ACI-ENT-EVN", "BGN-PMN-OSC", Color.Red, Color.Blue);
            CreateDay(Increment(1), "ACI-ENT-EVN", "BGN-PMN-OSC", Color.Red, Color.Blue);
            CreateDay(Increment(1), "ACI-ENT-EVN", "BGN-PMN-OSC", Color.Red, Color.Blue);
            CreateDay(Increment(1), "ACI-ENT-EVN", "BGN-PMN-OSC", Color.Red, Color.Blue);
            CreateDay(Increment(1), "ACI-ENT-EVN", "BGN-PMN-OSC", Color.Red, Color.Blue);
            CreateDay(Increment(1), "ACI-ENT-EVN", "BGN-PMN-OSC", Color.Red, Color.Blue);

            CreateDay(Increment(2), "ACI-ENT-EVN", "ENT-CVR-OSC", Color.Red, Color.Green);
            CreateDay(Increment(1), "ACI-ENT-EVN", "ENT-CVR-OSC", Color.Red, Color.Green);
            CreateDay(Increment(1), "ACI-ENT-EVN", "ENT-CVR-OSC", Color.Red, Color.Green);
            CreateDay(Increment(1), "ACI-ENT-EVN", "ENT-CVR-OSC", Color.Red, Color.Green);
            CreateDay(Increment(1), "ACI-ENT-EVN", "ENT-CVR-OSC", Color.Red, Color.Green);

            return(sourceDirectory);

            DateTimeOffset Increment(int days)
            {
                firstDate = firstDate.AddDays(days);
                return(firstDate);
            }

            void CreateDay(DateTimeOffset startDate, string colorMap1, string colorMap2, Rgb24 color1, Rgb24 color2)
            {
                var basename  = startDate.ToIso8601SafeString();
                var extension = Random.NextChoice(".wav", ".mp3", ".flac");
                var data      = new IndexGenerationData()
                {
                    //Source = this.outputDirectory.CombineFile(name),
                    AnalysisStartOffset           = TimeSpan.Zero,
                    BackgroundFilterCoeff         = SpectrogramConstants.BACKGROUND_FILTER_COEFF,
                    BgNoiseNeighbourhood          = IndexCalculateConfig.DefaultBgNoiseNeighborhood.Seconds(),
                    FrameLength                   = 512,
                    FrameStep                     = 512,
                    IndexCalculationDuration      = 60.Seconds(),
                    LongDurationSpectrogramConfig = new LdSpectrogramConfig()
                    {
                        ColorMap1 = colorMap1,
                        ColorMap2 = colorMap2,
                    },
                    MaximumSegmentDuration = 60.Seconds(),
                    RecordingBasename      = basename,
                    RecordingDuration      = TimeSpan.FromHours(24),
                    RecordingExtension     = extension,
                    RecordingStartDate     = startDate,
                    SampleRateOriginal     = 22050,
                    SampleRateResampled    = 22050,
                };

                var icdPath = FilenameHelpers.AnalysisResultPath(
                    sourceDirectory,
                    basename,
                    IndexGenerationData.FileNameFragment,
                    "json");

                Json.Serialise(icdPath.ToFileInfo(), data);

                var ribbon = new Image <Rgb24>(Configuration.Default, 1440, LdSpectrogramRibbons.RibbonPlotHeight, color1);

                ribbon.Save(FilenameHelpers.AnalysisResultPath(sourceDirectory, basename, colorMap1 + LdSpectrogramRibbons.SpectralRibbonTag, "png"));
                ribbon = new Image <Rgb24>(Configuration.Default, 1440, LdSpectrogramRibbons.RibbonPlotHeight, color2);
                ribbon.Save(FilenameHelpers.AnalysisResultPath(sourceDirectory, basename, colorMap2 + LdSpectrogramRibbons.SpectralRibbonTag, "png"));
            }
        }
Exemple #21
0
        [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);
        }
        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 = acousticIndicesConfig.LdSpectrogramConfig;

            string basename = Path.GetFileNameWithoutExtension(sourceAudio.Name);

            // output to disk (so other analyzers 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()
            {
                RecordingExtension            = 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 <Rgb24>, 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);
                    }
                }
            }
        }
        public override void SummariseResults(
            AnalysisSettings analysisSettings,
            FileSegment inputFileSegment,
            EventBase[] events,
            SummaryIndexBase[] indices,
            SpectralIndexBase[] spectralIndices,
            AnalysisResult2[] results)
        {
            // below is example of how to access values in ContentDescription config file.
            //sampleRate = analysisSettings.Configuration.GetIntOrNull(AnalysisKeys.ResampleRate) ?? sampleRate;
            var cdConfiguration     = (CdConfig)analysisSettings.Configuration;
            var ldSpectrogramConfig = cdConfiguration.LdSpectrogramConfig;

            //var cdConfigFile = analysisSettings.ConfigFile;
            //var configDirectory = cdConfigFile.DirectoryName ?? throw new ArgumentNullException(nameof(cdConfigFile), "Null value");
            var    sourceAudio      = inputFileSegment.Source;
            string basename         = Path.GetFileNameWithoutExtension(sourceAudio.Name);
            var    resultsDirectory = AnalysisCoordinator.GetNamedDirectory(analysisSettings.AnalysisOutputDirectory, this);

            // check for null values - this was recommended by ReSharper!
            if (inputFileSegment.TargetFileDuration == null || inputFileSegment.TargetFileSampleRate == null)
            {
                throw new NullReferenceException();
            }

            // output config data to disk so other analyzers can use the data,
            // Should contain data only - i.e. the configuration settings that generated these indices
            // this data can then be used by later analysis processes.
            var indexConfigData = new IndexGenerationData()
            {
                RecordingExtension            = inputFileSegment.Source.Extension,
                RecordingBasename             = basename,
                RecordingStartDate            = inputFileSegment.TargetFileStartDate,
                RecordingDuration             = inputFileSegment.TargetFileDuration.Value,
                SampleRateOriginal            = inputFileSegment.TargetFileSampleRate.Value,
                SampleRateResampled           = ContentSignatures.SampleRate,
                FrameLength                   = ContentSignatures.FrameSize,
                FrameStep                     = ContentSignatures.FrameSize,
                IndexCalculationDuration      = TimeSpan.FromSeconds(ContentSignatures.IndexCalculationDurationInSeconds),
                BgNoiseNeighbourhood          = TimeSpan.FromSeconds(5), // default value for content description
                AnalysisStartOffset           = inputFileSegment.SegmentStartOffset ?? TimeSpan.Zero,
                MaximumSegmentDuration        = analysisSettings.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
            var dictionaryOfSpectra = spectralIndices.ToTwoDimensionalArray(SpectralIndexValuesForContentDescription.CachedSelectors, TwoDimensionalArray.Rotate90ClockWise);

            // Calculate the index distribution statistics and write to a json file. Also save as png image
            // The following method returns var indexDistributions =, but we have no use for them.
            IndexDistributions.WriteSpectralIndexDistributionStatistics(dictionaryOfSpectra, resultsDirectory, basename);

            // Draw ldfc spectrograms and return path to 2maps image.
            string ldfcSpectrogramPath =
                DrawSpectrogramsFromSpectralIndices(
                    ldSpectrogramConfig,
                    outputDirectory: resultsDirectory,
                    indexGenerationData: indexConfigData,
                    basename: basename,
                    indexSpectrograms: dictionaryOfSpectra);

            // Gather the content description results into an array of DescriptionResult and then convert to dictionary
            var allContentDescriptionResults = results.Select(x => (DescriptionResult)x.MiscellaneousResults[nameof(DescriptionResult)]);
            var contentDictionary            = DataProcessing.ConvertResultsToDictionaryOfArrays(allContentDescriptionResults.ToList());

            // Write the results to a csv file
            var filePath = Path.Combine(resultsDirectory.FullName, "AcousticSignatures.csv");

            // TODO: fix this so it writes header and a column of content description values.
            //Csv.WriteToCsv(new FileInfo(filePath), contentDictionary);
            FileTools.WriteDictionaryAsCsvFile(contentDictionary, filePath);

            // prepare graphical plots of the acoustic signatures.
            var contentPlots = GetPlots(contentDictionary);
            var images       = GraphsAndCharts.DrawPlotDistributions(contentPlots);
            var plotsImage   = ImageTools.CombineImagesVertically(images);

            plotsImage.Save(Path.Combine(resultsDirectory.FullName, "DistributionsOfContentScores.png"));

            // Attach content description plots to LDFC spectrogram and write to file
            var ldfcSpectrogram = Image.Load <Rgb24>(ldfcSpectrogramPath);
            var image           = ContentVisualization.DrawLdfcSpectrogramWithContentScoreTracks(ldfcSpectrogram, contentPlots);
            var path3           = Path.Combine(resultsDirectory.FullName, basename + ".ContentDescription.png");

            image.Save(path3);
        }