22 private const bool VerboseLogging =
false;
23 #region Fields and Properties
25 private readonly SlideBase _slideSource;
26 private readonly OpenSlideGTK.OpenSlideBase _openSlideSource;
27 private readonly
bool _isOpenSlide;
30 private ConcurrentDictionary<string, CachedTile> _tileCache;
31 private int _maxCacheSize;
32 private object _cacheLock =
new object();
35 private SKSurface _compositeSurface;
36 private SKCanvas _compositeCanvas;
37 private int _currentSurfaceWidth = 600;
38 private int _currentSurfaceHeight = 400;
41 private ConcurrentDictionary<string, Task<SKImage>> _pendingTileFetches;
44 public int TileSize {
get;
set; } = 256;
45 public int PrefetchRadius {
get;
set; } = 1;
46 public SKSamplingOptions Sampling {
get;
set; } = SKSamplingOptions.Default;
47 public bool EnablePrefetch {
get;
set; } =
false;
56 private class CachedTile
58 public SKImage Image {
get;
set; }
59 public DateTime LastAccessed {
get;
set; }
60 public long SizeBytes {
get;
set; }
61 public string Key {
get;
set; }
63 public CachedTile(SKImage image,
string key)
67 LastAccessed = DateTime.UtcNow;
68 SizeBytes = CalculateSize(image);
71 private long CalculateSize(SKImage image)
73 if (image ==
null)
return 0;
74 return image.Width * image.Height * 4;
86 set => Index =
new TileIndex(Index.Col, Index.Row, value);
92 get => Index.Col * 256;
95 int col = value / 256;
96 Index =
new TileIndex(col, Index.Row, Level);
102 get => Index.Row * 256;
105 int row = value / 256;
106 Index =
new TileIndex(Index.Col, row, Level);
110 public ZCT Coordinate {
get;
set; }
111 public Extent Extent {
get;
set; }
112 public TileIndex Index {
get;
set; }
114 public RectangleD Bounds
118 return new RectangleD(
121 Extent.MaxX - Extent.MinX,
122 Extent.MaxY - Extent.MinY
127 public TileInfo TileInfo
129 get =>
new TileInfo { Index = Index, Extent = Extent };
133 Extent = value.Extent;
144 public string GetCacheKey()
147 string rgbKey =
string.Empty;
148 if (App.viewer?.Mode ==
ImageView.ViewMode.RGBImage &&
ImageView.SelectedImage !=
null)
150 rgbKey = $
"_{ImageView.SelectedImage.rgbChannels[0]}_{ImageView.SelectedImage.rgbChannels[1]}_{ImageView.SelectedImage.rgbChannels[2]}";
152 return $
"{Index.Col}_{Index.Row}_{Level}_{Coordinate.Z}_{Coordinate.C}_{Coordinate.T}{rgbKey}";
157 #region Constructor and Initialization
164 _openSlideSource = openSlideSource ??
throw new ArgumentNullException(nameof(openSlideSource));
167 InitializeCommon(maxCacheSizeMB);
175 _slideSource = slideSource ??
throw new ArgumentNullException(nameof(slideSource));
176 _isOpenSlide =
false;
178 InitializeCommon(maxCacheSizeMB);
181 private void InitializeCommon(
int maxCacheSizeMB)
183 _maxCacheSize = maxCacheSizeMB * 1024 * 1024;
184 _tileCache =
new ConcurrentDictionary<string, CachedTile>();
185 _pendingTileFetches =
new ConcurrentDictionary<string, Task<SKImage>>();
190 #region Public Stitching Methods
191 private readonly SemaphoreSlim _renderSemaphore =
new SemaphoreSlim(1, 1);
199 int viewportWidth = 600,
200 int viewportHeight = 400)
203 int level = CalculateLevelFromResolution(resolution);
204 var tileRequests = CalculateTileRequests(origin, viewportWidth, viewportHeight, level, coordinate, resolution);
205 if (viewportWidth <= 1 || viewportHeight <= 1)
208 viewportHeight = 400;
211 using (var surface = SKSurface.Create(
new SKImageInfo(
214 SKColorType.Rgba8888,
215 SKAlphaType.Premul)))
219 Console.WriteLine(
"Failed to create SKSurface");
223 var canvas = surface.Canvas;
224 canvas.Clear(SKColors.Gray);
227 await CompositeTilesAsync(canvas, tileRequests, origin, resolution);
230 return surface.Snapshot();
240 ZCT coordinate,
double resolution,
int pxwidth,
int pxheight)
242 var tileRequests = CalculateTileRequests(
243 new PointD(region.X,region.Y),pxwidth, pxheight, level, coordinate, resolution);
245 int outputWidth = (int)Math.Ceiling(region.W);
246 int outputHeight = (int)Math.Ceiling(region.H);
248 using (var surface = SKSurface.Create(
new SKImageInfo(outputWidth, outputHeight)))
250 var canvas = surface.Canvas;
251 canvas.Clear(SKColors.Transparent);
253 foreach (var request
in tileRequests)
255 var tileImage = FetchTileSync(request);
256 if (tileImage !=
null)
258 DrawTileToCanvas(canvas,request,
new AForge.PointD(region.X,region.Y), resolution, tileImage);
262 return surface.Snapshot();
268 #region Tile Fetching and Caching
273 private async Task<SKImage> FetchTileAsync(TileRequest request, ZCT coordinate)
275 string key = request.GetCacheKey();
278 if (_tileCache.TryGetValue(key, out var cached))
return cached.Image;
281 if (_pendingTileFetches.TryGetValue(key, out var pendingTask))
return await pendingTask;
284 var fetchTask = Task.Run(async () =>
291 TileInfo tf =
new TileInfo { Index = request.Index, Extent = request.Extent };
293 data = _openSlideSource.GetTile(tf);
297 TileInfo tf =
new TileInfo { Index = request.Index, Extent = request.Extent };
298 data = await FetchSlideTileAsync(tf, request.Coordinate).ConfigureAwait(
false);
301 if (data ==
null || data.Length == 0)
return null;
304 var levelResolution = CalculateResolutionFromLevel(request.Index.Level);
305 var image = ConvertTileDataToSKImage(data, request);
309 var cachedTile =
new CachedTile(image, DateTime.Now.ToString());
310 _tileCache.AddOrUpdate(key, cachedTile, (k, old) => cachedTile);
316 if (VerboseLogging) Console.WriteLine($
"Fetch error for tile {key}: {ex.Message} + {ex.StackTrace}");
320 _pendingTileFetches.TryRemove(key, out _);
325 _pendingTileFetches[key] = fetchTask;
326 return await fetchTask;
329 private bool ShouldComposeRgbTile()
331 if (_isOpenSlide || App.viewer?.Mode != ImageView.ViewMode.RGBImage)
334 var bioImage = _slideSource?.Image?.BioImage;
335 if (bioImage ==
null || bioImage.Channels ==
null || bioImage.Channels.Count <= 1)
337 if (bioImage.Resolutions ==
null || bioImage.Resolutions.Count == 0)
340 var pixelFormat = bioImage.Resolutions[0].PixelFormat;
341 return pixelFormat != AForge.PixelFormat.Format24bppRgb &&
342 pixelFormat != AForge.PixelFormat.Format48bppRgb;
345 private async Task<byte[]> FetchSlideTileAsync(TileInfo tileInfo, ZCT coordinate)
347 if (!ShouldComposeRgbTile())
348 return _slideSource.GetTile(tileInfo, coordinate);
350 var bioImage = _slideSource?.Image?.BioImage;
351 if (bioImage ==
null)
352 return _slideSource.GetTile(tileInfo, coordinate);
354 int channelCount = Math.Max(1, bioImage.Channels.Count);
355 int redIndex = Math.Clamp(bioImage.rgbChannels[0], 0, channelCount - 1);
356 int greenIndex = Math.Clamp(bioImage.rgbChannels[1], 0, channelCount - 1);
357 int blueIndex = Math.Clamp(bioImage.rgbChannels[2], 0, channelCount - 1);
359 byte[] redTile = await _slideSource.GetTileAsync(
new BioLib.TileInformation(tileInfo.Index, tileInfo.Extent,
new ZCT(coordinate.Z, redIndex, coordinate.T)),
new ZCT(coordinate.Z, redIndex, coordinate.T)).ConfigureAwait(
false);
360 byte[] greenTile = await _slideSource.GetTileAsync(
new BioLib.TileInformation(tileInfo.Index, tileInfo.Extent,
new ZCT(coordinate.Z, greenIndex, coordinate.T)),
new ZCT(coordinate.Z, greenIndex, coordinate.T)).ConfigureAwait(
false);
361 byte[] blueTile = await _slideSource.GetTileAsync(
new BioLib.TileInformation(tileInfo.Index, tileInfo.Extent,
new ZCT(coordinate.Z, blueIndex, coordinate.T)),
new ZCT(coordinate.Z, blueIndex, coordinate.T)).ConfigureAwait(
false);
362 return ComposeBgraTiles(redTile, greenTile, blueTile);
365 private static byte[] ComposeBgraTiles(
byte[] redTile,
byte[] greenTile,
byte[] blueTile)
367 byte[] fallback = redTile ?? greenTile ?? blueTile;
368 if (fallback ==
null)
371 redTile ??= fallback;
372 greenTile ??= fallback;
373 blueTile ??= fallback;
375 int byteCount = Math.Min(redTile.Length, Math.Min(greenTile.Length, blueTile.Length));
376 byteCount -= byteCount % 4;
380 byte[] composed =
new byte[byteCount];
381 for (
int i = 0; i < byteCount; i += 4)
383 byte r = ExtractIntensity(redTile, i);
384 byte g = ExtractIntensity(greenTile, i);
385 byte b = ExtractIntensity(blueTile, i);
386 byte a = (byte)Math.Max(redTile[i + 3], Math.Max(greenTile[i + 3], blueTile[i + 3]));
387 if (a == 0 && (r != 0 || g != 0 || b != 0))
399 private static byte ExtractIntensity(
byte[] tile,
int offset)
401 return (
byte)Math.Max(tile[offset], Math.Max(tile[offset + 1], tile[offset + 2]));
407 private SKImage FetchTileSync(TileRequest request)
409 string cacheKey = request.GetCacheKey();
412 if (_tileCache.TryGetValue(cacheKey, out var cachedTile))
414 cachedTile.LastAccessed = DateTime.UtcNow;
415 return cachedTile.Image;
421 byte[] tileData = FetchTileDataSync(request);
422 if (tileData ==
null || tileData.Length == 0)
425 SKImage skImage = ConvertTileDataToSKImage(tileData, request);
429 AddToCache(cacheKey, skImage);
436 if (VerboseLogging) Console.WriteLine($
"Error fetching tile sync {cacheKey}: {ex.Message}");
444 private async Task<byte[]> FetchTileDataAsync(TileRequest request)
448 Console.WriteLine($
"\n--- Fetching tile data for Level {request.Level}, Index[{request.Index.Col},{request.Index.Row}] ---");
449 Console.WriteLine($
" Extent: ({request.Extent.MinX:F0}, {request.Extent.MinY:F0}) to ({request.Extent.MaxX:F0}, {request.Extent.MaxY:F0})");
457 byte[] tileData = _openSlideSource.GetTile(request.TileInfo);
459 if (tileData ==
null || tileData.Length == 0)
461 if (VerboseLogging) Console.WriteLine($
" ERROR: OpenSlide returned null/empty tile data");
465 if (VerboseLogging) Console.WriteLine($
" OpenSlide returned {tileData.Length} bytes");
471 var sliceInfo =
new BioLib.SliceInfo(
476 CalculateResolutionFromLevel(request.Level),
480 var origin =
new PointD(request.Extent.MinX, request.Extent.MinY);
481 var size =
new AForge.Size((
int)request.Bounds.W, (
int)request.Bounds.H);
482 TileInformation tf =
new TileInformation(request.Index, request.Extent, request.Coordinate);
483 byte[] tileData = await _slideSource.GetTileAsync(tf, request.Coordinate);
485 if (tileData ==
null || tileData.Length == 0)
487 if (VerboseLogging) Console.WriteLine($
" ERROR: SlideBase returned null/empty tile data");
491 if (VerboseLogging) Console.WriteLine($
" SlideBase returned {tileData.Length} bytes");
499 Console.WriteLine($
" EXCEPTION in FetchTileDataAsync: {ex.Message}");
500 Console.WriteLine($
" {ex.StackTrace}");
508 private byte[] FetchTileDataSync(TileRequest request)
512 var sliceInfo =
new OpenSlideGTK.SliceInfo(
517 CalculateResolutionFromLevel(request.Level)
520 return _openSlideSource.GetSlice(sliceInfo, request.Coordinate, request.Level);
524 if (ShouldComposeRgbTile())
526 TileInfo tf =
new TileInfo { Index = request.Index, Extent = request.Extent };
527 return FetchSlideTileAsync(tf, request.Coordinate).Result;
530 var sliceInfo =
new BioLib.SliceInfo(
535 CalculateResolutionFromLevel(request.Level),
539 var origin =
new PointD(request.Bounds.X, request.Bounds.Y);
540 var size =
new AForge.Size((
int)request.Bounds.W, (
int)request.Bounds.H);
542 return _slideSource.GetSlice(sliceInfo, origin, size).Result;
548 #region Tile Compositing
553 private async Task CompositeTilesAsync(
555 List<TileRequest> tileRequests,
559 if (tileRequests.Count == 0)
return;
562 var tileTasks = tileRequests.Select(async req =>
564 var image = await FetchTileAsync(req, req.Coordinate);
565 return new { Request = req, Image = image };
568 var results = await Task.WhenAll(tileTasks);
570 using (var paint =
new SKPaint { IsAntialias =
false })
572 foreach (var result
in results)
574 if (result.Image ==
null)
continue;
575 DrawTileToCanvas(canvas, result.Request, origin, resolution, result.Image, paint);
582 private void DrawTileToCanvas(
585 PointD viewportOrigin,
588 SKPaint paint =
null)
590 if (tileImage ==
null)
return;
593 float destX = (float)((request.Extent.MinX - viewportOrigin.X) / resolution);
598 float destY = (float)((-request.Extent.MaxY - viewportOrigin.Y) / resolution);
601 float destW = (float)(request.Extent.Width / resolution);
602 float destH = (float)(request.Extent.Height / resolution);
604 var destRect =
new SKRect(destX, destY, destX + destW, destY + destH);
608 canvas.DrawImage(tileImage, destRect, paint);
610 canvas.DrawImage(tileImage, destRect, Sampling);
615 private SKPaint CreateCompositePaint()
620 BlendMode = SKBlendMode.SrcOver
627 private SKPaint CreateTilePaint()
637 #region Tile Calculation and Management
642 private List<TileRequest> CalculateTileRequests(PointD origin,
int viewportWidth,
int viewportHeight,
int level, ZCT coordinate,
double resolution)
647 double minX = origin.X;
648 double maxX = origin.X + (viewportWidth * resolution);
649 double maxY = -origin.Y;
650 double minY = -origin.Y - (viewportHeight * resolution);
653 var viewportExtent =
new Extent(minX, minY, maxX, maxY);
655 IEnumerable<TileInfo> tileInfos;
658 tileInfos = _openSlideSource.Schema.GetTileInfos(viewportExtent, level);
662 tileInfos = _slideSource.Schema.GetTileInfos(viewportExtent, level);
665 var requests =
new List<TileRequest>();
666 foreach (var info
in tileInfos)
668 requests.Add(
new TileRequest(info, coordinate));
674 #region Cache Management
679 private void AddToCache(
string key, SKImage image)
681 var cachedTile =
new CachedTile(image, key);
685 _tileCache[key] = cachedTile;
688 long currentCacheSize = CalculateTotalCacheSize();
690 if (currentCacheSize > _maxCacheSize)
692 EvictLeastRecentlyUsedTiles(currentCacheSize - _maxCacheSize);
700 private void EvictLeastRecentlyUsedTiles(
long bytesToFree)
702 var sortedTiles =
new List<CachedTile>(_tileCache.Values);
703 sortedTiles.Sort((a, b) => a.LastAccessed.CompareTo(b.LastAccessed));
706 foreach (var tile
in sortedTiles)
708 if (freedBytes >= bytesToFree)
711 if (_tileCache.TryRemove(tile.Key, out var removed))
713 freedBytes += removed.SizeBytes;
714 removed.Image?.Dispose();
722 private long CalculateTotalCacheSize()
725 foreach (var tile
in _tileCache.Values)
727 total += tile.SizeBytes;
739 foreach (var tile
in _tileCache.Values)
741 tile.Image?.Dispose();
749 #region Utility Methods
755 private SKImage ConvertTileDataToSKImage(
byte[] tileData, TileRequest request)
760 using (var data = SKData.CreateCopy(tileData))
762 var image = SKImage.FromEncodedData(data);
763 if (image !=
null)
return image;
768 int width = _isOpenSlide ? _openSlideSource.Schema.GetTileWidth(request.Index.Level)
769 : _slideSource.Schema.GetTileWidth(request.Index.Level);
770 int height = _isOpenSlide ? _openSlideSource.Schema.GetTileHeight(request.Index.Level)
771 : _slideSource.Schema.GetTileHeight(request.Index.Level);
773 var info =
new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
776 var imageInfo =
new SKImageInfo(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);
777 var rawData = SKData.CreateCopy(tileData);
778 var pixmap =
new SKPixmap(imageInfo, rawData.Data, imageInfo.RowBytes);
781 return SKImage.FromPixels(pixmap, (address, context) =>
788 if (VerboseLogging) Console.WriteLine($
"Error converting tile data: {ex.Message}");
795 private int CalculateLevelFromResolution(
double resolution)
799 return OpenSlideGTK.TileUtil.GetLevel(_openSlideSource.Schema.Resolutions, resolution);
803 return OpenSlideGTK.TileUtil.GetLevel(_slideSource.Schema.Resolutions, resolution);
810 private double CalculateResolutionFromLevel(
int level)
814 return _openSlideSource.Schema.Resolutions[level].UnitsPerPixel;
818 return _slideSource.Schema.Resolutions[level].UnitsPerPixel;
825 public void Dispose()
828 _pendingTileFetches.Clear();;