release
This commit is contained in:
parent
49b6f3810d
commit
52e830b459
23 changed files with 923 additions and 171 deletions
189
LiloStitcher.cs
189
LiloStitcher.cs
|
|
@ -1,136 +1,139 @@
|
|||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NetVips;
|
||||
|
||||
namespace lilo_stitcher_console;
|
||||
namespace LiloStitcher;
|
||||
|
||||
public record GenerateRequest(
|
||||
string CanvasRect,
|
||||
double[] CropOffset,
|
||||
double[] CropSize,
|
||||
double OutputScale
|
||||
public sealed record GenerateRequest(
|
||||
string Canvas_Rect,
|
||||
double[] Crop_Offset,
|
||||
double[] Crop_Size,
|
||||
double Output_Scale
|
||||
);
|
||||
|
||||
public readonly record struct PlateCoordinate(int Row, int Col)
|
||||
public readonly record struct PlateCoordinate( int Row, int Col )
|
||||
{
|
||||
public static PlateCoordinate Parse(string token)
|
||||
{
|
||||
var rowPart = new string(token.TakeWhile(char.IsLetter).ToArray()).ToUpperInvariant();
|
||||
var colPart = new string(token.SkipWhile(char.IsLetter).ToArray());
|
||||
|
||||
int row = 0;
|
||||
foreach (var character in rowPart)
|
||||
row = row * 26 + (character - 'A' + 1);
|
||||
|
||||
int.TryParse(colPart, out int col);
|
||||
return new PlateCoordinate(row, col);
|
||||
}
|
||||
}
|
||||
|
||||
public class TileCache(IMemoryCache cache)
|
||||
{
|
||||
private const long TileBytes = 720L * 720 * 3;
|
||||
|
||||
public Image? Get(string key) => cache.TryGetValue(key, out Image? img) ? img : null;
|
||||
|
||||
public void Set(string key, Image img) =>
|
||||
cache.Set(key, img, new MemoryCacheEntryOptions
|
||||
public static PlateCoordinate Parse( string token )
|
||||
{
|
||||
Size = TileBytes,
|
||||
SlidingExpiration = TimeSpan.FromMinutes(20)
|
||||
});
|
||||
if( string.IsNullOrWhiteSpace( token ) )
|
||||
throw new ArgumentException( "Empty coordinate." );
|
||||
|
||||
var rowPart = new string( token.TakeWhile( char.IsLetter ).ToArray() ).ToUpperInvariant();
|
||||
var colPart = new string( token.SkipWhile( char.IsLetter ).ToArray() );
|
||||
|
||||
int row = 0;
|
||||
foreach( char c in rowPart )
|
||||
row = row * 26 + ( c - 'A' + 1 );
|
||||
|
||||
return new PlateCoordinate( row, int.Parse( colPart ) );
|
||||
}
|
||||
}
|
||||
|
||||
public class TileLoader(TileCache cache, string assetDir)
|
||||
{
|
||||
public async Task<Image> LoadAsync(string name, CancellationToken ct)
|
||||
{
|
||||
if (cache.Get(name) is { } hit)
|
||||
return hit;
|
||||
|
||||
var image = await Task.Run(() =>
|
||||
Image.NewFromFile(Path.Combine(assetDir, $"{name}.png"), access: Enums.Access.Sequential), ct);
|
||||
|
||||
cache.Set(name, image);
|
||||
return image;
|
||||
}
|
||||
public class TileCache( IMemoryCache cache )
|
||||
{
|
||||
private const long TileBytes = 720L * 720 * 3;
|
||||
|
||||
public Image? Get( string key ) => cache.TryGetValue( key, out Image? img ) ? img : null;
|
||||
|
||||
public void Set( string key, Image img ) =>
|
||||
cache.Set( key, img, new MemoryCacheEntryOptions
|
||||
{
|
||||
Size = TileBytes,
|
||||
SlidingExpiration = TimeSpan.FromMinutes( 20 )
|
||||
} );
|
||||
}
|
||||
|
||||
public class LiloStitcher(TileLoader loader)
|
||||
public class TileLoader( TileCache cache, string assetDir )
|
||||
{
|
||||
public async Task<string> CreateImageAsync(GenerateRequest req, CancellationToken ct)
|
||||
public async Task<Image> LoadAsync( string name, CancellationToken ct )
|
||||
{
|
||||
(int rowMin, int colMin, int rows, int cols) = ParseCanvas(req.CanvasRect);
|
||||
if( cache.Get( name ) is { } hit )
|
||||
return hit;
|
||||
|
||||
Validate(req.CropOffset, nameof(req.CropOffset));
|
||||
Validate(req.CropSize, nameof(req.CropSize));
|
||||
var image = await Task.Run( () =>
|
||||
Image.NewFromFile( Path.Combine( assetDir, $"{name}.png" ), access: Enums.Access.Sequential ), ct );
|
||||
|
||||
double scale = req.OutputScale;
|
||||
if (scale <= 0 || scale > 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(req.OutputScale));
|
||||
cache.Set( name, image );
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
var tiles = new List<Image>(rows * cols);
|
||||
public class LiloStitcher( TileLoader loader )
|
||||
{
|
||||
public async Task<byte[]> CreateImageAsync( GenerateRequest req, CancellationToken ct )
|
||||
{
|
||||
(int rowMin, int colMin, int rows, int cols) = ParseCanvas( req.Canvas_Rect );
|
||||
|
||||
double scale = req.Output_Scale;
|
||||
if( scale <= 0 || scale > 1 )
|
||||
throw new ArgumentOutOfRangeException( nameof( req.Output_Scale ) );
|
||||
|
||||
var tiles = new List<Image>( rows * cols );
|
||||
for( int row = 0; row < rows; row++ )
|
||||
{
|
||||
for( int col = 0; col < cols; col++ )
|
||||
{
|
||||
string id = $"{RowName( rowMin + row )}{colMin + col}";
|
||||
var tile = await loader.LoadAsync( id, ct );
|
||||
if( scale < 1 ) tile = tile.Resize( scale );
|
||||
tiles.Add( tile );
|
||||
}
|
||||
for( int col = 0; col < cols; col++ )
|
||||
{
|
||||
string id = $"{RowName( rowMin + row )}{colMin + col}";
|
||||
var tile = await loader.LoadAsync( id, ct );
|
||||
if( scale < 1 ) tile = tile.Resize( scale );
|
||||
tiles.Add( tile );
|
||||
}
|
||||
}
|
||||
|
||||
var mosaic = Image.Arrayjoin(tiles.ToArray(), across: cols);
|
||||
var mosaic = Image.Arrayjoin( tiles.ToArray(), across: cols );
|
||||
|
||||
int offsetX = (int)Math.Truncate(req.CropOffset[0] * mosaic.Width);
|
||||
int offsetY = (int)Math.Truncate(req.CropOffset[1] * mosaic.Height);
|
||||
int offsetX = (int)Math.Truncate( req.Output_Scale * mosaic.Width );
|
||||
int offsetY = (int)Math.Truncate( req.Output_Scale * mosaic.Height );
|
||||
|
||||
int restWidth = mosaic.Width - offsetX;
|
||||
int restHeight = mosaic.Height - offsetY;
|
||||
|
||||
int cropWidth = Math.Max(1, (int)Math.Truncate(req.CropSize[0] * restWidth));
|
||||
int cropHeight = Math.Max(1, (int)Math.Truncate(req.CropSize[1] * restHeight));
|
||||
int cropWidth = Math.Max( 1, (int)Math.Truncate( req.Crop_Size[0] * restWidth ) );
|
||||
int cropHeight = Math.Max( 1, (int)Math.Truncate( req.Crop_Size[1] * restHeight ) );
|
||||
|
||||
int cropX = (int)Math.Truncate(offsetX / 2.0 + (restWidth - cropWidth) / 2.0);
|
||||
int cropY = (int)Math.Truncate(offsetY / 2.0 + (restHeight - cropHeight) / 2.0);
|
||||
int cropX = (int)Math.Truncate( offsetX / 2.0 + ( restWidth - cropWidth ) / 2.0 );
|
||||
int cropY = (int)Math.Truncate( offsetY / 2.0 + ( restHeight - cropHeight ) / 2.0 );
|
||||
|
||||
var cropRect = mosaic.Crop(cropX, cropY, cropWidth, cropHeight);
|
||||
var cropped = mosaic.Crop( cropX, cropY, cropWidth, cropHeight );
|
||||
|
||||
string tmpPath = Path.Combine(Path.GetTempPath(), $"mosaic-{Guid.NewGuid():N}.png");
|
||||
cropRect.WriteToFile(tmpPath);
|
||||
var finalImg = scale < 1.0 ? cropped.Resize( scale ) : cropped;
|
||||
|
||||
return tmpPath;
|
||||
string tmp = Path.GetTempFileName() + ".png";
|
||||
finalImg.WriteToFile( tmp );
|
||||
byte[] result = await File.ReadAllBytesAsync( tmp, ct );
|
||||
File.Delete( tmp );
|
||||
return result;
|
||||
}
|
||||
|
||||
private static (int rowMin, int colMin, int rows, int cols) ParseCanvas(string rect)
|
||||
private static (int rowMin, int colMin, int rows, int cols) ParseCanvas( string rect )
|
||||
{
|
||||
var parts = rect.ToUpperInvariant().Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||||
var part1 = PlateCoordinate.Parse(parts[0]);
|
||||
var part2 = PlateCoordinate.Parse(parts[1]);
|
||||
int rowMin = Math.Min(part1.Row, part2.Row);
|
||||
int rowMax = Math.Max(part1.Row, part2.Row);
|
||||
int colMin = Math.Min(part1.Col, part2.Col);
|
||||
int colMax = Math.Max(part1.Col, part2.Col);
|
||||
var parts = rect.ToUpperInvariant().Split( ':', StringSplitOptions.RemoveEmptyEntries );
|
||||
var part1 = PlateCoordinate.Parse( parts[0] );
|
||||
var part2 = PlateCoordinate.Parse( parts[1] );
|
||||
int rowMin = Math.Min( part1.Row, part2.Row );
|
||||
int rowMax = Math.Max( part1.Row, part2.Row );
|
||||
int colMin = Math.Min( part1.Col, part2.Col );
|
||||
int colMax = Math.Max( part1.Col, part2.Col );
|
||||
return (rowMin, colMin, rowMax - rowMin + 1, colMax - colMin + 1);
|
||||
}
|
||||
|
||||
private static void Validate(double[] arr, string name)
|
||||
private static void Validate( double[] arr, string name )
|
||||
{
|
||||
if (arr is null || arr.Length < 2)
|
||||
throw new ArgumentException($"{name} length");
|
||||
if (arr.Any(x => x < 0 || x > 1))
|
||||
throw new ArgumentOutOfRangeException(name);
|
||||
if( arr is null || arr.Length < 2 )
|
||||
throw new ArgumentException( $"{name} length" );
|
||||
if( arr.Any( x => x < 0 || x > 1 ) )
|
||||
throw new ArgumentOutOfRangeException( name );
|
||||
}
|
||||
|
||||
static string RowName(int row)
|
||||
static string RowName( int row )
|
||||
{
|
||||
var stringBuilder = new System.Text.StringBuilder();
|
||||
while (row > 0)
|
||||
{
|
||||
row--;
|
||||
stringBuilder.Insert(0, (char)('A' + row % 26));
|
||||
row /= 26;
|
||||
}
|
||||
return stringBuilder.ToString();
|
||||
var stringBuilder = new System.Text.StringBuilder();
|
||||
while( row > 0 )
|
||||
{
|
||||
row--;
|
||||
stringBuilder.Insert( 0, (char)( 'A' + row % 26 ) );
|
||||
row /= 26;
|
||||
}
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue