feat : rework with the logic again, use skiasharp instead
This commit is contained in:
parent
dd9f0d9cbe
commit
c390415b6e
3 changed files with 290 additions and 46 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
using SkiaSharp;
|
||||||
using StitcherApi.Models;
|
using StitcherApi.Models;
|
||||||
using StitcherApi.Services.Utilities;
|
using StitcherApi.Services.Utilities;
|
||||||
|
|
||||||
|
|
@ -5,64 +6,247 @@ namespace StitcherApi.Services;
|
||||||
|
|
||||||
public class ImageService : IImageService
|
public class ImageService : IImageService
|
||||||
{
|
{
|
||||||
|
private const int TileDimension = 720;
|
||||||
|
private const long HighQualityMemoryThreshold = 512 * 1024 * 1024 * 3;
|
||||||
|
private readonly string _assetPath;
|
||||||
private readonly ILogger<ImageService> _logger;
|
private readonly ILogger<ImageService> _logger;
|
||||||
private readonly StitchProcessor _stitchProcessor;
|
|
||||||
|
|
||||||
public ImageService(IConfiguration config, ILoggerFactory loggerFactory)
|
public ImageService(IConfiguration configuration, ILogger<ImageService> logger)
|
||||||
{
|
{
|
||||||
_logger = loggerFactory.CreateLogger<ImageService>();
|
_assetPath =
|
||||||
string assetPath =
|
configuration["AssetPath"]
|
||||||
config["AssetPath"] ?? throw new InvalidOperationException("AssetPath not configured.");
|
?? throw new InvalidOperationException("AssetPath is not configured.");
|
||||||
_stitchProcessor = new StitchProcessor(
|
_logger = logger;
|
||||||
assetPath,
|
|
||||||
loggerFactory.CreateLogger<StitchProcessor>()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<byte[]> GenerateImageAsync(GenerateImageRequest request)
|
public Task<byte[]> GenerateImageAsync(GenerateImageRequest request)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Processing request with the fast processor...");
|
return Task.Run(() =>
|
||||||
(int minRow, int minCol, int maxRow, int maxCol) = CoordinateParser.ParseCanvasRect(
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Starting image generation for canvas_rect: {CanvasRect}",
|
||||||
request.CanvasRect
|
request.CanvasRect
|
||||||
);
|
);
|
||||||
|
|
||||||
StitchRequest stitchRequest = CreateStitchRequest(request, minRow, minCol, maxRow, maxCol);
|
var (startRow, endRow, startCol, endCol) = CoordinateHelper.ParseCanvasRect(
|
||||||
|
request.CanvasRect
|
||||||
|
);
|
||||||
|
int canvasWidth = (endCol - startCol + 1) * TileDimension;
|
||||||
|
int canvasHeight = (endRow - startRow + 1) * TileDimension;
|
||||||
|
int cropX = (int)(request.CropOffset[0] * canvasWidth);
|
||||||
|
int cropY = (int)(request.CropOffset[1] * canvasHeight);
|
||||||
|
int cropW = (int)(request.CropSize[0] * canvasWidth);
|
||||||
|
int cropH = (int)(request.CropSize[1] * canvasHeight);
|
||||||
|
|
||||||
return await _stitchProcessor.ProcessAsync(stitchRequest);
|
int outputW = (int)(cropW * request.OutputScale);
|
||||||
|
int outputH = (int)(cropH * request.OutputScale);
|
||||||
|
if (outputW <= 0 || outputH <= 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Output dimensions are zero or negative ({Width}x{Height}). Returning empty byte array.",
|
||||||
|
outputW,
|
||||||
|
outputH
|
||||||
|
);
|
||||||
|
return Array.Empty<byte>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private StitchRequest CreateStitchRequest(
|
_logger.LogDebug("Calculated final dimensions: {Width}x{Height}", outputW, outputH);
|
||||||
GenerateImageRequest request,
|
|
||||||
int minRow,
|
|
||||||
int minCol,
|
|
||||||
int maxRow,
|
|
||||||
int maxCol
|
|
||||||
)
|
|
||||||
{
|
|
||||||
const int TILE_SIZE = 720;
|
|
||||||
int stitchedCanvasWidth = (maxCol - minCol + 1) * TILE_SIZE;
|
|
||||||
int stitchedCanvasHeight = (maxRow - minRow + 1) * TILE_SIZE;
|
|
||||||
int cropX = (int)(request.CropOffset[0] * stitchedCanvasWidth);
|
|
||||||
int cropY = (int)(request.CropOffset[1] * stitchedCanvasHeight);
|
|
||||||
int cropW = (int)(request.CropSize[0] * stitchedCanvasWidth);
|
|
||||||
int cropH = (int)(request.CropSize[1] * stitchedCanvasHeight);
|
|
||||||
int startTileCol = minCol + cropX / TILE_SIZE;
|
|
||||||
int endTileCol = minCol + (cropX + cropW - 1) / TILE_SIZE;
|
|
||||||
int startTileRow = minRow + cropY / TILE_SIZE;
|
|
||||||
int endTileRow = minRow + (cropY + cropH - 1) / TILE_SIZE;
|
|
||||||
|
|
||||||
return new StitchRequest(
|
long requiredMemory = (long)cropW * cropH * 4;
|
||||||
minRow,
|
if (requiredMemory <= HighQualityMemoryThreshold)
|
||||||
minCol,
|
{
|
||||||
startTileRow,
|
_logger.LogInformation(
|
||||||
startTileCol,
|
"Using high-quality rendering path (required memory: {Memory}MB)",
|
||||||
endTileRow,
|
requiredMemory / (1024 * 1024)
|
||||||
endTileCol,
|
);
|
||||||
|
return GenerateWithHighQuality(
|
||||||
cropX,
|
cropX,
|
||||||
cropY,
|
cropY,
|
||||||
cropW,
|
cropW,
|
||||||
cropH,
|
cropH,
|
||||||
|
outputW,
|
||||||
|
outputH,
|
||||||
|
startRow,
|
||||||
|
startCol
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Required memory ({Memory}MB) exceeds threshold. Using low-memory fallback path. Image quality may be reduced.",
|
||||||
|
requiredMemory / (1024 * 1024)
|
||||||
|
);
|
||||||
|
return GenerateWithLowMemory(
|
||||||
|
cropX,
|
||||||
|
cropY,
|
||||||
|
cropW,
|
||||||
|
cropH,
|
||||||
|
outputW,
|
||||||
|
outputH,
|
||||||
|
startRow,
|
||||||
|
startCol,
|
||||||
request.OutputScale
|
request.OutputScale
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
ex,
|
||||||
|
"An unhandled exception occurred during image generation for request: {@Request}",
|
||||||
|
request
|
||||||
|
);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GenerateWithHighQuality(
|
||||||
|
int cropX,
|
||||||
|
int cropY,
|
||||||
|
int cropW,
|
||||||
|
int cropH,
|
||||||
|
int outputW,
|
||||||
|
int outputH,
|
||||||
|
int startRow,
|
||||||
|
int startCol
|
||||||
|
)
|
||||||
|
{
|
||||||
|
using var cropBufferBitmap = new SKBitmap(cropW, cropH);
|
||||||
|
using var cropBufferCanvas = new SKCanvas(cropBufferBitmap);
|
||||||
|
|
||||||
|
DrawTilesToCanvas(cropBufferCanvas, cropX, cropY, cropW, cropH, startRow, startCol);
|
||||||
|
|
||||||
|
using var finalBitmap = new SKBitmap(outputW, outputH);
|
||||||
|
|
||||||
|
var sampling = new SKSamplingOptions(SKCubicResampler.Mitchell);
|
||||||
|
cropBufferBitmap.ScalePixels(finalBitmap, sampling);
|
||||||
|
|
||||||
|
return EncodeBitmap(finalBitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GenerateWithLowMemory(
|
||||||
|
int cropX,
|
||||||
|
int cropY,
|
||||||
|
int cropW,
|
||||||
|
int cropH,
|
||||||
|
int outputW,
|
||||||
|
int outputH,
|
||||||
|
int startRow,
|
||||||
|
int startCol,
|
||||||
|
float scale
|
||||||
|
)
|
||||||
|
{
|
||||||
|
using var finalBitmap = new SKBitmap(outputW, outputH);
|
||||||
|
using var finalCanvas = new SKCanvas(finalBitmap);
|
||||||
|
var sampling = new SKSamplingOptions(SKCubicResampler.Mitchell);
|
||||||
|
|
||||||
|
int firstTileCol = cropX / TileDimension;
|
||||||
|
int lastTileCol = (cropX + cropW - 1) / TileDimension;
|
||||||
|
int firstTileRow = cropY / TileDimension;
|
||||||
|
int lastTileRow = (cropY + cropH - 1) / TileDimension;
|
||||||
|
|
||||||
|
for (int r = firstTileRow; r <= lastTileRow; r++)
|
||||||
|
{
|
||||||
|
for (int c = firstTileCol; c <= lastTileCol; c++)
|
||||||
|
{
|
||||||
|
var tilePath = Path.Combine(
|
||||||
|
_assetPath,
|
||||||
|
$"{CoordinateHelper.IndexToRow(startRow + r)}{startCol + c + 1}.png"
|
||||||
|
);
|
||||||
|
using var tileBitmap = SKBitmap.Decode(tilePath);
|
||||||
|
if (tileBitmap == null)
|
||||||
|
continue;
|
||||||
|
using var tileImage = SKImage.FromBitmap(tileBitmap);
|
||||||
|
|
||||||
|
int tileCanvasX = c * TileDimension;
|
||||||
|
int tileCanvasY = r * TileDimension;
|
||||||
|
int intersectX = Math.Max(cropX, tileCanvasX);
|
||||||
|
int intersectY = Math.Max(cropY, tileCanvasY);
|
||||||
|
int intersectEndX = Math.Min(cropX + cropW, tileCanvasX + TileDimension);
|
||||||
|
int intersectEndY = Math.Min(cropY + cropH, tileCanvasY + TileDimension);
|
||||||
|
|
||||||
|
var sourceRect = SKRect.Create(
|
||||||
|
intersectX - tileCanvasX,
|
||||||
|
intersectY - tileCanvasY,
|
||||||
|
intersectEndX - intersectX,
|
||||||
|
intersectEndY - intersectY
|
||||||
|
);
|
||||||
|
var destRect = SKRect.Create(
|
||||||
|
(intersectX - cropX) * scale,
|
||||||
|
(intersectY - cropY) * scale,
|
||||||
|
(intersectEndX - intersectX) * scale,
|
||||||
|
(intersectEndY - intersectY) * scale
|
||||||
|
);
|
||||||
|
|
||||||
|
finalCanvas.DrawImage(tileImage, sourceRect, destRect, sampling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EncodeBitmap(finalBitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTilesToCanvas(
|
||||||
|
SKCanvas canvas,
|
||||||
|
int cropX,
|
||||||
|
int cropY,
|
||||||
|
int cropW,
|
||||||
|
int cropH,
|
||||||
|
int startRow,
|
||||||
|
int startCol
|
||||||
|
)
|
||||||
|
{
|
||||||
|
int firstTileCol = cropX / TileDimension;
|
||||||
|
int lastTileCol = (cropX + cropW - 1) / TileDimension;
|
||||||
|
int firstTileRow = cropY / TileDimension;
|
||||||
|
int lastTileRow = (cropY + cropH - 1) / TileDimension;
|
||||||
|
|
||||||
|
for (int r = firstTileRow; r <= lastTileRow; r++)
|
||||||
|
{
|
||||||
|
for (int c = firstTileCol; c <= lastTileCol; c++)
|
||||||
|
{
|
||||||
|
var tilePath = Path.Combine(
|
||||||
|
_assetPath,
|
||||||
|
$"{CoordinateHelper.IndexToRow(startRow + r)}{startCol + c + 1}.png"
|
||||||
|
);
|
||||||
|
using var tileBitmap = SKBitmap.Decode(tilePath);
|
||||||
|
if (tileBitmap == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int tileCanvasX = c * TileDimension;
|
||||||
|
int tileCanvasY = r * TileDimension;
|
||||||
|
int intersectX = Math.Max(cropX, tileCanvasX);
|
||||||
|
int intersectY = Math.Max(cropY, tileCanvasY);
|
||||||
|
int intersectEndX = Math.Min(cropX + cropW, tileCanvasX + TileDimension);
|
||||||
|
int intersectEndY = Math.Min(cropY + cropH, tileCanvasY + TileDimension);
|
||||||
|
|
||||||
|
var sourceRect = SKRect.Create(
|
||||||
|
intersectX - tileCanvasX,
|
||||||
|
intersectY - tileCanvasY,
|
||||||
|
intersectEndX - intersectX,
|
||||||
|
intersectEndY - intersectY
|
||||||
|
);
|
||||||
|
var destRect = SKRect.Create(
|
||||||
|
intersectX - cropX,
|
||||||
|
intersectY - cropY,
|
||||||
|
intersectEndX - intersectX,
|
||||||
|
intersectEndY - intersectY
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.DrawBitmap(tileBitmap, sourceRect, destRect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] EncodeBitmap(SKBitmap bitmap)
|
||||||
|
{
|
||||||
|
using var image = SKImage.FromBitmap(bitmap);
|
||||||
|
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Image generation successful. Returning {ByteCount} bytes.",
|
||||||
|
data.Size
|
||||||
|
);
|
||||||
|
return data.ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
60
Services/Utilities/CoordinateHelper.cs
Normal file
60
Services/Utilities/CoordinateHelper.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace StitcherApi.Services.Utilities;
|
||||||
|
|
||||||
|
public static class CoordinateHelper
|
||||||
|
{
|
||||||
|
private static readonly Regex CoordRegex = new(@"([A-Z]+)(\d+)", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static (int startRow, int endRow, int startCol, int endCol) ParseCanvasRect(
|
||||||
|
string rectStr
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var parts = rectStr.Split(':');
|
||||||
|
if (parts.Length != 2)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Invalid canvas_rect format. Expected format 'A1:H12'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var (r1, c1) = ParseSingleCoordinate(parts[0]);
|
||||||
|
var (r2, c2) = ParseSingleCoordinate(parts[1]);
|
||||||
|
|
||||||
|
return (Math.Min(r1, r2), Math.Max(r1, r2), Math.Min(c1, c2), Math.Max(c1, c2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string IndexToRow(int index)
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
var result = "";
|
||||||
|
while (index > 0)
|
||||||
|
{
|
||||||
|
int remainder = (index - 1) % 26;
|
||||||
|
result = (char)('A' + remainder) + result;
|
||||||
|
index = (index - 1) / 26;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int row, int col) ParseSingleCoordinate(string coord)
|
||||||
|
{
|
||||||
|
var match = CoordRegex.Match(coord);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Invalid coordinate format: '{coord}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string rowStr = match.Groups[1].Value;
|
||||||
|
int col = int.Parse(match.Groups[2].Value) - 1;
|
||||||
|
return (RowToIndex(rowStr), col);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RowToIndex(string rowStr)
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
foreach (char c in rowStr)
|
||||||
|
{
|
||||||
|
index = index * 26 + (c - 'A' + 1);
|
||||||
|
}
|
||||||
|
return index - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,6 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.5" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
<PackageReference Include="SkiaSharp" Version="3.119.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue