Compare commits
No commits in common. "master" and "0.0.0" have entirely different histories.
22 changed files with 0 additions and 910 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -28,7 +28,6 @@
|
|||
[Oo]bj/
|
||||
_UpgradeReport_Files/
|
||||
[Pp]ackages/
|
||||
/.contest
|
||||
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
|
|
|||
13
.idea/.idea.StitchATon/.idea/.gitignore
generated
vendored
13
.idea/.idea.StitchATon/.idea/.gitignore
generated
vendored
|
|
@ -1,13 +0,0 @@
|
|||
# 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
4
.idea/.idea.StitchATon/.idea/encodings.xml
generated
|
|
@ -1,4 +0,0 @@
|
|||
<?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
8
.idea/.idea.StitchATon/.idea/indexLayout.xml
generated
|
|
@ -1,8 +0,0 @@
|
|||
<?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
6
.idea/.idea.StitchATon/.idea/vcs.xml
generated
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
1
LocalNuget/.gitignore
vendored
1
LocalNuget/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
*.nupkg
|
||||
14
NuGet.Config
14
NuGet.Config
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
<add key="local" value="./LocalNuget" />
|
||||
<add key="fmlx-nuget" value="https://nexus.formulatrix.com/repository/nuget/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
<packageSourceCredentials>
|
||||
<fmlx-nuget>
|
||||
<add key="Username" value="formulatrix" />
|
||||
<add key="ClearTextPassword" value="faraday435" />
|
||||
</fmlx-nuget>
|
||||
</packageSourceCredentials>
|
||||
</configuration>
|
||||
120
README.md
120
README.md
|
|
@ -1,120 +0,0 @@
|
|||
# I Paid for 4 GB of RAM So I Will Use The Whole 4 GB 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**
|
||||
|
||||
If running via dotnet run, Download the `.nupkg` file [here](https://null.formulatrix.dev/reinardras/stitch_something/releases/tag/0.0.0) and save on `LocalNuget` before running. Otherwise it shouldn't be necessary.
|
||||
|
||||
If there's still a problem with library being missing, download the `.so` file and put it on `/usr/local/lib`.
|
||||
|
||||
|
||||
## Running
|
||||
|
||||
Either of these method works.
|
||||
|
||||
### Via dotnet run
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
### Via releases
|
||||
|
||||
Download [here](https://null.formulatrix.dev/reinardras/stitch_something/releases/).
|
||||
|
||||
Run with the same parameters:
|
||||
|
||||
```
|
||||
ASSET_PATH_RO=<path> ./StitchATon --profile deploy
|
||||
```
|
||||
|
||||
## 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.**
|
||||
|
||||
### Encoding and Serving 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.
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
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" );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<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.10.0.20241108" />
|
||||
<PackageReference Include="OpenCvSharp4_runtime.debian.12-arm64" Version="4.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StitchATon\StitchATon.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
|
||||
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
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
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");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
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] );
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
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();
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
{
|
||||
"$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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
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];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<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.10.0.20241108" />
|
||||
<PackageReference Include="OpenCvSharp4_runtime.debian.12-arm64" Version="4.10.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
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 );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"StitchATon.Controller": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"StitchATon.Controller": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
92
mise.toml
92
mise.toml
|
|
@ -1,92 +0,0 @@
|
|||
# Quick Guide
|
||||
# - install mise
|
||||
# - download the asset and extract them to ASSET_PATH_RO
|
||||
# - mise trust mise.toml
|
||||
# - mise run verify-asset
|
||||
# - require hashdeep
|
||||
# `hashdeep` package in debian
|
||||
# or `md5deep` package in fedora
|
||||
# or uncomment `tools."http:hashdeep"` below in windows
|
||||
# - mise run serve
|
||||
# - mise run arrange
|
||||
# - mise run action
|
||||
# - mise run assert
|
||||
# - mise run bench
|
||||
|
||||
[env]
|
||||
ASSET_PATH_RO = "{{ [xdg_cache_home, 'stitch-a-ton', 'asset'] | join_path }}"
|
||||
CONTEST_HOST = "http://localhost:7007"
|
||||
CONTEST_API = "/api/image/generate"
|
||||
CONTEST_OUTPUT = "{{ [cwd, '.contest'] | join_path }}"
|
||||
DOTNET_ENVIRONMENT = "Production"
|
||||
ANSWER_COMMIT_HASH = "89a07b40bf0414212c96945671a012035d375a25"
|
||||
|
||||
[tools]
|
||||
dotnet = "8"
|
||||
xh = "latest"
|
||||
uv = "latest"
|
||||
k6 = "latest"
|
||||
|
||||
# uncomment these if you're on windows
|
||||
#[tools."http:hashdeep"]
|
||||
#version = "4.4"
|
||||
|
||||
#[tools."http:hashdeep".platforms]
|
||||
#windows-x64 = {url = "https://github.com/jessek/hashdeep/releases/download/v4.4/md5deep-4.4.zip"}
|
||||
|
||||
[tasks.setup]
|
||||
run = '''
|
||||
{% if env.CONTEST_OUTPUT is not exists %}
|
||||
mkdir .contest
|
||||
{% endif %}
|
||||
'''
|
||||
|
||||
[tasks.verify-asset]
|
||||
dir = "{{ env.ASSET_PATH_RO }}"
|
||||
run = '''
|
||||
xh get https://null.formulatrix.dev/Contest/stitch-a-ton-answer/raw/commit/{{ env.ANSWER_COMMIT_HASH }}/asset.txt -o ../asset.txt
|
||||
hashdeep -arbvk ../asset.txt .
|
||||
'''
|
||||
|
||||
[tasks.arrange]
|
||||
depends = ['setup']
|
||||
dir = "{{ env.CONTEST_OUTPUT }}"
|
||||
outputs = ['answer.json', 'action.py', 'assert.py', 'bench.js', 'fuzzy.json']
|
||||
run = '''
|
||||
xh get https://null.formulatrix.dev/Contest/stitch-a-ton-answer/raw/commit/{{ env.ANSWER_COMMIT_HASH }}/answer.json -o answer.json
|
||||
xh get https://null.formulatrix.dev/Contest/stitch-a-ton-answer/raw/commit/{{ env.ANSWER_COMMIT_HASH }}/action.py -o action.py
|
||||
xh get https://null.formulatrix.dev/Contest/stitch-a-ton-answer/raw/commit/{{ env.ANSWER_COMMIT_HASH }}/assert.py -o assert.py
|
||||
xh get https://null.formulatrix.dev/Contest/stitch-a-ton-answer/raw/commit/{{ env.ANSWER_COMMIT_HASH }}/bench.js -o bench.js
|
||||
xh get https://null.formulatrix.dev/Contest/stitch-a-ton-answer/raw/commit/{{ env.ANSWER_COMMIT_HASH }}/fuzzy.json -o fuzzy.json
|
||||
'''
|
||||
|
||||
[tasks.serve]
|
||||
run = "dotnet run -c Release --no-launch-profile --urls {{env.CONTEST_HOST}} --project StitchATon"
|
||||
|
||||
[tasks.quick]
|
||||
depends = ['arrange']
|
||||
dir = "{{ env.CONTEST_OUTPUT }}"
|
||||
run = '''
|
||||
xh post {{env.CONTEST_HOST}}{{env.CONTEST_ENDPOINT}} canvas_rect=A1:H12 crop_offset:=[0,0] crop_size:=[1,1] output_scale:=0.25 -o quick.png
|
||||
'''
|
||||
|
||||
[tasks.action]
|
||||
depends = ['arrange']
|
||||
dir = "{{ env.CONTEST_OUTPUT }}"
|
||||
run = '''
|
||||
uv run --no-config --script {{ [env.CONTEST_OUTPUT, 'action.py'] | join_path }}
|
||||
'''
|
||||
|
||||
[tasks.assert]
|
||||
depends = ['arrange']
|
||||
dir = "{{ env.CONTEST_OUTPUT }}"
|
||||
run = '''
|
||||
uvx --no-config --with-requirements assert.py pytest assert.py
|
||||
'''
|
||||
|
||||
[tasks.bench]
|
||||
depends = ['arrange']
|
||||
dir = "{{ env.CONTEST_OUTPUT }}"
|
||||
run = '''
|
||||
k6 run -e TARGET_URL="{{ env.CONTEST_HOST }}{{ env.CONTEST_API }}" bench.js
|
||||
'''
|
||||
Loading…
Add table
Add a link
Reference in a new issue