stitch_something/StitchATon/Services/ImageProvider.cs

172 lines
4.5 KiB
C#
Raw Permalink Normal View History

2025-08-01 15:29:06 +07:00
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<ImageStatus> _ready;
private Mat _canvas;
private readonly Config _config;
private ILogger<ImageProvider> _logger;
enum ImageStatus
{
Blank,
Loading,
Ready
}
public ImageProvider( Config config, ILogger<ImageProvider> logger )
{
_config = config;
_logger = logger;
_ready = new Grid2D<ImageStatus>( _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<Task>? 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<Task>( 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<Mat> GetImage( Rect soi, Point2f roiOffsetRatio, Point2f roiSizeRatio )
{
var globalRoi = GetGlobalRoi( soi, roiOffsetRatio, roiSizeRatio);
var adaptedSoi = GetSoi( globalRoi );
await LoadImages( adaptedSoi );
return _canvas[globalRoi];
}
}