Initial commit
This commit is contained in:
commit
ef3b7d68fb
30 changed files with 1568 additions and 0 deletions
55
Domain/Configuration.cs
Normal file
55
Domain/Configuration.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
namespace StitchATon2.Domain;
|
||||
|
||||
public class Configuration
|
||||
{
|
||||
public required string AssetPath { get; init; }
|
||||
public required string CachePath { get; init; }
|
||||
|
||||
public required int Columns { get; init; }
|
||||
public required int Rows { get; init; }
|
||||
|
||||
public required int Width { get; init; }
|
||||
public required int Height { get; init; }
|
||||
|
||||
|
||||
public int FullWidth => Width * Columns;
|
||||
public int FullHeight => Height * Rows;
|
||||
|
||||
public int BottomTileIndex => Height - 1;
|
||||
public int RightTileIndex => Width - 1;
|
||||
|
||||
public int TileCount => Columns * Rows;
|
||||
|
||||
public required int ImageCacheCapacity { get; init; }
|
||||
public required int IntegralCacheCapacity { get; init; }
|
||||
|
||||
public static Configuration Default
|
||||
{
|
||||
get
|
||||
{
|
||||
var assetPath = Environment.GetEnvironmentVariable("ASSET_PATH_RO")!;
|
||||
var cachePath = Path.Combine(Path.GetTempPath(), "d42df2a2-60ac-4dc3-a6b9-d4c04f2e08e6");
|
||||
return new Configuration
|
||||
{
|
||||
AssetPath = assetPath,
|
||||
CachePath = cachePath,
|
||||
Columns = 55,
|
||||
Rows = 31,
|
||||
Width = 720,
|
||||
Height = 720,
|
||||
ImageCacheCapacity = 5,
|
||||
IntegralCacheCapacity = 10,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string GetAssetPath(string assetName)
|
||||
{
|
||||
return Path.Combine(AssetPath, assetName);
|
||||
}
|
||||
|
||||
public string GetCachePath(string assetName)
|
||||
{
|
||||
return Path.Combine(CachePath, assetName);
|
||||
}
|
||||
}
|
||||
51
Domain/GridSection.cs
Normal file
51
Domain/GridSection.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
namespace StitchATon2.Domain;
|
||||
|
||||
public class GridSection
|
||||
{
|
||||
public TileManager TileManager { get; }
|
||||
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
public int OffsetX { get; }
|
||||
public int OffsetY { get; }
|
||||
|
||||
public Tile Origin { get; }
|
||||
|
||||
public GridSection(
|
||||
TileManager tileManager,
|
||||
string coordinatePair,
|
||||
float cropX,
|
||||
float cropY,
|
||||
float cropWidth,
|
||||
float cropHeight)
|
||||
{
|
||||
TileManager = tileManager;
|
||||
var config = tileManager.Configuration;
|
||||
|
||||
var (tile0, tile1) = tileManager.GetTilePair(coordinatePair);
|
||||
|
||||
var (col0, col1) = tile0.Column < tile1.Column
|
||||
? (tile0.Column, tile1.Column)
|
||||
: (tile1.Column, tile0.Column);
|
||||
|
||||
var (row0, row1) = tile0.Row < tile1.Row
|
||||
? (tile0.Row, tile1.Row)
|
||||
: (tile1.Row, tile0.Row);
|
||||
|
||||
var gridWidth = (col1 - col0 + 1) * config.Width;
|
||||
var gridHeight = (row1 - row0 + 1) * config.Height;
|
||||
|
||||
var x0 = (int)(gridWidth * cropX);
|
||||
var y0 = (int)(gridHeight * cropY);
|
||||
var x1 = (int)(gridWidth * (cropWidth + cropX));
|
||||
var y1 = (int)(gridHeight * (cropHeight + cropY));
|
||||
|
||||
Width = x1 - x0;
|
||||
Height = y1 - y0;
|
||||
|
||||
(var columnOffset, OffsetX) = Math.DivRem(x0, config.Width);
|
||||
(var rowOffset, OffsetY) = Math.DivRem(y0, config.Height);
|
||||
|
||||
Origin = tileManager.GetTile(col0 + columnOffset, row0 + rowOffset);
|
||||
}
|
||||
}
|
||||
155
Domain/ImageCreators/ImageCreator.cs
Normal file
155
Domain/ImageCreators/ImageCreator.cs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using StitchATon2.Infra;
|
||||
using StitchATon2.Infra.Buffers;
|
||||
using StitchATon2.Infra.Encoders;
|
||||
|
||||
namespace StitchATon2.Domain.ImageCreators;
|
||||
|
||||
public class ImageCreator
|
||||
{
|
||||
private readonly GridSection _section;
|
||||
|
||||
private int FullWidth => _section.TileManager.Configuration.FullWidth;
|
||||
private int FullHeight => _section.TileManager.Configuration.FullHeight;
|
||||
|
||||
private int OffsetX => _section.OffsetX;
|
||||
private int OffsetY => _section.OffsetY;
|
||||
|
||||
private int Width => _section.Width;
|
||||
private int Height => _section.Height;
|
||||
|
||||
private int TileWidth => _section.TileManager.Configuration.Width;
|
||||
private int TileHeight => _section.TileManager.Configuration.Height;
|
||||
private Tile TileOrigin => _section.Origin;
|
||||
|
||||
private int RightmostPixelIndex => _section.TileManager.Configuration.RightTileIndex;
|
||||
private int BottomPixelIndex => _section.TileManager.Configuration.BottomTileIndex;
|
||||
|
||||
private TileManager TileManager => _section.TileManager;
|
||||
|
||||
private readonly Int32Pixel[] _mmfReadBuffer;
|
||||
|
||||
public ImageCreator(GridSection section)
|
||||
{
|
||||
_section = section;
|
||||
_mmfReadBuffer = ArrayPool<Int32Pixel>.Shared.Rent(TileWidth);
|
||||
}
|
||||
|
||||
public async Task WriteToStream(Stream writableStream, float scale)
|
||||
{
|
||||
var scaleFactor = MathF.ReciprocalEstimate(scale);
|
||||
var targetWidth = (int)(Width / scaleFactor);
|
||||
var targetHeight = (int)(Height / scaleFactor);
|
||||
|
||||
var encoder = new PngStreamEncoder(writableStream, targetWidth, targetHeight);
|
||||
await encoder.WriteHeader();
|
||||
|
||||
var outputBufferSize = targetWidth * Unsafe.SizeOf<Rgb24>();
|
||||
using var outputBuffer = MemoryAllocator.AllocateManaged<byte>(outputBufferSize);
|
||||
|
||||
using var xLookup = Utils.BoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX);
|
||||
using var yLookup = Utils.BoundsMatrix(scaleFactor, targetHeight, FullHeight, OffsetY);
|
||||
|
||||
using var yStartMap = MemoryAllocator.Allocate<Int32Pixel>(targetWidth);
|
||||
using var yEndMap = MemoryAllocator.Allocate<Int32Pixel>(targetWidth);
|
||||
|
||||
var yStart = OffsetY;
|
||||
Task? outputTask = null;
|
||||
for (var y = 0; y < targetHeight; y++)
|
||||
{
|
||||
var yEnd = yLookup[y];
|
||||
|
||||
var (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight);
|
||||
MapRow(localRow0, localOffsetY0, xLookup.Span[..targetWidth], yStartMap);
|
||||
|
||||
var (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight);
|
||||
MapRow(localRow1, localOffsetY1, xLookup.Span[..targetWidth], yEndMap);
|
||||
|
||||
if (localRow0 != localRow1)
|
||||
{
|
||||
MapRowAppend(localRow0, BottomPixelIndex, xLookup.Span[..targetWidth], yEndMap);
|
||||
}
|
||||
|
||||
if(outputTask != null)
|
||||
await outputTask;
|
||||
|
||||
int xStart = OffsetX, x0 = 0;
|
||||
for (int x1 = 0, i = 0; x1 < targetWidth; x1++)
|
||||
{
|
||||
var xEnd = xLookup[x1];
|
||||
|
||||
var pixel = yEndMap[x1];
|
||||
pixel += yStartMap[x0];
|
||||
pixel -= yEndMap[x0];
|
||||
pixel -= yStartMap[x1];
|
||||
|
||||
pixel /= Math.Max(1, (xEnd - xStart) * (yEnd - yStart));
|
||||
outputBuffer.Memory.Span[i++] = (byte)pixel.R;
|
||||
outputBuffer.Memory.Span[i++] = (byte)pixel.G;
|
||||
outputBuffer.Memory.Span[i++] = (byte)pixel.B;
|
||||
|
||||
xStart = xEnd;
|
||||
x0 = x1;
|
||||
}
|
||||
|
||||
outputTask = encoder.WriteData(outputBuffer.Memory[..outputBufferSize]);
|
||||
yStart = yEnd;
|
||||
}
|
||||
|
||||
await encoder.WriteEndOfFile();
|
||||
}
|
||||
|
||||
private void MapRow(int rowOffset, int yOffset, Span<int> sourceMap, IBuffer<Int32Pixel> destination)
|
||||
{
|
||||
var currentTile = TileManager.GetAdjacent(TileOrigin, 0, rowOffset);
|
||||
var xAdder = Int32Pixel.Zero;
|
||||
var xOffset = 0;
|
||||
var written = 0;
|
||||
while (true)
|
||||
{
|
||||
currentTile.Integral.Acquire(yOffset, _mmfReadBuffer);
|
||||
int localX;
|
||||
while (written < sourceMap.Length && (localX = sourceMap[written] - xOffset) < TileWidth)
|
||||
{
|
||||
destination.Span[written] = _mmfReadBuffer[localX];
|
||||
destination.Span[written] += xAdder;
|
||||
written++;
|
||||
}
|
||||
|
||||
if (written >= sourceMap.Length)
|
||||
break;
|
||||
|
||||
xAdder += _mmfReadBuffer[RightmostPixelIndex];
|
||||
xOffset += TileWidth;
|
||||
currentTile = TileManager.GetAdjacent(currentTile, 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void MapRowAppend(int rowOffset, int yOffset, Span<int> sourceMap, IBuffer<Int32Pixel> destination)
|
||||
{
|
||||
var currentTile = TileManager.GetAdjacent(TileOrigin, 0, rowOffset);
|
||||
var xAdder = Int32Pixel.Zero;
|
||||
var xOffset = 0;
|
||||
var written = 0;
|
||||
while (true)
|
||||
{
|
||||
currentTile.Integral.Acquire(yOffset, _mmfReadBuffer);
|
||||
int localX;
|
||||
while (written < sourceMap.Length && (localX = sourceMap[written] - xOffset) < TileWidth)
|
||||
{
|
||||
destination.Span[written] += _mmfReadBuffer[localX];
|
||||
destination.Span[written] += xAdder;
|
||||
written++;
|
||||
}
|
||||
|
||||
if (written >= sourceMap.Length)
|
||||
break;
|
||||
|
||||
xAdder += _mmfReadBuffer[RightmostPixelIndex];
|
||||
xOffset += TileWidth;
|
||||
currentTile = TileManager.GetAdjacent(currentTile, 1, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Domain/StitchATon2.Domain.csproj
Normal file
13
Domain/StitchATon2.Domain.csproj
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Infra\StitchATon2.Infra.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
15
Domain/Tile.cs
Normal file
15
Domain/Tile.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using StitchATon2.Infra;
|
||||
|
||||
namespace StitchATon2.Domain;
|
||||
|
||||
public class Tile
|
||||
{
|
||||
public required int Id { get; init; }
|
||||
|
||||
public required int Column { get; init; }
|
||||
public required int Row { get; init; }
|
||||
|
||||
public required string Coordinate { get; init; }
|
||||
|
||||
public required ImageIntegral Integral { get; init; }
|
||||
}
|
||||
108
Domain/TileManager.cs
Normal file
108
Domain/TileManager.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using StitchATon2.Infra;
|
||||
using StitchATon2.Infra.Buffers;
|
||||
|
||||
namespace StitchATon2.Domain;
|
||||
|
||||
public sealed class TileManager : IDisposable
|
||||
{
|
||||
private readonly IMemoryOwner<Tile> _tiles;
|
||||
|
||||
public Configuration Configuration { get; }
|
||||
|
||||
public TileManager(Configuration config)
|
||||
{
|
||||
Configuration = config;
|
||||
_tiles = MemoryAllocator.AllocateManaged<Tile>(config.TileCount);
|
||||
var tilesSpan = _tiles.Memory.Span;
|
||||
for (var id = 0; id < config.TileCount; id++)
|
||||
tilesSpan[id] = CreateTile(id);
|
||||
|
||||
Console.WriteLine("Tile manager created");
|
||||
}
|
||||
|
||||
~TileManager() => Dispose();
|
||||
|
||||
private Tile CreateTile(int id)
|
||||
{
|
||||
var (row, column) = int.DivRem(id, Configuration.Columns);
|
||||
var coordinate = $"{Utils.GetSBSNotation(++row)}{++column}";
|
||||
return new Tile
|
||||
{
|
||||
Id = id,
|
||||
Row = row,
|
||||
Column = column,
|
||||
Coordinate = coordinate,
|
||||
Integral = new ImageIntegral(
|
||||
imagePath: Configuration.GetAssetPath($"{coordinate}.png"),
|
||||
outputDirectory: Configuration.CachePath,
|
||||
width: Configuration.Width,
|
||||
height: Configuration.Height
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private int GetId(int column, int row) => column - 1 + (row - 1) * Configuration.Columns;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Tile GetTile(int id) => _tiles.Memory.Span[id];
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Tile GetTile(int column, int row) => GetTile(GetId(column, row));
|
||||
|
||||
public Tile GetTile(string coordinate)
|
||||
{
|
||||
var (column, row) = Utils.GetSBSCoordinate(coordinate);
|
||||
return GetTile(column, row);
|
||||
}
|
||||
|
||||
public Tile? TryGetAdjacent(Tile tile, int columnOffset, int rowOffset)
|
||||
{
|
||||
var column = tile.Column + columnOffset;
|
||||
if(column <= 0 || column > Configuration.Columns)
|
||||
return null;
|
||||
|
||||
var row = tile.Row + rowOffset;
|
||||
if(row <= 0 || row > Configuration.Rows)
|
||||
return null;
|
||||
|
||||
return GetTile(column, row);
|
||||
}
|
||||
|
||||
public (Tile TopLeft, Tile BottomRight) GetTilePair(string coordinatePair)
|
||||
{
|
||||
var index = coordinatePair.IndexOf(':');
|
||||
var topLeft = GetTile(coordinatePair[..index++]);
|
||||
var bottomRight = GetTile(coordinatePair[index..]);
|
||||
|
||||
return (topLeft, bottomRight);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Tile GetAdjacent(Tile tile, int columnOffset, int rowOffset)
|
||||
{
|
||||
return GetTile(tile.Column + columnOffset, tile.Row + rowOffset);
|
||||
}
|
||||
|
||||
public GridSection CreateSection(
|
||||
string coordinatePair,
|
||||
float cropX,
|
||||
float cropY,
|
||||
float cropWidth,
|
||||
float cropHeight)
|
||||
=> new(
|
||||
this,
|
||||
coordinatePair,
|
||||
cropX,
|
||||
cropY,
|
||||
cropWidth,
|
||||
cropHeight);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_tiles.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
84
Domain/Utils.cs
Normal file
84
Domain/Utils.cs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
using System.Diagnostics.Contracts;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using StitchATon2.Infra.Buffers;
|
||||
|
||||
namespace StitchATon2.Domain;
|
||||
|
||||
public static class Utils
|
||||
{
|
||||
[Pure]
|
||||
public static string GetSBSNotation(int row)
|
||||
=> row <= 26
|
||||
? new string([(char)(row + 'A' - 1)])
|
||||
: new string(['A', (char)(row + 'A' - 27)]);
|
||||
|
||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int DivCeil(int a, int b)
|
||||
{
|
||||
return (a + b - 1) / b;
|
||||
}
|
||||
|
||||
public static (int Column, int Row) GetSBSCoordinate(string coordinate)
|
||||
{
|
||||
var column = coordinate[^1] - '0';
|
||||
if(char.IsDigit(coordinate[^2]))
|
||||
column += 10 * (coordinate[^2] - '0');
|
||||
|
||||
var row = char.IsLetter(coordinate[1])
|
||||
? 26 + coordinate[1] - 'A' + 1
|
||||
: coordinate[0] - 'A' + 1;
|
||||
|
||||
return (column, row);
|
||||
}
|
||||
|
||||
|
||||
public static IBuffer<int> BoundsMatrix(float scaleFactor, int length, int max, int offset)
|
||||
{
|
||||
var vectorSize = DivCeil(length, Vector<float>.Count);
|
||||
using var buffer = MemoryAllocator.Allocate<Vector<float>>(vectorSize);
|
||||
|
||||
var span = buffer.Span;
|
||||
var vectorMin = Vector<int>.Zero;
|
||||
var vectorOffset = new Vector<int>(offset - 1);
|
||||
var vectorMax = new Vector<int>(max - 1);
|
||||
var vectorScale = new Vector<float>(scaleFactor);
|
||||
|
||||
var vectorSequence = SequenceVector(0f, 1f);
|
||||
|
||||
var seq = 0f;
|
||||
for (var i = 0; i < vectorSize; i++, seq += Vector<float>.Count)
|
||||
{
|
||||
var sequence = new Vector<float>(seq) + vectorSequence;
|
||||
span[i] = Vector.Multiply(sequence, vectorScale);
|
||||
span[i] = Vector.Add(span[i], vectorScale);
|
||||
span[i] = Vector.Ceiling(span[i]);
|
||||
}
|
||||
|
||||
var result = MemoryAllocator.Allocate<int>(vectorSize * Vector<int>.Count);
|
||||
var resultSpan = MemoryMarshal.Cast<int, Vector<int>>(result.Span);
|
||||
for (var i = 0; i < vectorSize; i++)
|
||||
{
|
||||
resultSpan[i] = Vector.ConvertToInt32(span[i]);
|
||||
resultSpan[i] = Vector.Add(resultSpan[i], vectorOffset);
|
||||
resultSpan[i] = Vector.Min(resultSpan[i], vectorMax);
|
||||
resultSpan[i] = Vector.Max(resultSpan[i], vectorMin);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Vector<float> SequenceVector(float start, float step)
|
||||
{
|
||||
var vector = Vector<float>.Zero;
|
||||
ref var reference = ref Unsafe.As<Vector<float>, float>(ref vector);
|
||||
for (var i = 0; i < Vector<float>.Count; i++)
|
||||
{
|
||||
ref var current = ref Unsafe.Add(ref reference, i);
|
||||
current = start + step * i;
|
||||
}
|
||||
|
||||
return vector;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue