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); }
/// <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); }
/// <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); }
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); } } }