normalized the crops and optimized

This commit is contained in:
mbsbahru 2025-07-31 12:57:14 +07:00
parent ddb324bbd4
commit 661c4b955c
4 changed files with 67 additions and 50 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View file

@ -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);
int idx = 0; stitched.Mutate(context =>
for (int r = 0; r < rows; r++)
{ {
for (int c = 0; c < cols; c++, idx++) int idx = 0;
for (int row = 0; row < rows; row++)
{ {
stitched.Mutate(ctx => ctx.DrawImage(bitmaps[idx], new Point(c * TileSize, r * TileSize), 1f)); for (int col = 0; col < cols; col++, idx++)
{
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,32 +140,33 @@ 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)
{
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++)
{ {
if (arr is null || arr.Length < len) var v = arr[i];
throw new ArgumentException($"{name} must have length {len}"); if (v < 0 || v > upper)
double upper = inclusiveUpper ? 1.0 : 1.0 - double.Epsilon; throw new ArgumentOutOfRangeException($"{name}[{i}]= {v} outside [0,{upper}]");
for (int i = 0; i < len; i++)
{
var v = arr[i];
if (v < 0 || v > upper)
throw new ArgumentOutOfRangeException($"{name}[{i}]= {v} outside [0,{upper}]");
}
}
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();
} }
} }
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();
}
} }

View file

@ -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)

View file

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