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
SKSlideRenderer.cs
1using SkiaSharp;
2using System;
3using System.Threading;
4using System.Threading.Tasks;
5using AForge;
6using BioLib;
7using SkiaSharp.Views.Gtk;
8
9namespace BioGTK
10{
15 public class SKSlideRenderer : IDisposable
16 {
17 #region Fields and Properties
18 private readonly SemaphoreSlim _renderSemaphore = new SemaphoreSlim(1, 1);
20 public bool LastRenderSkipped { get; private set; }
21 private SkiaStitchingPipeline _stitchingPipeline;
22 private bool _disposed;
23
24 // Current rendering state
25 private SKImage _currentRenderedImage;
26 private PointD _lastOrigin;
27 private double _lastResolution;
28 private ZCT _lastCoordinate;
29 private int _lastWidth;
30 private int _lastHeight;
31 private volatile int _renderStateVersion;
32
33 // Source tracking
34 private OpenSlideGTK.OpenSlideBase _openSlideSource;
35 private SlideBase _slideSource;
36 private bool _isOpenSlide;
37 private bool _hasSource;
38
39 public int MaxCacheSizeMB { get; set; } = 512;
40 public int TileSize { get; set; } = 256;
41 public SKSamplingOptions Sampling { get; set; } = SKSamplingOptions.Default;
42 public bool EnablePrefetch { get; set; } = true;
43
44 // State tracking
45 public bool IsRendering { get; private set; }
46 public bool HasValidImage => _currentRenderedImage != null;
47
48 #endregion
49
50 #region Constructor
51
55 public SKSlideRenderer(SKDrawingArea sk)
56 {
57 InitializeDefaults();
58 }
59
60 private void InitializeDefaults()
61 {
62 _lastOrigin = new PointD(0, 0);
63 _lastResolution = 1.0;
64 _lastCoordinate = new ZCT(0, 0, 0);
65 _lastWidth = 600;
66 _lastHeight = 400;
67 }
68
69 #endregion
70
71 #region Source Management
72
76 public void SetSource(OpenSlideGTK.OpenSlideBase openSlideSource)
77 {
78 if (openSlideSource == null)
79 throw new ArgumentNullException(nameof(openSlideSource));
80
81 ClearCurrentSource();
82
83 _openSlideSource = openSlideSource;
84 _isOpenSlide = true;
85 _hasSource = true;
86
87 InitializeStitchingPipeline();
88 }
89
93 public void SetSource(SlideBase slideSource)
94 {
95 if (slideSource == null)
96 throw new ArgumentNullException(nameof(slideSource));
97
98 ClearCurrentSource();
99
100 _slideSource = slideSource;
101 _isOpenSlide = false;
102 _hasSource = true;
103
104 InitializeStitchingPipeline();
105 }
106
110 private void InitializeStitchingPipeline()
111 {
112 _stitchingPipeline?.Dispose();
113
114 if (_isOpenSlide)
115 {
116 _stitchingPipeline = new SkiaStitchingPipeline(_openSlideSource, MaxCacheSizeMB);
117 }
118 else
119 {
120 _stitchingPipeline = new SkiaStitchingPipeline(_slideSource, MaxCacheSizeMB);
121 }
122
123 // Apply configuration
124 _stitchingPipeline.TileSize = TileSize;
125 //_stitchingPipeline.FilterQuality = FilterQuality;
126 _stitchingPipeline.EnablePrefetch = EnablePrefetch;
127 }
128
132 private void ClearCurrentSource()
133 {
134 Interlocked.Increment(ref _renderStateVersion);
135 _currentRenderedImage?.Dispose();
136 _currentRenderedImage = null;
137
138 _stitchingPipeline?.Dispose();
139 _stitchingPipeline = null;
140
141 _hasSource = false;
142 }
143
149 {
150 Interlocked.Increment(ref _renderStateVersion);
151 }
152
153 #endregion
154
155 #region Async View Update
156
160 public async Task UpdateViewAsync(
161 PointD origin,
162 double resolution,
163 ZCT coordinate,
164 int width = 600,
165 int height = 400)
166 {
167 if (_disposed || !_hasSource || _stitchingPipeline == null)
168 return;
169
170 // If a render is already in progress, record the skip and return.
171 // The finally block of the running render will re-schedule via RequestDeferredRender.
172 if (!_renderSemaphore.Wait(0))
173 {
174 LastRenderSkipped = true;
175 return;
176 }
177
178 // We hold the semaphore — clear the skip flag and run.
179 LastRenderSkipped = false;
180 int renderVersion = _renderStateVersion;
181
182 try
183 {
184 IsRendering = true;
185
186 // Store current parameters
187 _lastOrigin = origin;
188 _lastResolution = resolution;
189 _lastCoordinate = coordinate;
190 _lastWidth = width;
191 _lastHeight = height;
192
193 await RenderFullQualityAsync(origin, resolution, coordinate, width, height, renderVersion);
194 }
195 catch (Exception ex)
196 {
197 Console.WriteLine($"Error during view update: {ex.Message}");
198 }
199 finally
200 {
201 IsRendering = false;
202 _renderSemaphore.Release();
203
204 // If a request was skipped while we were running, reschedule so the
205 // view always ends up showing the latest coordinates.
206 if (!_disposed && LastRenderSkipped)
207 Gtk.Application.Invoke((s, a) => App.viewer?.RequestDeferredRender());
208 }
209 }
210
214 private async Task RenderLowResPreviewAsync(
215 PointD origin,
216 int width,
217 int height,
218 double resolution,
219 ZCT coordinate)
220 {
221 try
222 {
223 // Use a lower resolution for preview (e.g., 4x lower)
224 double previewResolution = resolution * 4.0;
225 int previewWidth = width / 2;
226 int previewHeight = height / 2;
227
228 // Create preview with stitching pipeline
229 var previewImage = await _stitchingPipeline.StitchViewportAsync(
230 origin,
231 previewResolution,
232 coordinate,
233 previewWidth,
234 previewHeight
235 );
236 /*
237 if (previewImage != null)
238 {
239 _lowResPreview?.Dispose();
240 _lowResPreview = previewImage;
241
242 // Request immediate redraw with preview
243 //RequestRedraw();
244 }
245 */
246 }
247 catch (Exception ex)
248 {
249 Console.WriteLine($"Preview render error: {ex.Message}");
250 }
251 }
252
256 private async Task RenderFullQualityAsync(
257 PointD origin,
258 double resolution,
259 ZCT coordinate,
260 int width = 600,
261 int height = 400,
262 int renderVersion = 0)
263 {
264 try
265 {
266 // Use stitching pipeline to create full quality composite
267 var stitchedImage = await _stitchingPipeline.StitchViewportAsync(
268 origin,
269 resolution,
270 coordinate,
271 width,
272 height
273 );
274
275 if (renderVersion != _renderStateVersion)
276 {
277 stitchedImage?.Dispose();
278 return;
279 }
280
281 if (stitchedImage != null)
282 {
283 _currentRenderedImage?.Dispose();
284 _currentRenderedImage = stitchedImage;
285
286 // Request redraw with new image
287 //RequestRedraw();
288 }
289 }
290 catch (Exception ex)
291 {
292 Console.WriteLine($"Full quality render error: {ex.Message}");
293 }
294 }
295
296 #endregion
297
298 #region Synchronous Rendering Methods
299
303 public SKImage RenderRegion(
304 RectangleD region,
305 int level,
306 ZCT coordinate, double resolution, int pxwidth, int pxheight)
307 {
308 if (!_hasSource || _stitchingPipeline == null)
309 return null;
310
311 return _stitchingPipeline.StitchRegion(region, level, coordinate, resolution, pxwidth, pxheight);
312 }
313
317 public SKImage RenderCurrentViewSync()
318 {
319 if (!_hasSource || _stitchingPipeline == null)
320 return null;
321
322 return _stitchingPipeline.StitchViewportAsync(
323 _lastOrigin,
324 _lastResolution,
325 _lastCoordinate,
326 _lastWidth,
327 _lastHeight
328 ).Result;
329 }
330
331 #endregion
332
333 #region Image Access for Rendering
334
339 public SKImage GetCurrentImage()
340 {
341 // Return full quality if available, otherwise preview
342 return _currentRenderedImage;
343 }
344
349 public void DrawToCanvas(SKCanvas canvas, int canvasWidth, int canvasHeight)
350 {
351 if (canvas == null)
352 return;
353
354 var image = GetCurrentImage();
355 if (image == null)
356 return;
357 try
358 {
359 using (var paint = new SKPaint
360 {
361 IsAntialias = true,
362 BlendMode = SKBlendMode.SrcOver
363 })
364 {
365 // Draw image to fill canvas
366 var destRect = new SKRect(0, 0, canvasWidth, canvasHeight);
367 canvas.DrawImage(image, destRect, paint);
368 }
369 }
370 catch (Exception ex)
371 {
372 Console.WriteLine($"Error drawing to canvas: {ex.Message}");
373 }
374 }
375
376 #endregion
377
378 #region Update Management
379
383 private bool NeedsUpdate(
384 PointD origin,
385 int width,
386 int height,
387 double resolution,
388 ZCT coordinate)
389 {
390 const double EPSILON = 0.0001;
391
392 return Math.Abs(origin.X - _lastOrigin.X) > EPSILON ||
393 Math.Abs(origin.Y - _lastOrigin.Y) > EPSILON ||
394 Math.Abs(resolution - _lastResolution) > EPSILON ||
395 width != _lastWidth ||
396 height != _lastHeight ||
397 coordinate.Z != _lastCoordinate.Z ||
398 coordinate.C != _lastCoordinate.C ||
399 coordinate.T != _lastCoordinate.T;
400 }
401
405 private bool ShouldRenderPreview(double resolution)
406 {
407 // Use preview for high-resolution views where full quality takes time
408 return resolution < 1.0; // Zoomed in significantly
409 }
410
414 private void RequestRedraw()
415 {
416 Gtk.Application.Invoke((sender, args) =>
417 {
418 App.viewer.RequestDeferredRender();
419 });
420 }
421
422 #endregion
423
424 #region Cache Management
425
429 public void ClearCache()
430 {
431 _stitchingPipeline?.ClearCache();
432 }
433
438 {
439 // Could be extended to expose actual stats from pipeline
440 return new CacheStatistics
441 {
442 IsEnabled = _stitchingPipeline != null,
443 MaxSizeMB = MaxCacheSizeMB,
444 TileSize = TileSize
445 };
446 }
447
448 public class CacheStatistics
449 {
450 public bool IsEnabled { get; set; }
451 public int MaxSizeMB { get; set; }
452 public int TileSize { get; set; }
453 }
454
455 #endregion
456
457 #region Configuration Updates
458
463 int? maxCacheSizeMB = null,
464 int? tileSize = null,
465 SKSamplingOptions? sampling = null,
466 bool? enablePrefetch = null,
467 bool? enableProgressiveRendering = null)
468 {
469 bool needsReinit = false;
470
471 if (maxCacheSizeMB.HasValue && maxCacheSizeMB.Value != MaxCacheSizeMB)
472 {
473 MaxCacheSizeMB = maxCacheSizeMB.Value;
474 needsReinit = true;
475 }
476
477 if (tileSize.HasValue && tileSize.Value != TileSize)
478 {
479 TileSize = tileSize.Value;
480 needsReinit = true;
481 }
482
483 // Reinitialize pipeline if structural changes were made
484 if (needsReinit && _hasSource)
485 {
486 InitializeStitchingPipeline();
487 }
488 }
489
490 #endregion
491
492 #region Disposal
493
494 public void Dispose()
495 {
496 if (_disposed)
497 return;
498
499 _disposed = true;
500 _currentRenderedImage?.Dispose();
501 _stitchingPipeline?.Dispose();
502 _currentRenderedImage = null;
503 _stitchingPipeline = null;
504 }
505
506 #endregion
507 }
508}
Manages slide rendering by coordinating between the SkiaStitchingPipeline and SlideGLArea Handles asy...
SKSlideRenderer(SKDrawingArea sk)
Initialize renderer for a specific GLArea widget.
async Task UpdateViewAsync(PointD origin, double resolution, ZCT coordinate, int width=600, int height=400)
Update the rendered view asynchronously - main entry point for rendering.
CacheStatistics GetCacheStatistics()
Get cache statistics for monitoring.
void InvalidateRenderState()
Marks any in-flight render as stale so the next viewport update can replace it without the old result...
void SetSource(OpenSlideGTK.OpenSlideBase openSlideSource)
Set OpenSlide source and initialize pipeline.
void ClearCache()
Clear the tile cache.
SKImage RenderRegion(RectangleD region, int level, ZCT coordinate, double resolution, int pxwidth, int pxheight)
Render a specific region synchronously (for exports, small areas)
void DrawToCanvas(SKCanvas canvas, int canvasWidth, int canvasHeight)
Draw the current image to a canvas with proper scaling Called during the Skia render callback from Sl...
void SetSource(SlideBase slideSource)
Set BioLib slide source and initialize pipeline.
void UpdateConfiguration(int? maxCacheSizeMB=null, int? tileSize=null, SKSamplingOptions? sampling=null, bool? enablePrefetch=null, bool? enableProgressiveRendering=null)
Update renderer configuration and reinitialize if needed.
SKImage RenderCurrentViewSync()
Render current viewport synchronously (blocking)
bool LastRenderSkipped
True if the last UpdateViewAsync call was skipped because a render was already in progress.
SKImage GetCurrentImage()
Get the current image for rendering - called by GLArea during draw Returns the best available image (...
SkiaSharp-based tile stitching pipeline for compositing large pyramidal images.
Definition SkiaStitch.cs:21
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.
Definition ML.cs:17
Definition Zarr.cs:20