First Commit

This commit is contained in:
reinard.setiadji@formulatrix.com 2025-08-01 15:29:06 +07:00
parent bb40883c7d
commit 696158848f
18 changed files with 787 additions and 0 deletions

13
.idea/.idea.StitchATon/.idea/.gitignore generated vendored Normal file
View 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

View 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>

View 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
View 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
View 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
View 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" );
}
}

View 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
View 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
View 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");
}
}

View 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
View 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();

View 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/"
}
}
}
}

View 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];
}
}

View 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>

View 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;
}
}

View 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 );
}
}

View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"StitchATon.Controller": "Information"
}
}
}

View file

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"StitchATon.Controller": "Information"
}
},
"AllowedHosts": "*"
}