diff --git a/.idea/.idea.StitchATon/.idea/.gitignore b/.idea/.idea.StitchATon/.idea/.gitignore
new file mode 100644
index 0000000..78e63ec
--- /dev/null
+++ b/.idea/.idea.StitchATon/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/.idea.StitchATon.iml
+/contentModel.xml
+/projectSettingsUpdater.xml
+/modules.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.idea.StitchATon/.idea/encodings.xml b/.idea/.idea.StitchATon/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/.idea/.idea.StitchATon/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.StitchATon/.idea/indexLayout.xml b/.idea/.idea.StitchATon/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/.idea.StitchATon/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.StitchATon/.idea/vcs.xml b/.idea/.idea.StitchATon/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/.idea.StitchATon/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fc53482
--- /dev/null
+++ b/README.md
@@ -0,0 +1,101 @@
+# I Paid for 4 Gigabytes of RAM So I Will Use All 4 Gigabytes of RAM and Probably More
+Submission for Stitch-A-Ton Contest.
+
+## Prerequisites
+- **Dotnet installation**
+- **~4 GB** of memory on heavier operations. Command below sets the swap file size to 4GB.
+ ```
+ sudo dphys-swapfile swapoff
+ sudo sed -i 's/^\(CONF_SWAPSIZE=\).*/\14096/' /etc/dphys-swapfile
+ sudo dphys-swapfile setup
+ sudo dphys-swapfile swapon
+ sudo reboot
+ ```
+ If the test cases does not request a large portion of the canvas ***AND*** resizes it to a relatively still large ratio (i,e, ~0.9) **at the same time**, this might not be necessary.
+- **Access to** the `mkfifo` command, satisfied by default bar very special cases
+- **OpenCVSharp4 Runtime for Raspberry Pi 5**
+ A copy exists in this repository's release page.
+
+
+## Running
+On root directory:
+```
+ASSET_PATH_RO= dotnet run --project StitchATon --profile deploy
+```
+
+The API is accessible at `:5255`, providing the following api:
+- [POST] `api/image/generate`: complies with competition guidelines
+- [GET] `api/image/sanity`: generates a predefined crop region:
+
+ `G6:I8`, `(.1, .1)` offset, `(.8,.8)` crop, at 0.6 scale.
+
+To browse the API, prepend `ASPNETCORE_ENVIRONMENT=Development` to the command and go to `/swagger/index.html`.
+
+## Writeup
+
+This submission contains no specific magic in the image processing, just OpenCVSharp stretched to the best of its ability according to my knowledge. This section contains a brief overview of the main features.
+
+Per the writer's knowledge, the end result is **fast enough for the operation to be bottlenecked by network transfer speed** instead of image processing, except when resizing.
+
+*(note: I don't do rigorous benchmarks for that, take it with a grain of salt)*
+
+### Canvas Memory Usage
+During initialization, a blank 55x31 of 720x720 canvas is created, along with a 55*31 grid of enums indicating whether a chunk is already loaded, is currently being loaded, or ready to use.
+
+The memory usage of this canvas follows what chunks has been loaded into it, topping at ~2.6GB when all chunks are loaded.
+
+When multiple requests refer to the same region of the canvas, it doesn't need to be loaded again.
+
+### Coordinate Processing
+When parsing the request, it's possible that the requested canvas size doesn't correspond to what chunks that will actually be read; for example `A1:A3` at no offset and `(0.2, 1)` crop will only read some parts of `A1` chunk.
+
+```
+ canvas
+┌─────────────────┬─────────────────┬─────────────────┐
+│ ┌───────────┐ │ │ │
+│ │ │ │ │ │
+│ │ final │ │ │ │
+│ │ result │ │ │ │
+│ │ │ │ │ │
+│ └───────────┘ │ │ │
+└─────────────────┴─────────────────┴─────────────────┘
+```
+
+To handle that, the resulting global crop RoI is calculated first and the chunks that are *actually* needed is identified (referred as *Adapted Sectors of Interest*).
+
+### Chunk Loading
+OpenCV Mats are thread safe given any operation is performed on non-overlapping regions of it. This allows multitasking reading the chunk to the main canvas.
+
+After coordinate processing, chunks in the adapted SoI are checked for their load status, then processed accordingly.
+
+The status "Currently being loaded" is relevant when multiple requests requiring the same chunk(s) are underway; on such case the loader that checks later spinlocks until it's done loading.
+
+**This mechanism enables the shared canvas to serve multiple processes.**
+
+### Serving and Encoding cropped image
+After the needed chunks are certain to be loaded to the main canvas, next is cropping it; which is a trivial operation in OpenCV, not requiring any extra memory since it still refers to the main canvas.
+
+What's not trivial is *encoding* it, which after some quick tests shows to take longer than reading and decoding multiple images from the canvas.
+
+OpenCV provides two functions that can help decode to PNG:
+- `Cv2.ImWrite` that writes to a file, and
+- `Cv2.ImEncode` that writes to a byte array.
+
+Both of these requires the encoding to finish before the resulting data can be used.
+
+To alleviate this problem, a **named pipe** (some sort of file pointer that works as a pipe buffer) is used; `ImWrite`-ing to said named pipe and have ASP.NET read from it. By doing this:
+- The encode and send process is parallelized
+- No extra memory needs to be allocated to encode the image; either on storage or RAM
+
+### (Unsolved) Resizing
+
+This remains as the only pain point that's not straightforward to solve. If no resize is requested, it's solvable by cropping off the main canvas and encoding it to a named pipe; eliminating a lot of time and memory overhead on the way.
+
+If resize is requested, a new Mat containing the resized image needs to be allocated.
+
+Ideas for this problem:
+- resize function that outputs a stream,
+- Imencode/imwrite function that accepts a stream.
+
+
+
diff --git a/StitchATon.Bench/Program.cs b/StitchATon.Bench/Program.cs
new file mode 100644
index 0000000..c0ad218
--- /dev/null
+++ b/StitchATon.Bench/Program.cs
@@ -0,0 +1,106 @@
+using System.Diagnostics;
+using BenchmarkDotNet.Attributes;
+using Microsoft.Extensions.Logging.Abstractions;
+using OpenCvSharp;
+using StitchATon.Services;
+
+namespace StitchATon.Bench;
+
+public class Program
+{
+ static readonly ImageProvider Ip = new ( new ImageProvider.Config(
+ "/home/retorikal/Downloads/tiles1705/",
+ 720,
+ 55,
+ 31),
+ NullLogger.Instance );
+ // static ImageProvider ip = new ( "/mnt/ramdisk/" );
+
+ static async Task Fn1()
+ {
+ using var im = await Ip.GetImage(
+ new Rect( new Point( 0, 0 ), new Size( 2, 1 ) ),
+ new Point2f( 0, 0 ),
+ new Point2f( 1, 1 ) );
+ // Cv2.ImEncode(".png", im, out _);
+ Cv2.ImWrite("/tmp/im1.png", im );
+ }
+
+ static async Task Fn2()
+ {
+ using var im = await Ip.GetImage(
+ new Rect( new Point( 1, 1 ), new Size( 3, 3 ) ),
+ new Point2f( 0, 0 ),
+ new Point2f( 1, 1 ) );
+ // Cv2.ImEncode(".png", im, out _);
+ // Cv2.ImWrite("/tmp/im2.png", im2 );
+ }
+
+ static async Task Fn3()
+ {
+ using var im = await Ip.GetImage(
+ new Rect( new Point( 2, 2 ), new Size( 3, 3 ) ),
+ new Point2f( 0.2f, 0.2f ),
+ new Point2f( .6f, .6f ) );
+ // Cv2.ImEncode(".png", im, out _);
+ // Cv2.ImWrite("/tmp/im3.png", im3 );
+ }
+
+ static async Task Fn4()
+ {
+ using var im = await Ip.GetImage(
+ new Rect( new Point( 0, 0 ), new Size( 10, 10 ) ),
+ new Point2f( 0.2f, 0.2f ),
+ new Point2f( .6f, .6f ) );
+ // Cv2.ImEncode(".png", im, out _);
+ // Cv2.ImWrite("/tmp/im4.png", im4 );
+ }
+
+
+ // [Benchmark]
+ static async Task Conc()
+ {
+ await Task.WhenAll(
+ Fn1(),
+ Fn2(),
+ Fn3(),
+ Fn4()
+ );
+ }
+
+ // [Benchmark]
+ static async Task Serial()
+ {
+ await Fn1();
+ await Fn2();
+ await Fn3();
+ await Fn4();
+ }
+
+ public static ProcessStartInfo MkfifoPs = new("mkfifo", "/tmp/out.png");
+ public static ProcessStartInfo RmfifoPs = new("rm", "/tmp/out.png");
+
+ [Benchmark]
+ public void Mkfifo()
+ {
+ Process.Start( MkfifoPs )?.WaitForExit();
+ Process.Start( RmfifoPs )?.WaitForExit();
+ }
+
+ public async static Task Main( string[] args )
+ {
+
+ await Fn1();
+ // var summary = BenchmarkRunner.Run();
+ // var stopwatch = Stopwatch.StartNew();
+ //
+ //
+ // stopwatch.Stop();
+ // Console.WriteLine( $"Elapsed time: {stopwatch.ElapsedMilliseconds} ms" );
+ //
+ // var stopwatchAfter = Stopwatch.StartNew();
+ // await Fn4();
+ // stopwatchAfter.Stop();
+ // Console.WriteLine( $"Elapsed time: {stopwatchAfter.ElapsedMilliseconds} ms" );
+ }
+}
\ No newline at end of file
diff --git a/StitchATon.Bench/StitchATon.Bench.csproj b/StitchATon.Bench/StitchATon.Bench.csproj
new file mode 100644
index 0000000..afa069d
--- /dev/null
+++ b/StitchATon.Bench/StitchATon.Bench.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ StitchATon.Bench
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StitchATon.sln b/StitchATon.sln
new file mode 100644
index 0000000..e553226
--- /dev/null
+++ b/StitchATon.sln
@@ -0,0 +1,20 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon", "StitchATon\StitchATon.csproj", "{CE9E5242-9FC1-4B19-BD20-C6A2103C2DC8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon.Bench", "StitchATon.Bench\StitchATon.Bench.csproj", "{84B2813B-56FB-4DB8-999D-9F363DBF5E19}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {CE9E5242-9FC1-4B19-BD20-C6A2103C2DC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CE9E5242-9FC1-4B19-BD20-C6A2103C2DC8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CE9E5242-9FC1-4B19-BD20-C6A2103C2DC8}.Debug|Any CPU.ActiveCfg = Release|Any CPU
+ {CE9E5242-9FC1-4B19-BD20-C6A2103C2DC8}.Debug|Any CPU.Build.0 = Release|Any CPU
+ {84B2813B-56FB-4DB8-999D-9F363DBF5E19}.Debug|Any CPU.ActiveCfg = Release|Any CPU
+ {84B2813B-56FB-4DB8-999D-9F363DBF5E19}.Debug|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/StitchATon/Controller.cs b/StitchATon/Controller.cs
new file mode 100644
index 0000000..0b8aa0a
--- /dev/null
+++ b/StitchATon/Controller.cs
@@ -0,0 +1,98 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.ObjectPool;
+using OpenCvSharp;
+using StitchATon.DTO;
+using StitchATon.Services;
+using StitchATon.Utility;
+
+namespace StitchATon;
+
+[ApiController]
+[Route("api/image/")]
+public class ImageController( ImageProvider ip, ObjectPool npPool, ILogger logger ) : ControllerBase
+{
+ [HttpPost]
+ [Route("generate")]
+ public async Task GetImage([FromBody] GenerateInput generateInput)
+ {
+ logger.LogInformation( $"GetImage requested at {generateInput.CanvasRect}" );
+
+ var namedPipe = npPool.Get();
+ var imagePath = namedPipe.PipeFullname;
+
+ using var roiIm = await ip.GetImage(
+ generateInput.ParsedCanvasRect(),
+ generateInput.ParsedCropOffset(),
+ generateInput.ParsedCropSize()
+ );
+
+ var scaledSize = new Size(
+ roiIm.Cols * generateInput.OutputScale,
+ roiIm.Rows * generateInput.OutputScale
+ );
+ Mat resizedIm = new();
+
+ if( scaledSize == roiIm.Size() )
+ resizedIm = roiIm;
+ else
+ Cv2.Resize( roiIm, resizedIm, scaledSize );
+
+ // Spawn new task to write to the named pipe
+ Task.Run( () =>
+ {
+ Cv2.ImWrite( imagePath, resizedIm );
+ resizedIm.Dispose();
+ } );
+
+ // Stream the named pipe as output
+ var fileStream = new FileStream(
+ imagePath,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read,
+ 4096,
+ FileOptions.Asynchronous | FileOptions.SequentialScan);
+
+ return File(fileStream, "image/png");
+ }
+
+ [HttpGet]
+ [Route("sanity")]
+ public async Task GetImageSanityTest()
+ {
+ var namedPipe = npPool.Get();
+ var imagePath = namedPipe.PipeFullname;
+
+ using var roiIm = await ip.GetImage(
+ new Rect( new Point( 5, 6 ), new Size( 3, 3 ) ),
+ new Point2f( .1f, .1f ),
+ new Point2f( .8f, .8f ) );
+
+ var mul = .7;
+ var scaledSize = new Size(roiIm.Cols * mul , roiIm.Rows * mul);
+ Mat resizedIm = new();
+
+ if( scaledSize == roiIm.Size() )
+ resizedIm = roiIm;
+ else
+ Cv2.Resize( roiIm, resizedIm, scaledSize );
+
+ // Spawn new task to write to the named pipe
+ Task.Run( () =>
+ {
+ Cv2.ImWrite( imagePath, resizedIm );
+ resizedIm.Dispose();
+ } );
+
+ // Stream the named pipe as output
+ var fileStream = new FileStream(
+ imagePath,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read,
+ 4096,
+ FileOptions.Asynchronous | FileOptions.SequentialScan);
+
+ return File(fileStream, "image/png");
+ }
+}
diff --git a/StitchATon/DTO/Generate.cs b/StitchATon/DTO/Generate.cs
new file mode 100644
index 0000000..8e05eb3
--- /dev/null
+++ b/StitchATon/DTO/Generate.cs
@@ -0,0 +1,48 @@
+using JetBrains.Annotations;
+using OpenCvSharp;
+
+namespace StitchATon.DTO;
+
+public class GenerateInput
+{
+ public required string CanvasRect { get; [UsedImplicitly] init; }
+ public required float[] CropOffset { get; [UsedImplicitly] init; }
+ public required float[] CropSize { get; [UsedImplicitly] init; }
+ public required float OutputScale { get; [UsedImplicitly] init; }
+
+ public Rect ParsedCanvasRect()
+ {
+ var corners = CanvasRect.Split( ":" );
+ var c1 = ParseCanvasCoord( corners[0] );
+ var c2 = ParseCanvasCoord( corners[1] );
+
+ return new Rect(
+ int.Min( c1.X, c2.X ),
+ int.Min( c1.Y, c2.Y ),
+ int.Abs( c1.X - c2.X ) + 1, // Inclusive bbox
+ int.Abs( c1.Y - c2.Y ) + 1 // Inclusive bbox
+ );
+ }
+
+ private Point ParseCanvasCoord(string labwareCoord)
+ {
+ int y = 0;
+ int x = 0;
+ foreach( var c in labwareCoord )
+ {
+ if( 'A' <= c && c <= 'Z' )
+ y = ( y * 26 ) + ( c - 'A' + 1);
+ else
+ x = ( x * 10 ) + ( c - '0' );
+ }
+
+ y--;
+ x--;
+
+ return new(x, y);
+ }
+
+ public Point2f ParsedCropOffset() => new( CropOffset[0], CropOffset[1] );
+
+ public Point2f ParsedCropSize() => new( CropSize[0], CropSize[1] );
+}
\ No newline at end of file
diff --git a/StitchATon/Program.cs b/StitchATon/Program.cs
new file mode 100644
index 0000000..5b69be0
--- /dev/null
+++ b/StitchATon/Program.cs
@@ -0,0 +1,50 @@
+using System.Text.Json;
+using Microsoft.Extensions.ObjectPool;
+using StitchATon.Services;
+using StitchATon.Utility;
+
+
+var builder = WebApplication.CreateBuilder( args );
+
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+var path = Environment.GetEnvironmentVariable( "ASSET_PATH_RO" );
+if( path == null )
+ throw new ArgumentException("Please supply the directory path on ASSET_PATH_RO env var");
+
+// Image Loader
+builder.Services.AddSingleton( new ImageProvider.Config(
+ path,
+ 720,
+ 55,
+ 31) );
+builder.Services.AddSingleton();
+
+// FIFO named pipe pool
+builder.Services.AddSingleton>( new DefaultObjectPool(
+ new DefaultPooledObjectPolicy(),
+ 10 ) );
+builder.Services.AddControllers().AddJsonOptions( options =>
+ {
+ options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
+ options.JsonSerializerOptions.WriteIndented = true;
+ }
+
+ );
+builder.Logging.AddConsole();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if( app.Environment.IsDevelopment() )
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseCors();
+app.UseRouting();
+app.MapControllers();
+
+app.Run();
diff --git a/StitchATon/Properties/launchSettings.json b/StitchATon/Properties/launchSettings.json
new file mode 100644
index 0000000..c8b7b7d
--- /dev/null
+++ b/StitchATon/Properties/launchSettings.json
@@ -0,0 +1,51 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:24734",
+ "sslPort": 44313
+ }
+ },
+ "profiles": {
+ "deploy": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5255"
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5255",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASSET_PATH_RO": "/home/retorikal/Downloads/tiles1705/"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7105;http://localhost:5255",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASSET_PATH_RO": "/home/retorikal/Downloads/tiles1705/"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASSET_PATH_RO": "/home/retorikal/Downloads/tiles1705/"
+ }
+ }
+ }
+}
diff --git a/StitchATon/Services/ImageProvider.cs b/StitchATon/Services/ImageProvider.cs
new file mode 100644
index 0000000..ea119b4
--- /dev/null
+++ b/StitchATon/Services/ImageProvider.cs
@@ -0,0 +1,172 @@
+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];
+ }
+}
\ No newline at end of file
diff --git a/StitchATon/StitchATon.csproj b/StitchATon/StitchATon.csproj
new file mode 100644
index 0000000..ccf4e06
--- /dev/null
+++ b/StitchATon/StitchATon.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\.nuget\packages\jetbrains.annotations\2023.3.0\lib\netstandard2.0\JetBrains.Annotations.dll
+
+
+
+
diff --git a/StitchATon/Utility/Grid2D.cs b/StitchATon/Utility/Grid2D.cs
new file mode 100644
index 0000000..46a922d
--- /dev/null
+++ b/StitchATon/Utility/Grid2D.cs
@@ -0,0 +1,26 @@
+namespace StitchATon.Utility;
+
+public class Grid2D
+{
+ private T[] _buffer;
+ public readonly int W;
+ public readonly int H;
+
+ public Grid2D( int width, int height )
+ {
+ W = width;
+ H = height;
+ _buffer = new T[W * H];
+ }
+
+ private int Map( int x, int y )
+ {
+ return x + ( y * W );
+ }
+
+ public T this[ int x, int y ]
+ {
+ get => _buffer[Map( x, y )];
+ set => _buffer[Map( x, y )] = value;
+ }
+}
\ No newline at end of file
diff --git a/StitchATon/Utility/NamedPipe.cs b/StitchATon/Utility/NamedPipe.cs
new file mode 100644
index 0000000..0fea233
--- /dev/null
+++ b/StitchATon/Utility/NamedPipe.cs
@@ -0,0 +1,22 @@
+using System.Diagnostics;
+
+namespace StitchATon.Utility;
+
+public class PngNamedPipe : IDisposable
+{
+ private ProcessStartInfo _mkfifoPs = new("mkfifo");
+ private ProcessStartInfo _rmfifoPs = new("rm");
+ public readonly string PipeFullname = Path.Join( Path.GetTempPath(), Guid.NewGuid() + ".png" );
+
+ public PngNamedPipe( )
+ {
+ _mkfifoPs.Arguments = PipeFullname;
+ Process.Start( _mkfifoPs )?.WaitForExit();
+ }
+
+ public void Dispose()
+ {
+ _rmfifoPs.Arguments = PipeFullname;
+ Process.Start( _rmfifoPs );
+ }
+}
\ No newline at end of file
diff --git a/StitchATon/appsettings.Development.json b/StitchATon/appsettings.Development.json
new file mode 100644
index 0000000..7e8b4b2
--- /dev/null
+++ b/StitchATon/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "StitchATon.Controller": "Information"
+ }
+ }
+}
diff --git a/StitchATon/appsettings.json b/StitchATon/appsettings.json
new file mode 100644
index 0000000..f769bb8
--- /dev/null
+++ b/StitchATon/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "StitchATon.Controller": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}