First Commit
This commit is contained in:
parent
bb40883c7d
commit
696158848f
18 changed files with 787 additions and 0 deletions
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