refactor into using ImageSharp
This commit is contained in:
parent
efaaee3b17
commit
db3c833c4c
5 changed files with 125 additions and 111 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +1,4 @@
|
||||||
bin
|
bin
|
||||||
obj
|
obj
|
||||||
|
.vs
|
||||||
|
*.png
|
||||||
115
LiloStitcher.cs
115
LiloStitcher.cs
|
|
@ -1,5 +1,8 @@
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using SkiaSharp;
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using SixLabors.ImageSharp.Formats.Png;
|
||||||
|
|
||||||
namespace LiloStitcher;
|
namespace LiloStitcher;
|
||||||
|
|
||||||
|
|
@ -10,7 +13,7 @@ public record GenerateRequest(
|
||||||
double OutputScale
|
double OutputScale
|
||||||
);
|
);
|
||||||
|
|
||||||
public record struct PlateCoordinate( int Row, int Col )
|
public readonly record struct PlateCoordinate(int Row, int Col)
|
||||||
{
|
{
|
||||||
public static PlateCoordinate Parse(string token)
|
public static PlateCoordinate Parse(string token)
|
||||||
{
|
{
|
||||||
|
|
@ -19,24 +22,21 @@ public record struct PlateCoordinate( int Row, int Col )
|
||||||
|
|
||||||
int row = 0;
|
int row = 0;
|
||||||
foreach (var c in rowPart)
|
foreach (var c in rowPart)
|
||||||
{
|
|
||||||
row = row * 26 + (c - 'A' + 1);
|
row = row * 26 + (c - 'A' + 1);
|
||||||
}
|
|
||||||
|
|
||||||
int.TryParse(colPart, out int col);
|
int.TryParse(colPart, out int col);
|
||||||
|
|
||||||
return new PlateCoordinate(row, col);
|
return new PlateCoordinate(row, col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TileCache(IMemoryCache cache)
|
public class TileCache(IMemoryCache cache)
|
||||||
{
|
{
|
||||||
private const long TileBytes = 720L * 720 * 4;
|
private const long TileBytes = 720L * 720 * 3;
|
||||||
|
|
||||||
public SKBitmap? Get( string key ) => cache.TryGetValue( key, out SKBitmap? bmp ) ? bmp : null;
|
public Image<Rgba32>? Get(string key) => cache.TryGetValue(key, out Image<Rgba32>? img) ? img : null;
|
||||||
|
|
||||||
public void Set( string key, SKBitmap bmp ) =>
|
public void Set(string key, Image<Rgba32> img) =>
|
||||||
cache.Set( key, bmp, new MemoryCacheEntryOptions
|
cache.Set(key, img, new MemoryCacheEntryOptions
|
||||||
{
|
{
|
||||||
Size = TileBytes,
|
Size = TileBytes,
|
||||||
SlidingExpiration = TimeSpan.FromMinutes(20)
|
SlidingExpiration = TimeSpan.FromMinutes(20)
|
||||||
|
|
@ -45,18 +45,16 @@ public class TileCache( IMemoryCache cache )
|
||||||
|
|
||||||
public class TileLoader(TileCache cache, string assetDir)
|
public class TileLoader(TileCache cache, string assetDir)
|
||||||
{
|
{
|
||||||
public async Task<SKBitmap> LoadAsync( string name, CancellationToken ct )
|
public async Task<Image<Rgba32>> LoadAsync(string name, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if( cache.Get( name ) is { } hit ) return hit;
|
if (cache.Get(name) is { } hit)
|
||||||
|
return hit;
|
||||||
|
|
||||||
var path = Path.Combine(assetDir, $"{name}.png");
|
var path = Path.Combine(assetDir, $"{name}.png");
|
||||||
|
await using var fs = File.OpenRead(path);
|
||||||
var bytes = await File.ReadAllBytesAsync( path, ct ).ConfigureAwait( false );
|
var image = await Image.LoadAsync<Rgba32>(fs, ct).ConfigureAwait(false);
|
||||||
|
cache.Set(name, image);
|
||||||
using var data = SKData.CreateCopy( bytes );
|
return image;
|
||||||
var bmp = SKBitmap.Decode( data );
|
|
||||||
cache.Set( name, bmp );
|
|
||||||
return bmp;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,8 +64,8 @@ public class LiloStitcher( TileLoader loader )
|
||||||
|
|
||||||
public async Task<byte[]> CreateImageAsync(GenerateRequest req, CancellationToken ct)
|
public async Task<byte[]> CreateImageAsync(GenerateRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var parts = req.CanvasRect.ToUpperInvariant().Split( ':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries );
|
var parts = req.CanvasRect.ToUpperInvariant()
|
||||||
|
.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||||
var part1 = PlateCoordinate.Parse(parts[0]);
|
var part1 = PlateCoordinate.Parse(parts[0]);
|
||||||
var part2 = PlateCoordinate.Parse(parts[1]);
|
var part2 = PlateCoordinate.Parse(parts[1]);
|
||||||
|
|
||||||
|
|
@ -84,52 +82,61 @@ public class LiloStitcher( TileLoader loader )
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var bitmaps = await Task.WhenAll(names.Select(n => loader.LoadAsync(n, ct))).ConfigureAwait(false);
|
var bitmaps = await Task.WhenAll(names.Select(n => loader.LoadAsync(n, ct))).ConfigureAwait(false);
|
||||||
|
var stitched = new Image<Rgba32>(cols * TileSize, rows * TileSize);
|
||||||
var fullInfo = new SKImageInfo( cols * TileSize, rows * TileSize );
|
|
||||||
using var surface = SKSurface.Create( fullInfo );
|
|
||||||
var canvas = surface.Canvas;
|
|
||||||
|
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
for( int row = 0; row < rows; row++ )
|
for (int r = 0; r < rows; r++)
|
||||||
for( int column = 0; column < cols; column++, idx++ )
|
{
|
||||||
canvas.DrawBitmap( bitmaps[idx], column * TileSize, row * TileSize );
|
for (int c = 0; c < cols; c++, idx++)
|
||||||
|
{
|
||||||
|
stitched.Mutate(ctx => ctx.DrawImage(bitmaps[idx], new Point(c * TileSize, r * TileSize), 1f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
using var full = new SKBitmap( fullInfo );
|
Validate(req.CropOffset, 2, nameof(req.CropOffset), inclusiveUpper: true);
|
||||||
surface.ReadPixels( fullInfo, full.GetPixels(), full.RowBytes, 0, 0 );
|
Validate(req.CropSize, 2, nameof(req.CropSize), inclusiveUpper: true);
|
||||||
|
|
||||||
ValidateFraction( req.CropOffset, 2, nameof( req.CropOffset ), inclusiveUpper: true );
|
int offsetX = (int)(req.CropOffset[0] * stitched.Width);
|
||||||
ValidateFraction( req.CropSize, 2, nameof( req.CropSize ), inclusiveUpper: true );
|
int offsetY = (int)(req.CropOffset[1] * stitched.Height);
|
||||||
|
int cropWidth = (int)(req.CropSize[0] * stitched.Width);
|
||||||
|
int cropHeight = (int)(req.CropSize[1] * stitched.Height);
|
||||||
|
|
||||||
int offsetX = (int)( req.CropOffset[0] * fullInfo.Width );
|
if (offsetX + cropWidth > stitched.Width) cropWidth = stitched.Width - offsetX;
|
||||||
int offsetY = (int)( req.CropOffset[1] * fullInfo.Height );
|
if (offsetY + cropHeight > stitched.Height) cropHeight = stitched.Height - offsetY;
|
||||||
int cropWidth = (int)( req.CropSize[0] * fullInfo.Width );
|
|
||||||
int cropHeight = (int)( req.CropSize[1] * fullInfo.Height );
|
|
||||||
|
|
||||||
if( offsetX + cropWidth > fullInfo.Width ) cropWidth = fullInfo.Width - offsetX;
|
var cropRect = new Rectangle(offsetX, offsetY, cropWidth, cropHeight);
|
||||||
if( offsetY + cropHeight > fullInfo.Height ) cropHeight = fullInfo.Height - offsetY;
|
|
||||||
|
|
||||||
var cropRect = new SKRectI( offsetX, offsetY, offsetX + cropWidth, offsetY + cropHeight );
|
using var cropped = stitched.Clone(ctx => ctx.Crop(cropRect));
|
||||||
using var crop = new SKBitmap( cropWidth, cropHeight );
|
|
||||||
full.ExtractSubset( crop, cropRect );
|
|
||||||
|
|
||||||
double scale = Math.Clamp(req.OutputScale, 0.0, 1.0);
|
double scale = Math.Clamp(req.OutputScale, 0.0, 1.0);
|
||||||
int truncatedWidth = Math.Max( 1, (int)Math.Round( cropWidth * scale ) );
|
Image<Rgba32> output;
|
||||||
int truncatedHeight = Math.Max( 1, (int)Math.Round( cropHeight * scale ) );
|
if (scale < 1.0)
|
||||||
|
|
||||||
using var finalBmp = scale < 1.0
|
|
||||||
? crop.Resize( new SKSizeI( truncatedWidth, truncatedHeight ), new SKSamplingOptions( SKFilterMode.Linear ) )
|
|
||||||
: crop;
|
|
||||||
|
|
||||||
using var image = SKImage.FromBitmap( finalBmp );
|
|
||||||
using var data = image.Encode( SKEncodedImageFormat.Png, 100 );
|
|
||||||
return data.ToArray();
|
|
||||||
|
|
||||||
static void ValidateFraction( double[] arr, int len, string name, bool inclusiveUpper )
|
|
||||||
{
|
{
|
||||||
|
int width = Math.Max(1, (int)Math.Truncate(cropWidth * scale));
|
||||||
|
int height = Math.Max(1, (int)Math.Truncate(cropHeight * scale));
|
||||||
|
output = cropped.Clone(ctx => ctx.Resize(width, height));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output = cropped.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var memStream = new MemoryStream();
|
||||||
|
await output.SaveAsync(memStream, new PngEncoder
|
||||||
|
{
|
||||||
|
}, ct).ConfigureAwait(false);
|
||||||
|
return memStream.ToArray();
|
||||||
|
|
||||||
|
static void Validate(double[] arr, int len, string name, bool inclusiveUpper)
|
||||||
|
{
|
||||||
|
if (arr is null || arr.Length < len)
|
||||||
|
throw new ArgumentException($"{name} must have length {len}");
|
||||||
|
double upper = inclusiveUpper ? 1.0 : 1.0 - double.Epsilon;
|
||||||
for (int i = 0; i < len; i++)
|
for (int i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
double v = arr[i];
|
var v = arr[i];
|
||||||
double upper = inclusiveUpper ? 1.0 : 1.0 - double.Epsilon;
|
if (v < 0 || v > upper)
|
||||||
|
throw new ArgumentOutOfRangeException($"{name}[{i}]= {v} outside [0,{upper}]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
28
Program.cs
28
Program.cs
|
|
@ -1,4 +1,4 @@
|
||||||
|
using System.Diagnostics;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
namespace LiloStitcher;
|
namespace LiloStitcher;
|
||||||
|
|
@ -8,14 +8,18 @@ public static class Program
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var opt = new Options();
|
Stopwatch sw = Stopwatch.StartNew();
|
||||||
opt.CanvasRect = "A1:H12";
|
var begin = sw.ElapsedMilliseconds;
|
||||||
opt.CropOffset = [0.25, 0.25];
|
var opt = new Options
|
||||||
opt.CropSize = [0.75, 0.75];
|
{
|
||||||
opt.OutputScale = 0.5;
|
CanvasRect = "A1:AE55",
|
||||||
opt.OutputPath = "stitched.png";
|
CropOffset = [0.0, 0.0],
|
||||||
|
CropSize = [1.0, 1.0],
|
||||||
|
OutputScale = 1.0,
|
||||||
|
OutputPath = "stitched.png"
|
||||||
|
};
|
||||||
|
|
||||||
string tileFilePath = "../stitch-a-ton/tiles1705";
|
string tileFilePath = "../tiles1705"; // should later be directed to the read-only `ASSET_PATH_RO` environment variable
|
||||||
string assetDir = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), tileFilePath));
|
string assetDir = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), tileFilePath));
|
||||||
|
|
||||||
using var memCache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 256L * 1024 * 1024 });
|
using var memCache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 256L * 1024 * 1024 });
|
||||||
|
|
@ -31,10 +35,12 @@ public static class Program
|
||||||
);
|
);
|
||||||
|
|
||||||
Console.WriteLine("Stitching...");
|
Console.WriteLine("Stitching...");
|
||||||
var png = await stitcher.CreateImageAsync( req, CancellationToken.None );
|
var pngBytes = await stitcher.CreateImageAsync(req, CancellationToken.None);
|
||||||
|
|
||||||
File.WriteAllBytes( opt.OutputPath!, png );
|
File.WriteAllBytes(opt.OutputPath!, pngBytes);
|
||||||
Console.WriteLine( $"Done. Wrote {opt.OutputPath} ({png.Length / 1024.0:F1} KB)" );
|
Console.WriteLine($"Done. Wrote {opt.OutputPath} ({pngBytes.Length / 1024.0:F1} KB)");
|
||||||
|
|
||||||
|
Console.WriteLine(begin - sw.ElapsedMilliseconds);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
|
||||||
<PackageReference Include="SkiaSharp" Version="3.119.0" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue