normalized the crops and optimized
This commit is contained in:
parent
ddb324bbd4
commit
661c4b955c
4 changed files with 67 additions and 50 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
|
|
@ -21,8 +21,8 @@ public readonly record struct PlateCoordinate(int Row, int Col)
|
||||||
var colPart = new string(token.SkipWhile(char.IsLetter).ToArray());
|
var colPart = new string(token.SkipWhile(char.IsLetter).ToArray());
|
||||||
|
|
||||||
int row = 0;
|
int row = 0;
|
||||||
foreach (var c in rowPart)
|
foreach (var character in rowPart)
|
||||||
row = row * 26 + (c - 'A' + 1);
|
row = row * 26 + (character - 'A' + 1);
|
||||||
|
|
||||||
int.TryParse(colPart, out int col);
|
int.TryParse(colPart, out int col);
|
||||||
return new PlateCoordinate(row, col);
|
return new PlateCoordinate(row, col);
|
||||||
|
|
@ -33,9 +33,9 @@ public class TileCache(IMemoryCache cache)
|
||||||
{
|
{
|
||||||
private const long TileBytes = 720L * 720 * 3;
|
private const long TileBytes = 720L * 720 * 3;
|
||||||
|
|
||||||
public Image<Rgba32>? Get(string key) => cache.TryGetValue(key, out Image<Rgba32>? img) ? img : null;
|
public Image<Rgb24>? Get(string key) => cache.TryGetValue(key, out Image<Rgb24>? img) ? img : null;
|
||||||
|
|
||||||
public void Set(string key, Image<Rgba32> img) =>
|
public void Set(string key, Image<Rgb24> img) =>
|
||||||
cache.Set(key, img, new MemoryCacheEntryOptions
|
cache.Set(key, img, new MemoryCacheEntryOptions
|
||||||
{
|
{
|
||||||
Size = TileBytes,
|
Size = TileBytes,
|
||||||
|
|
@ -45,14 +45,14 @@ public class TileCache(IMemoryCache cache)
|
||||||
|
|
||||||
public class TileLoader(TileCache cache, string assetDir)
|
public class TileLoader(TileCache cache, string assetDir)
|
||||||
{
|
{
|
||||||
public async Task<Image<Rgba32>> LoadAsync(string name, CancellationToken ct)
|
public async Task<Image<Rgb24>> LoadAsync(string name, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (cache.Get(name) is { } hit)
|
if (cache.Get(name) is { } hit)
|
||||||
return hit;
|
return hit;
|
||||||
|
|
||||||
var path = Path.Combine(assetDir, $"{name}.png");
|
var path = Path.Combine(assetDir, $"{name}.png");
|
||||||
await using var fs = File.OpenRead(path);
|
await using var fs = File.OpenRead(path);
|
||||||
var image = await Image.LoadAsync<Rgba32>(fs, ct).ConfigureAwait(false);
|
var image = await Image.LoadAsync<Rgb24>(fs, ct).ConfigureAwait(false);
|
||||||
cache.Set(name, image);
|
cache.Set(name, image);
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
@ -62,10 +62,17 @@ public class LiloStitcher(TileLoader loader)
|
||||||
{
|
{
|
||||||
private const int TileSize = 720;
|
private const int TileSize = 720;
|
||||||
|
|
||||||
|
private static readonly GraphicsOptions _copy = new()
|
||||||
|
{
|
||||||
|
Antialias = false,
|
||||||
|
AlphaCompositionMode = PixelAlphaCompositionMode.Src,
|
||||||
|
BlendPercentage = 1f
|
||||||
|
};
|
||||||
|
|
||||||
public async Task<byte[]> CreateImageAsync(GenerateRequest req, CancellationToken ct)
|
public async Task<byte[]> CreateImageAsync(GenerateRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var parts = req.CanvasRect.ToUpperInvariant()
|
var parts = req.CanvasRect.ToUpperInvariant().Split(':', StringSplitOptions.TrimEntries |
|
||||||
.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
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]);
|
||||||
|
|
||||||
|
|
@ -82,34 +89,43 @@ 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 stitched = new Image<Rgb24>(cols * TileSize, rows * TileSize);
|
||||||
|
|
||||||
|
stitched.Mutate(context =>
|
||||||
|
{
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
for (int r = 0; r < rows; r++)
|
for (int row = 0; row < rows; row++)
|
||||||
{
|
{
|
||||||
for (int c = 0; c < cols; c++, idx++)
|
for (int col = 0; col < cols; col++, idx++)
|
||||||
{
|
{
|
||||||
stitched.Mutate(ctx => ctx.DrawImage(bitmaps[idx], new Point(c * TileSize, r * TileSize), 1f));
|
context.DrawImage(bitmaps[idx], new Point(col * TileSize, row * TileSize), 1f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Validate(req.CropOffset, 2, nameof(req.CropOffset), inclusiveUpper: true);
|
Validate(req.CropOffset, 2, nameof(req.CropOffset), inclusiveUpper: true);
|
||||||
Validate(req.CropSize, 2, nameof(req.CropSize), inclusiveUpper: true);
|
Validate(req.CropSize, 2, nameof(req.CropSize), inclusiveUpper: true);
|
||||||
|
|
||||||
int offsetX = (int)(req.CropOffset[0] * stitched.Width);
|
int offsetX = (int)Math.Truncate(req.CropOffset[0] * stitched.Width);
|
||||||
int offsetY = (int)(req.CropOffset[1] * stitched.Height);
|
int offsetY = (int)Math.Truncate(req.CropOffset[1] * stitched.Height);
|
||||||
int cropWidth = (int)(req.CropSize[0] * stitched.Width);
|
|
||||||
int cropHeight = (int)(req.CropSize[1] * stitched.Height);
|
|
||||||
|
|
||||||
if (offsetX + cropWidth > stitched.Width) cropWidth = stitched.Width - offsetX;
|
int restWidth = stitched.Width - offsetX;
|
||||||
if (offsetY + cropHeight > stitched.Height) cropHeight = stitched.Height - offsetY;
|
int restHeight = stitched.Height - offsetY;
|
||||||
|
|
||||||
var cropRect = new Rectangle(offsetX, offsetY, cropWidth, cropHeight);
|
int cropWidth = Math.Max(1, (int)Math.Truncate(req.CropSize[0] * restWidth ));
|
||||||
|
int cropHeight = Math.Max(1, (int)Math.Truncate(req.CropSize[1] * restHeight));
|
||||||
|
|
||||||
using var cropped = stitched.Clone(ctx => ctx.Crop(cropRect));
|
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 = new Rectangle(cropX, cropY, cropWidth, cropHeight);
|
||||||
|
using var cropped = stitched.Clone(context => context.Crop(cropRect));
|
||||||
|
|
||||||
double scale = Math.Clamp(req.OutputScale, 0.0, 1.0);
|
double scale = Math.Clamp(req.OutputScale, 0.0, 1.0);
|
||||||
Image<Rgba32> output;
|
if (scale <= 0 || scale > 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(req.OutputScale), "OutputScale must be > 0 and ≤ 1.0");
|
||||||
|
|
||||||
|
Image<Rgb24> output;
|
||||||
if (scale < 1.0)
|
if (scale < 1.0)
|
||||||
{
|
{
|
||||||
int width = Math.Max(1, (int)Math.Truncate(cropWidth * scale));
|
int width = Math.Max(1, (int)Math.Truncate(cropWidth * scale));
|
||||||
|
|
@ -124,8 +140,10 @@ public class LiloStitcher(TileLoader loader)
|
||||||
using var memStream = new MemoryStream();
|
using var memStream = new MemoryStream();
|
||||||
await output.SaveAsync(memStream, new PngEncoder
|
await output.SaveAsync(memStream, new PngEncoder
|
||||||
{
|
{
|
||||||
|
CompressionLevel = PngCompressionLevel.Level1
|
||||||
}, ct).ConfigureAwait(false);
|
}, ct).ConfigureAwait(false);
|
||||||
return memStream.ToArray();
|
return memStream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
static void Validate(double[] arr, int len, string name, bool inclusiveUpper)
|
static void Validate(double[] arr, int len, string name, bool inclusiveUpper)
|
||||||
{
|
{
|
||||||
|
|
@ -151,5 +169,4 @@ public class LiloStitcher(TileLoader loader)
|
||||||
}
|
}
|
||||||
return stringBuilder.ToString();
|
return stringBuilder.ToString();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -13,9 +13,9 @@ public static class Program
|
||||||
var opt = new Options
|
var opt = new Options
|
||||||
{
|
{
|
||||||
CanvasRect = "A1:AE55",
|
CanvasRect = "A1:AE55",
|
||||||
CropOffset = [0.0, 0.0],
|
CropOffset = [0.4, 0.4],
|
||||||
CropSize = [1.0, 1.0],
|
CropSize = [0.8, 0.8],
|
||||||
OutputScale = 1.0,
|
OutputScale = 0.5,
|
||||||
OutputPath = "stitched.png"
|
OutputPath = "stitched.png"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -40,7 +40,7 @@ public static class Program
|
||||||
File.WriteAllBytes(opt.OutputPath!, pngBytes);
|
File.WriteAllBytes(opt.OutputPath!, pngBytes);
|
||||||
Console.WriteLine($"Done. Wrote {opt.OutputPath} ({pngBytes.Length / 1024.0:F1} KB)");
|
Console.WriteLine($"Done. Wrote {opt.OutputPath} ({pngBytes.Length / 1024.0:F1} KB)");
|
||||||
|
|
||||||
Console.WriteLine(begin - sw.ElapsedMilliseconds);
|
Console.WriteLine(sw.ElapsedMilliseconds - begin);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<RootNamespace>lilo_stitcher_console</RootNamespace>
|
<RootNamespace>lilo_stitcher_console</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue