From 696158848f4643818b93afc294a357a41468b0c1 Mon Sep 17 00:00:00 2001 From: "reinard.setiadji@formulatrix.com" Date: Fri, 1 Aug 2025 15:29:06 +0700 Subject: [PATCH] First Commit --- .idea/.idea.StitchATon/.idea/.gitignore | 13 ++ .idea/.idea.StitchATon/.idea/encodings.xml | 4 + .idea/.idea.StitchATon/.idea/indexLayout.xml | 8 + .idea/.idea.StitchATon/.idea/vcs.xml | 6 + README.md | 101 +++++++++++ StitchATon.Bench/Program.cs | 106 ++++++++++++ StitchATon.Bench/StitchATon.Bench.csproj | 21 +++ StitchATon.sln | 20 +++ StitchATon/Controller.cs | 98 +++++++++++ StitchATon/DTO/Generate.cs | 48 ++++++ StitchATon/Program.cs | 50 ++++++ StitchATon/Properties/launchSettings.json | 51 ++++++ StitchATon/Services/ImageProvider.cs | 172 +++++++++++++++++++ StitchATon/StitchATon.csproj | 22 +++ StitchATon/Utility/Grid2D.cs | 26 +++ StitchATon/Utility/NamedPipe.cs | 22 +++ StitchATon/appsettings.Development.json | 9 + StitchATon/appsettings.json | 10 ++ 18 files changed, 787 insertions(+) create mode 100644 .idea/.idea.StitchATon/.idea/.gitignore create mode 100644 .idea/.idea.StitchATon/.idea/encodings.xml create mode 100644 .idea/.idea.StitchATon/.idea/indexLayout.xml create mode 100644 .idea/.idea.StitchATon/.idea/vcs.xml create mode 100644 README.md create mode 100644 StitchATon.Bench/Program.cs create mode 100644 StitchATon.Bench/StitchATon.Bench.csproj create mode 100644 StitchATon.sln create mode 100644 StitchATon/Controller.cs create mode 100644 StitchATon/DTO/Generate.cs create mode 100644 StitchATon/Program.cs create mode 100644 StitchATon/Properties/launchSettings.json create mode 100644 StitchATon/Services/ImageProvider.cs create mode 100644 StitchATon/StitchATon.csproj create mode 100644 StitchATon/Utility/Grid2D.cs create mode 100644 StitchATon/Utility/NamedPipe.cs create mode 100644 StitchATon/appsettings.Development.json create mode 100644 StitchATon/appsettings.json 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": "*" +}