First Commit
This commit is contained in:
parent
bb40883c7d
commit
696158848f
18 changed files with 787 additions and 0 deletions
13
.idea/.idea.StitchATon/.idea/.gitignore
generated
vendored
Normal file
13
.idea/.idea.StitchATon/.idea/.gitignore
generated
vendored
Normal file
|
|
@ -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
|
||||||
4
.idea/.idea.StitchATon/.idea/encodings.xml
generated
Normal file
4
.idea/.idea.StitchATon/.idea/encodings.xml
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||||
|
</project>
|
||||||
8
.idea/.idea.StitchATon/.idea/indexLayout.xml
generated
Normal file
8
.idea/.idea.StitchATon/.idea/indexLayout.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="UserContentModel">
|
||||||
|
<attachedFolders />
|
||||||
|
<explicitIncludes />
|
||||||
|
<explicitExcludes />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.StitchATon/.idea/vcs.xml
generated
Normal file
6
.idea/.idea.StitchATon/.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
101
README.md
Normal file
101
README.md
Normal file
|
|
@ -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=<path> 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
106
StitchATon.Bench/Program.cs
Normal file
106
StitchATon.Bench/Program.cs
Normal file
|
|
@ -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<ImageProvider>.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<Program>();
|
||||||
|
// 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" );
|
||||||
|
}
|
||||||
|
}
|
||||||
21
StitchATon.Bench/StitchATon.Bench.csproj
Normal file
21
StitchATon.Bench/StitchATon.Bench.csproj
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>StitchATon.Bench</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
|
||||||
|
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
|
||||||
|
<PackageReference Include="OpenCvSharp4_runtime.debian.12-x64" Version="4.10.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\StitchATon\StitchATon.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
20
StitchATon.sln
Normal file
20
StitchATon.sln
Normal file
|
|
@ -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
|
||||||
98
StitchATon/Controller.cs
Normal file
98
StitchATon/Controller.cs
Normal file
|
|
@ -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<PngNamedPipe> npPool, ILogger<Controller> logger ) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
[Route("generate")]
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
48
StitchATon/DTO/Generate.cs
Normal file
48
StitchATon/DTO/Generate.cs
Normal file
|
|
@ -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] );
|
||||||
|
}
|
||||||
50
StitchATon/Program.cs
Normal file
50
StitchATon/Program.cs
Normal file
|
|
@ -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<ImageProvider>();
|
||||||
|
|
||||||
|
// FIFO named pipe pool
|
||||||
|
builder.Services.AddSingleton<ObjectPool<PngNamedPipe>>( new DefaultObjectPool<PngNamedPipe>(
|
||||||
|
new DefaultPooledObjectPolicy<PngNamedPipe>(),
|
||||||
|
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();
|
||||||
51
StitchATon/Properties/launchSettings.json
Normal file
51
StitchATon/Properties/launchSettings.json
Normal file
|
|
@ -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/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
172
StitchATon/Services/ImageProvider.cs
Normal file
172
StitchATon/Services/ImageProvider.cs
Normal file
|
|
@ -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<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];
|
||||||
|
}
|
||||||
|
}
|
||||||
22
StitchATon/StitchATon.csproj
Normal file
22
StitchATon/StitchATon.csproj
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.18"/>
|
||||||
|
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
|
||||||
|
<PackageReference Include="OpenCvSharp4_runtime.debian.12-x64" Version="4.10.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="JetBrains.Annotations">
|
||||||
|
<HintPath>..\..\..\.nuget\packages\jetbrains.annotations\2023.3.0\lib\netstandard2.0\JetBrains.Annotations.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
26
StitchATon/Utility/Grid2D.cs
Normal file
26
StitchATon/Utility/Grid2D.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
namespace StitchATon.Utility;
|
||||||
|
|
||||||
|
public class Grid2D<T>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
StitchATon/Utility/NamedPipe.cs
Normal file
22
StitchATon/Utility/NamedPipe.cs
Normal file
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
9
StitchATon/appsettings.Development.json
Normal file
9
StitchATon/appsettings.Development.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"StitchATon.Controller": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
StitchATon/appsettings.json
Normal file
10
StitchATon/appsettings.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"StitchATon.Controller": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue