BioGTK  6.7.0
A .NET library & program for annotating, editing various microscopy imaging formats using Bioformats supported images. Including whole slide, pyramidal, and series.
Loading...
Searching...
No Matches
SkiaStitch.cs
1using AForge;
2using BioLib;
3using BruTile;
4using CSScripting;
5using OpenSlideGTK;
6using org.apache.xmlgraphics.image.codec.tiff;
7using SkiaSharp;
8using System;
9using System.Collections.Concurrent;
10using System.Collections.Generic;
11using System.Runtime.InteropServices;
12using System.Threading;
13using System.Threading.Tasks;
14
15namespace BioGTK
16{
20 public class SkiaStitchingPipeline : IDisposable
21 {
22 private const bool VerboseLogging = false;
23 #region Fields and Properties
24
25 private readonly SlideBase _slideSource;
26 private readonly OpenSlideGTK.OpenSlideBase _openSlideSource;
27 private readonly bool _isOpenSlide;
28
29 // Tile cache with thread-safe access
30 private ConcurrentDictionary<string, CachedTile> _tileCache;
31 private int _maxCacheSize;
32 private object _cacheLock = new object();
33
34 // Rendering state
35 private SKSurface _compositeSurface;
36 private SKCanvas _compositeCanvas;
37 private int _currentSurfaceWidth = 600;
38 private int _currentSurfaceHeight = 400;
39
40 // Async operation management
41 private ConcurrentDictionary<string, Task<SKImage>> _pendingTileFetches;
42
43 // Configuration
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;
48
49 #endregion
50
51 #region Nested Types
52
56 private class CachedTile
57 {
58 public SKImage Image { get; set; }
59 public DateTime LastAccessed { get; set; }
60 public long SizeBytes { get; set; }
61 public string Key { get; set; }
62
63 public CachedTile(SKImage image, string key)
64 {
65 Image = image;
66 Key = key;
67 LastAccessed = DateTime.UtcNow;
68 SizeBytes = CalculateSize(image);
69 }
70
71 private long CalculateSize(SKImage image)
72 {
73 if (image == null) return 0;
74 return image.Width * image.Height * 4; // Assume RGBA
75 }
76 }
77
81 public class TileRequest
82 {
83 public int Level
84 {
85 get => Index.Level;
86 set => Index = new TileIndex(Index.Col, Index.Row, value);
87 }
88
89 // FIXED: Col is X, Row is Y
90 public int TileX
91 {
92 get => Index.Col * 256; // Col = X coordinate
93 set
94 {
95 int col = value / 256;
96 Index = new TileIndex(col, Index.Row, Level);
97 }
98 }
99
100 public int TileY
101 {
102 get => Index.Row * 256; // Row = Y coordinate
103 set
104 {
105 int row = value / 256;
106 Index = new TileIndex(Index.Col, row, Level);
107 }
108 }
109
110 public ZCT Coordinate { get; set; }
111 public Extent Extent { get; set; }
112 public TileIndex Index { get; set; }
113
114 public RectangleD Bounds
115 {
116 get
117 {
118 return new RectangleD(
119 Extent.MinX,
120 Extent.MinY,
121 Extent.MaxX - Extent.MinX,
122 Extent.MaxY - Extent.MinY
123 );
124 }
125 }
126
127 public TileInfo TileInfo
128 {
129 get => new TileInfo { Index = Index, Extent = Extent };
130 set
131 {
132 Index = value.Index;
133 Extent = value.Extent;
134 }
135 }
136
137 public TileRequest(TileInfo tf, ZCT coord)
138 {
139 Index = tf.Index;
140 Extent = tf.Extent;
141 Coordinate = coord;
142 }
143
144 public string GetCacheKey()
145 {
146 // Use Col/Row directly to avoid confusion
147 string rgbKey = string.Empty;
148 if (App.viewer?.Mode == ImageView.ViewMode.RGBImage && ImageView.SelectedImage != null)
149 {
150 rgbKey = $"_{ImageView.SelectedImage.rgbChannels[0]}_{ImageView.SelectedImage.rgbChannels[1]}_{ImageView.SelectedImage.rgbChannels[2]}";
151 }
152 return $"{Index.Col}_{Index.Row}_{Level}_{Coordinate.Z}_{Coordinate.C}_{Coordinate.T}{rgbKey}";
153 }
154 }
155 #endregion
156
157 #region Constructor and Initialization
158
162 public SkiaStitchingPipeline(OpenSlideGTK.OpenSlideBase openSlideSource, int maxCacheSizeMB = 512)
163 {
164 _openSlideSource = openSlideSource ?? throw new ArgumentNullException(nameof(openSlideSource));
165 _isOpenSlide = true;
166
167 InitializeCommon(maxCacheSizeMB);
168 }
169
173 public SkiaStitchingPipeline(SlideBase slideSource, int maxCacheSizeMB = 512)
174 {
175 _slideSource = slideSource ?? throw new ArgumentNullException(nameof(slideSource));
176 _isOpenSlide = false;
177
178 InitializeCommon(maxCacheSizeMB);
179 }
180
181 private void InitializeCommon(int maxCacheSizeMB)
182 {
183 _maxCacheSize = maxCacheSizeMB * 1024 * 1024;
184 _tileCache = new ConcurrentDictionary<string, CachedTile>();
185 _pendingTileFetches = new ConcurrentDictionary<string, Task<SKImage>>();
186 }
187
188 #endregion
189
190 #region Public Stitching Methods
191 private readonly SemaphoreSlim _renderSemaphore = new SemaphoreSlim(1, 1);
195 public async Task<SKImage> StitchViewportAsync(
196 PointD origin,
197 double resolution,
198 ZCT coordinate,
199 int viewportWidth = 600,
200 int viewportHeight = 400)
201 {
202 // Calculate viewport parameters
203 int level = CalculateLevelFromResolution(resolution);
204 var tileRequests = CalculateTileRequests(origin, viewportWidth, viewportHeight, level, coordinate, resolution);
205 if (viewportWidth <= 1 || viewportHeight <= 1)
206 {
207 viewportWidth = 600;
208 viewportHeight = 400;
209 }
210 // Create a NEW surface for this render - no shared state, fully parallel-safe
211 using (var surface = SKSurface.Create(new SKImageInfo(
212 viewportWidth,
213 viewportHeight,
214 SKColorType.Rgba8888,
215 SKAlphaType.Premul)))
216 {
217 if (surface == null)
218 {
219 Console.WriteLine("Failed to create SKSurface");
220 return null;
221 }
222
223 var canvas = surface.Canvas;
224 canvas.Clear(SKColors.Gray);
225
226 // Fetch and composite all tiles
227 await CompositeTilesAsync(canvas, tileRequests, origin, resolution);
228
229 // Create and return snapshot
230 return surface.Snapshot();
231 } // Surface automatically disposed here
232 }
233
237 public SKImage StitchRegion(
238 RectangleD region,
239 int level,
240 ZCT coordinate, double resolution, int pxwidth, int pxheight)
241 {
242 var tileRequests = CalculateTileRequests(
243 new PointD(region.X,region.Y),pxwidth, pxheight, level, coordinate, resolution);
244
245 int outputWidth = (int)Math.Ceiling(region.W);
246 int outputHeight = (int)Math.Ceiling(region.H);
247
248 using (var surface = SKSurface.Create(new SKImageInfo(outputWidth, outputHeight)))
249 {
250 var canvas = surface.Canvas;
251 canvas.Clear(SKColors.Transparent);
252
253 foreach (var request in tileRequests)
254 {
255 var tileImage = FetchTileSync(request);
256 if (tileImage != null)
257 {
258 DrawTileToCanvas(canvas,request, new AForge.PointD(region.X,region.Y), resolution, tileImage);
259 }
260 }
261
262 return surface.Snapshot();
263 }
264 }
265
266 #endregion
267
268 #region Tile Fetching and Caching
269
273 private async Task<SKImage> FetchTileAsync(TileRequest request, ZCT coordinate)
274 {
275 string key = request.GetCacheKey();
276
277 // 1. Check cache again to be sure
278 if (_tileCache.TryGetValue(key, out var cached)) return cached.Image;
279
280 // 2. Check if already being fetched
281 if (_pendingTileFetches.TryGetValue(key, out var pendingTask)) return await pendingTask;
282
283 // 3. Create the fetch task
284 var fetchTask = Task.Run(async () =>
285 {
286 try
287 {
288 byte[] data;
289 if (_isOpenSlide)
290 {
291 TileInfo tf = new TileInfo { Index = request.Index, Extent = request.Extent };
292 // OpenSlide usually takes Level, X-index, Y-index
293 data = _openSlideSource.GetTile(tf);
294 }
295 else
296 {
297 TileInfo tf = new TileInfo { Index = request.Index, Extent = request.Extent };
298 data = await FetchSlideTileAsync(tf, request.Coordinate).ConfigureAwait(false);
299 }
300
301 if (data == null || data.Length == 0) return null;
302
303 // Convert to Skia Image (using the shear-fix logic from before)
304 var levelResolution = CalculateResolutionFromLevel(request.Index.Level);
305 var image = ConvertTileDataToSKImage(data, request);
306
307 if (image != null)
308 {
309 var cachedTile = new CachedTile(image, DateTime.Now.ToString());
310 _tileCache.AddOrUpdate(key, cachedTile, (k, old) => cachedTile);
311 return image;
312 }
313 }
314 catch (Exception ex)
315 {
316 if (VerboseLogging) Console.WriteLine($"Fetch error for tile {key}: {ex.Message} + {ex.StackTrace}");
317 }
318 finally
319 {
320 _pendingTileFetches.TryRemove(key, out _);
321 }
322 return null;
323 });
324
325 _pendingTileFetches[key] = fetchTask;
326 return await fetchTask;
327 }
328
329 private bool ShouldComposeRgbTile()
330 {
331 if (_isOpenSlide || App.viewer?.Mode != ImageView.ViewMode.RGBImage)
332 return false;
333
334 var bioImage = _slideSource?.Image?.BioImage;
335 if (bioImage == null || bioImage.Channels == null || bioImage.Channels.Count <= 1)
336 return false;
337 if (bioImage.Resolutions == null || bioImage.Resolutions.Count == 0)
338 return false;
339
340 var pixelFormat = bioImage.Resolutions[0].PixelFormat;
341 return pixelFormat != AForge.PixelFormat.Format24bppRgb &&
342 pixelFormat != AForge.PixelFormat.Format48bppRgb;
343 }
344
345 private async Task<byte[]> FetchSlideTileAsync(TileInfo tileInfo, ZCT coordinate)
346 {
347 if (!ShouldComposeRgbTile())
348 return _slideSource.GetTile(tileInfo, coordinate);
349
350 var bioImage = _slideSource?.Image?.BioImage;
351 if (bioImage == null)
352 return _slideSource.GetTile(tileInfo, coordinate);
353
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);
358
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);
363 }
364
365 private static byte[] ComposeBgraTiles(byte[] redTile, byte[] greenTile, byte[] blueTile)
366 {
367 byte[] fallback = redTile ?? greenTile ?? blueTile;
368 if (fallback == null)
369 return null;
370
371 redTile ??= fallback;
372 greenTile ??= fallback;
373 blueTile ??= fallback;
374
375 int byteCount = Math.Min(redTile.Length, Math.Min(greenTile.Length, blueTile.Length));
376 byteCount -= byteCount % 4;
377 if (byteCount <= 0)
378 return null;
379
380 byte[] composed = new byte[byteCount];
381 for (int i = 0; i < byteCount; i += 4)
382 {
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))
388 a = byte.MaxValue;
389
390 composed[i] = b;
391 composed[i + 1] = g;
392 composed[i + 2] = r;
393 composed[i + 3] = a;
394 }
395
396 return composed;
397 }
398
399 private static byte ExtractIntensity(byte[] tile, int offset)
400 {
401 return (byte)Math.Max(tile[offset], Math.Max(tile[offset + 1], tile[offset + 2]));
402 }
403
407 private SKImage FetchTileSync(TileRequest request)
408 {
409 string cacheKey = request.GetCacheKey();
410
411 // Check cache
412 if (_tileCache.TryGetValue(cacheKey, out var cachedTile))
413 {
414 cachedTile.LastAccessed = DateTime.UtcNow;
415 return cachedTile.Image;
416 }
417
418 // Fetch directly
419 try
420 {
421 byte[] tileData = FetchTileDataSync(request);
422 if (tileData == null || tileData.Length == 0)
423 return null;
424
425 SKImage skImage = ConvertTileDataToSKImage(tileData, request);
426
427 if (skImage != null)
428 {
429 AddToCache(cacheKey, skImage);
430 }
431
432 return skImage;
433 }
434 catch (Exception ex)
435 {
436 if (VerboseLogging) Console.WriteLine($"Error fetching tile sync {cacheKey}: {ex.Message}");
437 return null;
438 }
439 }
440
444 private async Task<byte[]> FetchTileDataAsync(TileRequest request)
445 {
446 if (VerboseLogging)
447 {
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})");
450 }
451
452 try
453 {
454 if (_isOpenSlide)
455 {
456 // Use BruTile's GetTile method directly
457 byte[] tileData = _openSlideSource.GetTile(request.TileInfo);
458
459 if (tileData == null || tileData.Length == 0)
460 {
461 if (VerboseLogging) Console.WriteLine($" ERROR: OpenSlide returned null/empty tile data");
462 return null;
463 }
464
465 if (VerboseLogging) Console.WriteLine($" OpenSlide returned {tileData.Length} bytes");
466 return tileData;
467 }
468 else
469 {
470 // For BioLib SlideBase
471 var sliceInfo = new BioLib.SliceInfo(
472 request.Extent.MinX,
473 request.Extent.MinY,
474 request.Bounds.W,
475 request.Bounds.H,
476 CalculateResolutionFromLevel(request.Level),
477 request.Coordinate
478 );
479
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);
484
485 if (tileData == null || tileData.Length == 0)
486 {
487 if (VerboseLogging) Console.WriteLine($" ERROR: SlideBase returned null/empty tile data");
488 return null;
489 }
490
491 if (VerboseLogging) Console.WriteLine($" SlideBase returned {tileData.Length} bytes");
492 return tileData;
493 }
494 }
495 catch (Exception ex)
496 {
497 if (VerboseLogging)
498 {
499 Console.WriteLine($" EXCEPTION in FetchTileDataAsync: {ex.Message}");
500 Console.WriteLine($" {ex.StackTrace}");
501 }
502 return null;
503 }
504 }
508 private byte[] FetchTileDataSync(TileRequest request)
509 {
510 if (_isOpenSlide)
511 {
512 var sliceInfo = new OpenSlideGTK.SliceInfo(
513 request.Bounds.X,
514 request.Bounds.Y,
515 request.Bounds.W,
516 request.Bounds.H,
517 CalculateResolutionFromLevel(request.Level)
518 );
519
520 return _openSlideSource.GetSlice(sliceInfo, request.Coordinate, request.Level);
521 }
522 else
523 {
524 if (ShouldComposeRgbTile())
525 {
526 TileInfo tf = new TileInfo { Index = request.Index, Extent = request.Extent };
527 return FetchSlideTileAsync(tf, request.Coordinate).Result;
528 }
529
530 var sliceInfo = new BioLib.SliceInfo(
531 request.Bounds.X,
532 request.Bounds.Y,
533 request.Bounds.W,
534 request.Bounds.H,
535 CalculateResolutionFromLevel(request.Level),
536 request.Coordinate
537 );
538
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);
541
542 return _slideSource.GetSlice(sliceInfo, origin, size).Result;
543 }
544 }
545
546 #endregion
547
548 #region Tile Compositing
549
553 private async Task CompositeTilesAsync(
554 SKCanvas canvas,
555 List<TileRequest> tileRequests,
556 PointD origin,
557 double resolution)
558 {
559 if (tileRequests.Count == 0) return;
560
561 // Fetch all tiles in parallel
562 var tileTasks = tileRequests.Select(async req =>
563 {
564 var image = await FetchTileAsync(req, req.Coordinate);
565 return new { Request = req, Image = image };
566 }).ToArray();
567
568 var results = await Task.WhenAll(tileTasks);
569
570 using (var paint = new SKPaint { IsAntialias = false })
571 {
572 foreach (var result in results)
573 {
574 if (result.Image == null) continue;
575 DrawTileToCanvas(canvas, result.Request, origin, resolution, result.Image, paint);
576 }
577 }
578 }
582 private void DrawTileToCanvas(
583 SKCanvas canvas,
584 TileRequest request,
585 PointD viewportOrigin,
586 double resolution,
587 SKImage tileImage,
588 SKPaint paint = null)
589 {
590 if (tileImage == null) return;
591
592 // X: (Tile Left - Viewport Left) / Resolution
593 float destX = (float)((request.Extent.MinX - viewportOrigin.X) / resolution);
594
595 // Viewport origin is stored in positive-down image/stage space.
596 // BruTile extents remain in OSM world space, so convert tile top
597 // into the same positive-down convention here.
598 float destY = (float)((-request.Extent.MaxY - viewportOrigin.Y) / resolution);
599
600 // Calculate dimensions based on the Extent to ensure precision
601 float destW = (float)(request.Extent.Width / resolution);
602 float destH = (float)(request.Extent.Height / resolution);
603
604 var destRect = new SKRect(destX, destY, destX + destW, destY + destH);
605
606 // Use Sampling if provided in the class, otherwise use the paint object
607 if (paint != null)
608 canvas.DrawImage(tileImage, destRect, paint);
609 else
610 canvas.DrawImage(tileImage, destRect, Sampling);
611 }
615 private SKPaint CreateCompositePaint()
616 {
617 return new SKPaint
618 {
619 IsAntialias = true,
620 BlendMode = SKBlendMode.SrcOver
621 };
622 }
623
627 private SKPaint CreateTilePaint()
628 {
629 return new SKPaint
630 {
631 IsAntialias = false // Tiles should align perfectly
632 };
633 }
634
635 #endregion
636
637 #region Tile Calculation and Management
638
642 private List<TileRequest> CalculateTileRequests(PointD origin, int viewportWidth, int viewportHeight, int level, ZCT coordinate, double resolution)
643 {
644 // origin is the Top-Left of the viewport in positive-down image/stage
645 // space. Convert to BruTile's OSM world-space extent here.
646
647 double minX = origin.X;
648 double maxX = origin.X + (viewportWidth * resolution);
649 double maxY = -origin.Y;
650 double minY = -origin.Y - (viewportHeight * resolution);
651
652 // Create the extent (MinX, MinY, MaxX, MaxY)
653 var viewportExtent = new Extent(minX, minY, maxX, maxY);
654
655 IEnumerable<TileInfo> tileInfos;
656 if (_isOpenSlide)
657 {
658 tileInfos = _openSlideSource.Schema.GetTileInfos(viewportExtent, level);
659 }
660 else
661 {
662 tileInfos = _slideSource.Schema.GetTileInfos(viewportExtent, level);
663 }
664
665 var requests = new List<TileRequest>();
666 foreach (var info in tileInfos)
667 {
668 requests.Add(new TileRequest(info, coordinate));
669 }
670 return requests;
671 }
672 #endregion
673
674 #region Cache Management
675
679 private void AddToCache(string key, SKImage image)
680 {
681 var cachedTile = new CachedTile(image, key);
682
683 lock (_cacheLock)
684 {
685 _tileCache[key] = cachedTile;
686
687 // Check if we need to evict
688 long currentCacheSize = CalculateTotalCacheSize();
689
690 if (currentCacheSize > _maxCacheSize)
691 {
692 EvictLeastRecentlyUsedTiles(currentCacheSize - _maxCacheSize);
693 }
694 }
695 }
696
700 private void EvictLeastRecentlyUsedTiles(long bytesToFree)
701 {
702 var sortedTiles = new List<CachedTile>(_tileCache.Values);
703 sortedTiles.Sort((a, b) => a.LastAccessed.CompareTo(b.LastAccessed));
704
705 long freedBytes = 0;
706 foreach (var tile in sortedTiles)
707 {
708 if (freedBytes >= bytesToFree)
709 break;
710
711 if (_tileCache.TryRemove(tile.Key, out var removed))
712 {
713 freedBytes += removed.SizeBytes;
714 removed.Image?.Dispose();
715 }
716 }
717 }
718
722 private long CalculateTotalCacheSize()
723 {
724 long total = 0;
725 foreach (var tile in _tileCache.Values)
726 {
727 total += tile.SizeBytes;
728 }
729 return total;
730 }
731
735 public void ClearCache()
736 {
737 lock (_cacheLock)
738 {
739 foreach (var tile in _tileCache.Values)
740 {
741 tile.Image?.Dispose();
742 }
743 _tileCache.Clear();
744 }
745 }
746
747 #endregion
748
749 #region Utility Methods
750
754 // SkiaStitch.cs - Update this method
755 private SKImage ConvertTileDataToSKImage(byte[] tileData, TileRequest request)
756 {
757 try
758 {
759 // 1. Try standard decoding (JPEG/PNG/etc)
760 using (var data = SKData.CreateCopy(tileData))
761 {
762 var image = SKImage.FromEncodedData(data);
763 if (image != null) return image;
764 }
765
766 // 2. Fallback for Raw Pixels (The "Shear" Fix)
767 // Use the Schema's defined TileWidth/Height, NOT the calculated resolution width
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);
772
773 var info = new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
774 // If decode fails, create from raw RGBA data
775 // CRITICAL: Don't dispose SKData until image is disposed
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);
779
780 // Use release delegate to properly manage lifetime
781 return SKImage.FromPixels(pixmap, (address, context) =>
782 {
783 rawData?.Dispose();
784 }, null);
785 }
786 catch (Exception ex)
787 {
788 if (VerboseLogging) Console.WriteLine($"Error converting tile data: {ex.Message}");
789 return null;
790 }
791 }
795 private int CalculateLevelFromResolution(double resolution)
796 {
797 if (_isOpenSlide)
798 {
799 return OpenSlideGTK.TileUtil.GetLevel(_openSlideSource.Schema.Resolutions, resolution);
800 }
801 else
802 {
803 return OpenSlideGTK.TileUtil.GetLevel(_slideSource.Schema.Resolutions, resolution);
804 }
805 }
806
810 private double CalculateResolutionFromLevel(int level)
811 {
812 if (_isOpenSlide)
813 {
814 return _openSlideSource.Schema.Resolutions[level].UnitsPerPixel;
815 }
816 else
817 {
818 return _slideSource.Schema.Resolutions[level].UnitsPerPixel;
819 }
820 }
821
822 #endregion
823
824 #region Disposal
825 public void Dispose()
826 {
827 ClearCache();
828 _pendingTileFetches.Clear();;
829 }
830 #endregion
831 }
832}
SkiaSharp-based tile stitching pipeline for compositing large pyramidal images.
Definition SkiaStitch.cs:21
SkiaStitchingPipeline(SlideBase slideSource, int maxCacheSizeMB=512)
Initialize pipeline for BioLib slide source.
void ClearCache()
Clear entire cache.
SKImage StitchRegion(RectangleD region, int level, ZCT coordinate, double resolution, int pxwidth, int pxheight)
Stitch a specific region synchronously (for smaller areas)
async Task< SKImage > StitchViewportAsync(PointD origin, double resolution, ZCT coordinate, int viewportWidth=600, int viewportHeight=400)
Stitch tiles for a given viewport asynchronously.
SkiaStitchingPipeline(OpenSlideGTK.OpenSlideBase openSlideSource, int maxCacheSizeMB=512)
Initialize pipeline for OpenSlide source.
Definition ML.cs:17
Definition Zarr.cs:20