using OpenCvSharp; using StitchATon.Utility; namespace StitchATon.Services; public class ImageProvider { // Terminology // Chunk: 720*720 region thingy // Sector: Area defined in image chunk coordinates // Region: Area defined in image pixel coordinates public class Config(string imagePath, int sectorDim, int w, int h) { public string ImagePath = imagePath; public int SectorDim = sectorDim; public int W = w; public int H = h; } private Grid2D _ready; private Mat _canvas; private readonly Config _config; private ILogger _logger; enum ImageStatus { Blank, Loading, Ready } public ImageProvider( Config config, ILogger logger ) { _config = config; _logger = logger; _ready = new Grid2D( _config.W, _config.H ); _canvas = new Mat( _config.H * _config.SectorDim, _config.W * _config.SectorDim, MatType.CV_8UC3 ); } string GetImagePath( int x, int y ) { x++; y++; string letter = string.Empty; while (y > 0) { y--; // Adjust to make A=0, B=1, ..., Z=25 for modulo operation int remainder = y % 26; char digit = (char)('A' + remainder); letter = digit + letter; y /= 26; } var filename = $"{letter}{x}.png"; return Path.Join( _config.ImagePath, filename ); } async Task LoadImage( int x, int y ) { _ready[x, y] = ImageStatus.Loading; string path = GetImagePath( x, y ); _logger.LogInformation( $"{path} not loaded yet, reading" ); using Mat image = await Task.Run( () => Cv2.ImRead( path ) ); image.CopyTo( GetChunkMat(x, y) ); _ready[x, y] = ImageStatus.Ready; } // After this function is run, it is guaranteed that all images concerned within the SoI is loaded to the grand canvas. // Has a flagging mechanism to just wait if another call of this function is currently loading it. async Task LoadImages(Rect soi) { _logger.LogInformation( $"{soi.Width * soi.Height} chunks required" ); List? loadTasks = null; List<(int x, int y)>? loadedByOthers = null; for( int x = soi.Left; x < soi.Right; x++ ) for( int y = soi.Top; y < soi.Bottom; y++ ) switch( _ready[x, y] ) { case ImageStatus.Blank: if( loadTasks == null ) loadTasks = new List( soi.Width * soi.Height ); loadTasks.Add( LoadImage( x, y ) ); break; case ImageStatus.Loading: if( loadedByOthers == null ) loadedByOthers = new List<(int, int)>( 5 ); loadedByOthers.Add( (x, y) ); break; } if( loadTasks != null ) { await Task.WhenAll( loadTasks ); _logger.LogInformation( $"Finished loading {loadTasks.Count} images" ); } // Spinlock until all images are loaded. 1ms delay to prevent processor overload while( loadedByOthers != null && loadedByOthers.Count != 0 ) { await Task.Delay( 1 ); loadedByOthers.RemoveAll( coord => _ready[coord.x, coord.y] == ImageStatus.Ready ); } } Mat GetChunkMat( int x, int y ) { var roi = new Rect( _config.SectorDim * x, _config.SectorDim * y, _config.SectorDim, _config.SectorDim ); return _canvas[roi]; } Rect GetGlobalRoi( Rect soi, Point2f roiOffsetRatio, Point2f roiSizeRatio ) { var soiSizePx = new Size( soi.Size.Width * _config.SectorDim, soi.Size.Height * _config.SectorDim ); return new Rect( ( soi.X * _config.SectorDim ) + (int)( soiSizePx.Width * roiOffsetRatio.X ), ( soi.Y * _config.SectorDim ) + (int)( soiSizePx.Height * roiOffsetRatio.Y ), (int)( soiSizePx.Width * roiSizeRatio.X ), (int)( soiSizePx.Height * roiSizeRatio.Y ) ); } Rect GetSoi( Rect roi ) { var tl = roi.TopLeft; var br = roi.BottomRight; var soiTl = new Point( tl.X / _config.SectorDim, tl.Y / _config.SectorDim ); var soiBr = new Point( (int) Math.Ceiling( br.X / (float) _config.SectorDim ), (int) Math.Ceiling( br.Y / (float) _config.SectorDim ) ); return new Rect( soiTl.X, soiTl.Y, soiBr.X - soiTl.X, soiBr.Y - soiTl.Y ); } public async Task GetImage( Rect soi, Point2f roiOffsetRatio, Point2f roiSizeRatio ) { var globalRoi = GetGlobalRoi( soi, roiOffsetRatio, roiSizeRatio); var adaptedSoi = GetSoi( globalRoi ); await LoadImages( adaptedSoi ); return _canvas[globalRoi]; } }