BioImager  5.0.0
A .NET microscopy imaging application based on Bio library. Supports various microscopes by using imported libraries & GUI automation. Supports XInput game controllers to move stage, take images, run ImageJ macros on images or Bio C# scripts.
Loading...
Searching...
No Matches
SlideRenderer.cs
Go to the documentation of this file.
1using AForge;
2using BioImager;
3using BioLib;
4using BruTile;
5using OpenSlideGTK;
6using System;
7using System.Collections.Generic;
8using System.Linq;
9using System.Threading;
10using System.Threading.Tasks;
11
12namespace BioImager
13{
14 public class SlideRenderer
15 {
16 private readonly SlideGLArea _glArea;
17 private OpenSlideBase _openSlideBase;
18 private SlideBase _slideBase;
19 private bool _useOpenSlide;
20 private HashSet<TileIndex> _uploadedTiles = new();
21 private int _currentLevel = -1;
22 private bool _hasPendingView;
23 private PointD _pendingPyramidalOrigin;
24 private int _pendingViewportWidth;
25 private int _pendingViewportHeight;
26 private double _pendingResolution;
27 private ZCT _pendingCoordinate;
28 private readonly SemaphoreSlim _renderSemaphore = new(1, 1);
29 private readonly SemaphoreSlim _fetchSemaphore = new(6, 6);
30 private static readonly TimeSpan RemoteTileFetchTimeout = TimeSpan.FromSeconds(60);
31 private readonly HashSet<(TileIndex Index, int Version)> _pendingTileFetches = new();
32 private readonly object _pendingTileFetchLock = new();
33 private int _redrawQueued = 0;
34 private int _contentVersion;
35 private (int Z, int C, int T)? _contentCoordinate;
36 private BioImage? _rgbZarr;
37 private int[]? _rgbSelection;
38 private BioImage? _pendingRgbZarr;
39 private int[]? _pendingRgbSelection;
40
42 {
43 _glArea = glArea;
44 _glArea.GLReady += OnGlReady;
45 }
46
47 public void SetSource(OpenSlideBase source)
48 {
49 if (IsSource(source)) return;
50 _openSlideBase = source;
51 _slideBase = null;
52 _useOpenSlide = true;
53 ClearCache();
54 }
55
56 public void SetSource(SlideBase source)
57 {
58 if (IsSource(source)) return;
59 _slideBase = source;
60 _openSlideBase = null;
61 _useOpenSlide = false;
62 ClearCache();
63 }
64
65 public bool IsSource(OpenSlideBase source)
66 {
67 return _useOpenSlide && ReferenceEquals(_openSlideBase, source);
68 }
69
70 public bool IsSource(SlideBase source)
71 {
72 return !_useOpenSlide && ReferenceEquals(_slideBase, source);
73 }
74
75 public void ClearCache()
76 {
77 _contentVersion++;
78 _contentCoordinate = null;
79 _glArea.ClearTextureCache();
80 _uploadedTiles.Clear();
81 _currentLevel = -1;
82 lock (_pendingTileFetchLock)
83 {
84 _pendingTileFetches.Clear();
85 }
86 }
87
88 public async Task UpdateViewAsync(
89 PointD pyramidalOrigin,
90 int viewportWidth,
91 int viewportHeight,
92 double resolution,
93 ZCT coordinate,
94 BioImage? rgbZarr = null,
95 int[]? rgbSelection = null)
96 {
97 _pendingPyramidalOrigin = pyramidalOrigin;
98 _pendingViewportWidth = viewportWidth;
99 _pendingViewportHeight = viewportHeight;
100 _pendingResolution = resolution;
101 _pendingCoordinate = coordinate;
102 _pendingRgbZarr = rgbZarr;
103 _pendingRgbSelection = rgbSelection?.ToArray();
104 _hasPendingView = true;
105
106 if (_openSlideBase == null && _slideBase == null)
107 return;
108 if (viewportWidth <= 1 || viewportHeight <= 1 || resolution <= 0 || !double.IsFinite(resolution))
109 return;
110 if (!_glArea.IsGLReady)
111 return;
112 if (!_renderSemaphore.Wait(0))
113 {
114 return;
115 }
116
117 try
118 {
119 var schema = _useOpenSlide ? _openSlideBase.Schema : _slideBase.Schema;
120 int level = TileUtil.GetLevel(schema.Resolutions, resolution);
121 var levelRes = schema.Resolutions[level];
122 double levelUnitsPerPixel = levelRes.UnitsPerPixel;
123
124 var contentCoordinate = (coordinate.Z, coordinate.C, coordinate.T);
125 if (_contentCoordinate != contentCoordinate || !ReferenceEquals(_rgbZarr, rgbZarr) ||
126 !SelectionsEqual(_rgbSelection, rgbSelection) || level != _currentLevel)
127 {
128 // TileIndex alone does not distinguish planes or RGB selections.
129 ClearCache();
130 _contentCoordinate = contentCoordinate;
131 _rgbZarr = rgbZarr;
132 _rgbSelection = rgbSelection?.ToArray();
133 _currentLevel = level;
134 }
135 int contentVersion = _contentVersion;
136
137 double minX = pyramidalOrigin.X;
138 double width = viewportWidth * resolution;
139 double height = viewportHeight * resolution;
140 var viewportExtent = new Extent(minX, -pyramidalOrigin.Y - height, minX + width, -pyramidalOrigin.Y);
141
142 double tileMarginX = levelRes.TileWidth * levelUnitsPerPixel;
143 double tileMarginY = levelRes.TileHeight * levelUnitsPerPixel;
144 var fetchExtent = new Extent(
145 viewportExtent.MinX - tileMarginX,
146 viewportExtent.MinY - tileMarginY,
147 viewportExtent.MaxX + tileMarginX,
148 viewportExtent.MaxY + tileMarginY);
149
150 var tileInfos = schema.GetTileInfos(fetchExtent, level).ToList();
151 if (tileInfos.Count == 0)
152 {
153 var lookupExtent = BioLib.ExtentEx.WorldToPixelInvertedY(fetchExtent, levelUnitsPerPixel);
154 tileInfos = schema.GetTileInfos(lookupExtent, level).ToList();
155 }
156 var renderInfos = new List<TileRenderInfo>();
157 var pendingFetches = new List<(TileInfo Tile, Task<byte[]> FetchTask)>();
158 foreach (var tileInfo in tileInfos)
159 {
160 if (_glArea.HasTileTexture(tileInfo.Index))
161 {
162 renderInfos.Add(CalculateScreenPosition(
163 tileInfo,
164 pyramidalOrigin,
165 resolution,
166 level,
167 schema));
168 continue;
169 }
170
171 bool alreadyPending;
172 lock (_pendingTileFetchLock)
173 {
174 alreadyPending = !_pendingTileFetches.Add((tileInfo.Index, contentVersion));
175 }
176
177 if (!alreadyPending)
178 {
179 pendingFetches.Add((tileInfo, FetchTileAsync(tileInfo, level, coordinate, schema, rgbZarr, rgbSelection?.ToArray(), RemoteTileFetchTimeout)));
180 }
181 renderInfos.Add(CalculateScreenPosition(
182 tileInfo, pyramidalOrigin, resolution, level, schema));
183 }
184
185 foreach (var pending in pendingFetches)
186 {
187 _ = ProcessPendingFetchAsync(
188 pending.Tile,
189 pending.FetchTask,
190 pyramidalOrigin,
191 resolution,
192 level,
193 schema,
194 contentVersion,
195 rgbZarr != null);
196 }
197
198 _glArea.SetTilesToRender(renderInfos);
199 _glArea.RequestRedraw();
200 }
201 finally
202 {
203 _renderSemaphore.Release();
204 }
205 }
206
207 private void OnGlReady(object sender, EventArgs e)
208 {
209 if (!_hasPendingView)
210 return;
211
212 _ = _glArea.BeginInvoke(new System.Action(async () =>
213 {
214 try
215 {
216 await UpdateViewAsync(
217 _pendingPyramidalOrigin,
218 _pendingViewportWidth,
219 _pendingViewportHeight,
220 _pendingResolution,
221 _pendingCoordinate,
222 _pendingRgbZarr,
223 _pendingRgbSelection);
224 }
225 catch { }
226 }));
227 }
228
229 private async Task<byte[]> FetchTileAsync(TileInfo tileInfo, int level, ZCT coordinate,
230 ITileSchema schema, BioImage? rgbZarr, int[]? rgbSelection, TimeSpan timeout)
231 {
232 // Capture the source before awaiting: a tab can select another image while I/O is pending.
233 var openSource = _openSlideBase;
234 var slideSource = _slideBase;
235 try
236 {
237 using var cts = new CancellationTokenSource(timeout);
238 await _fetchSemaphore.WaitAsync(cts.Token);
239 try
240 {
241 byte[] data;
242 if (rgbZarr != null)
243 {
244 double units = schema.Resolutions[level].UnitsPerPixel;
245 int width = (int)Math.Round(tileInfo.Extent.Width / units);
246 int height = (int)Math.Round(tileInfo.Extent.Height / units);
247 data = await ZarrChannelView.ReadRgbTileAsync(rgbZarr, level,
248 tileInfo.Index.Col * schema.GetTileWidth(level),
249 tileInfo.Index.Row * schema.GetTileHeight(level),
250 width, height, coordinate, rgbSelection!).WaitAsync(cts.Token);
251 // SlideGLArea uploads BGRA; the direct Zarr reader returns RGBA.
252 for (int i = 0; i + 3 < data.Length; i += 4)
253 (data[i], data[i + 2]) = (data[i + 2], data[i]);
254 }
255 else
256 {
257 data = openSource != null
258 ? await openSource.GetTileAsync(tileInfo).WaitAsync(cts.Token)
259 : await slideSource.GetTileAsync(tileInfo, coordinate).WaitAsync(cts.Token);
260 if (data != null && rgbSelection != null &&
261 data.Length == schema.GetTileWidth(level) * schema.GetTileHeight(level) * 4)
262 {
263 data = (byte[])data.Clone();
264 for (int i = 0; i + 3 < data.Length; i += 4)
265 for (int color = 0; color < 3; color++)
266 if (rgbSelection[color] < 0) data[i + 2 - color] = 0;
267 }
268 }
269 return data;
270 }
271 finally { _fetchSemaphore.Release(); }
272 }
273 catch (Exception ex)
274 {
275 System.Diagnostics.Trace.WriteLine($"Could not fetch tile {tileInfo.Index}: {ex.Message}");
276 return null;
277 }
278 }
279
280 private async Task ProcessPendingFetchAsync(
281 TileInfo tile,
282 Task<byte[]> fetchTask,
283 PointD pyramidalOrigin,
284 double resolution,
285 int level,
286 ITileSchema schema,
287 int contentVersion,
288 bool directZarr)
289 {
290 try
291 {
292 byte[] tileData = await fetchTask.ConfigureAwait(false);
293 if (tileData == null || contentVersion != _contentVersion)
294 return;
295
296 double units = schema.Resolutions[level].UnitsPerPixel;
297 int tW = (int)Math.Round(tile.Extent.Width / units);
298 int tH = (int)Math.Round(tile.Extent.Height / units);
299 if (tW <= 0 || tH <= 0) return;
300 if (!directZarr)
301 {
302 int sourceWidth = schema.GetTileWidth(level);
303 int sourceHeight = schema.GetTileHeight(level);
304 // Edge TileInfo extents can be smaller than the padded source buffer.
305 int pixels = sourceWidth * sourceHeight;
306 int bytesPerPixel = pixels > 0 && tileData.Length % pixels == 0 ? tileData.Length / pixels : 0;
307 if ((tW != sourceWidth || tH != sourceHeight) && bytesPerPixel is >= 1 and <= 4)
308 {
309 byte[] clipped = new byte[checked(tW * tH * bytesPerPixel)];
310 int rowBytes = tW * bytesPerPixel;
311 for (int row = 0; row < tH; row++)
312 Buffer.BlockCopy(tileData, row * sourceWidth * bytesPerPixel, clipped, row * rowBytes, rowBytes);
313 tileData = clipped;
314 }
315 }
316
317 await RunOnUiThreadAsync(() =>
318 {
319 if (contentVersion != _contentVersion) return;
320 if (_glArea.UploadTileTexture(tile.Index, tileData, tW, tH))
321 {
322 _uploadedTiles.Add(tile.Index);
323 _glArea.RequestRedraw();
324 }
325 });
326 }
327 catch
328 {
329 }
330 finally
331 {
332 lock (_pendingTileFetchLock)
333 {
334 _pendingTileFetches.Remove((tile.Index, contentVersion));
335 }
336
337 if (Interlocked.Exchange(ref _redrawQueued, 1) == 0)
338 {
339 try
340 {
341 await RunOnUiThreadAsync(() => _glArea.RequestRedraw());
342 }
343 finally
344 {
345 Interlocked.Exchange(ref _redrawQueued, 0);
346 }
347 }
348 }
349 }
350
351 private Task RunOnUiThreadAsync(System.Action action)
352 {
353 if (_glArea.IsDisposed)
354 return Task.CompletedTask;
355
356 if (_glArea.InvokeRequired)
357 {
358 var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
359 try
360 {
361 _glArea.BeginInvoke(new System.Action(() =>
362 {
363 try
364 {
365 action();
366 tcs.TrySetResult(true);
367 }
368 catch (Exception ex)
369 {
370 tcs.TrySetException(ex);
371 }
372 }));
373 }
374 catch (Exception ex)
375 {
376 tcs.TrySetException(ex);
377 }
378
379 return tcs.Task;
380 }
381
382 action();
383 return Task.CompletedTask;
384 }
385
386 private TileRenderInfo CalculateScreenPosition(
387 TileInfo tile,
388 PointD pyramidalOrigin,
389 double viewResolution,
390 int level,
391 ITileSchema schema)
392 {
393 var tileExtent = tile.Extent;
394 var clippedExtent = new Extent(
395 Math.Max(tileExtent.MinX, schema.Extent.MinX),
396 Math.Max(tileExtent.MinY, schema.Extent.MinY),
397 Math.Min(tileExtent.MaxX, schema.Extent.MaxX),
398 Math.Min(tileExtent.MaxY, schema.Extent.MaxY));
399
400 if (clippedExtent.MaxX <= clippedExtent.MinX || clippedExtent.MaxY <= clippedExtent.MinY)
401 return new TileRenderInfo(tile.Index, -9999, -9999, 0, 0);
402
403 double originScreenX = pyramidalOrigin.X / viewResolution;
404 double originScreenY = pyramidalOrigin.Y / viewResolution;
405
406 float screenX = (float)(clippedExtent.MinX / viewResolution - originScreenX);
407 float screenY = (float)(-clippedExtent.MaxY / viewResolution - originScreenY);
408 float screenWidth = (float)((clippedExtent.MaxX - clippedExtent.MinX) / viewResolution);
409 float screenHeight = (float)((clippedExtent.MaxY - clippedExtent.MinY) / viewResolution);
410
411 return new TileRenderInfo(tile.Index, screenX, screenY, screenWidth, screenHeight);
412 }
413
414 private static bool SelectionsEqual(int[]? left, int[]? right) =>
415 ReferenceEquals(left, right) || (left != null && right != null && left.SequenceEqual(right));
416 public int CurrentLevel => _currentLevel;
417 public int CachedTextureCount => _uploadedTiles.Count;
418 }
419}
AForge.PointD PointD
BioLib.BioImage BioImage
Definition TabsView.cs:15
AForge.ZCT ZCT
Definition TabsView.cs:17
void SetSource(OpenSlideBase source)
bool IsSource(OpenSlideBase source)
SlideRenderer(SlideGLArea glArea)
async Task UpdateViewAsync(PointD pyramidalOrigin, int viewportWidth, int viewportHeight, double resolution, ZCT coordinate, BioImage? rgbZarr=null, int[]? rgbSelection=null)
void SetSource(SlideBase source)
bool IsSource(SlideBase source)
Information needed to render a single tile.