30 private Builder _builder;
31 public List<BioImage> Images =
new List<BioImage>();
32 private bool _updatingCoordinate =
false;
34 public void SetCoordinate(
int z,
int c,
int t)
36 if (SelectedImage ==
null)
38 if (_updatingCoordinate)
41 ZCT previousCoordinate = SelectedImage.Coordinate;
42 _updatingCoordinate =
true;
45 if (z > SelectedImage.SizeZ - 1 && SelectedImage.Type == BioImage.ImageType.well)
46 zBar.Value = zBar.Adjustment.Upper;
49 if (c > SelectedImage.SizeC - 1)
50 cBar.Value = cBar.Adjustment.Upper;
53 if (t > SelectedImage.SizeT - 1)
54 tBar.Value = tBar.Adjustment.Upper;
58 if (SelectedImage.Type == BioImage.ImageType.well)
59 SelectedImage.Coordinate =
new ZCT(0, (
int)cBar.Value, (
int)tBar.Value);
61 SelectedImage.Coordinate =
new ZCT((
int)zBar.Value, (
int)cBar.Value, (
int)tBar.Value);
65 _updatingCoordinate =
false;
68 bool coordinateChanged = SelectedImage.Coordinate != previousCoordinate;
69 if (coordinateChanged)
70 App.channelsTool?.RefreshStatisticsForCoordinate(SelectedImage, SelectedImage.Coordinate);
71 if (SelectedImage.isPyramidal && coordinateChanged)
73 PreparePyramidalCoordinateRender();
74 ForceNavigationRefresh();
75 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, z, c, t);
80 if (SelectedImage?.isPyramidal !=
true && View !=
null)
82 RequestViewportRefresh();
84 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, z, c, t);
87 private void PreparePyramidalCoordinateRender()
89 if (SelectedImage?.isPyramidal !=
true)
97 SelectedImage.InvalidateTileCache();
98 slideRenderer?.ClearCache(resetLevel:
false, cancelActiveFetches:
false);
100 SelectedImage.ZarrDisplayMax = 0;
103 private void RequestViewportRefresh(
bool scheduleIdleRetry =
true)
115 if (!scheduleIdleRetry)
120 if (_viewerCleanupDone || View ==
null)
138 if (SelectedImage ==
null)
140 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, SelectedImage.Coordinate.Z, SelectedImage.Coordinate.C, SelectedImage.Coordinate.T);
141 return SelectedImage.Coordinate;
144 private bool TrySelectVisibleMaskAt(
double worldX,
double worldY,
bool ctrlHeld)
146 if (SelectedImage?.Annotations ==
null)
149 PointD worldPoint =
new PointD(worldX, worldY);
153 var candidates =
new List<ROI>();
154 candidates.AddRange(SelectedImage.Annotations);
155 candidates.AddRange(GetZarrLabelOverlays(SelectedImage));
159 for (
int pass = 0; pass < 2 && hit ==
null; pass++)
161 for (
int i = candidates.Count - 1; i >= 0; i--)
163 ROI an = candidates[i];
164 if (an ==
null || an.type != ROI.Type.Mask || an.roiMask ==
null || an.coord != coord)
167 if (pass == 0 && !an.Selected)
169 if (pass == 1 && an.Selected)
172 if (
Tools.HitTestMask(an, worldPoint))
185 foreach (ROI other
in candidates)
189 other.Selected =
false;
190 other.selectedPoints?.Clear();
196 hit.selectedPoints?.Clear();
197 Tools.selectedROI = hit;
208 selectedIndex = Images.Count - 1;
209 if (im.Resolutions[0].SizeX < 1920 && im.Resolutions[0].SizeY < 1080)
213 View.WidthRequest = im.Resolutions[0].SizeX;
214 View.HeightRequest = im.Resolutions[0].SizeY;
218 View.WidthRequest = im.Resolutions[0].SizeX;
219 View.HeightRequest = im.Resolutions[0].SizeY;
227 App.roiManager?.UpdateAnnotationList();
228 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, im?.file ??
string.Empty);
231 public void SetZarrLabelOverlays(BioImage image, List<ROI> overlays,
string? keyOverride =
null)
236 string key =
string.IsNullOrWhiteSpace(keyOverride) ? GetOverlayKey(image) : keyOverride;
237 if (
string.IsNullOrWhiteSpace(key))
240 zarrLabelOverlays[key] = overlays ??
new List<ROI>();
241 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, image?.file ??
string.Empty, overlays?.Count ?? 0, keyOverride ??
string.Empty);
244 public void RefreshZarrLabelOverlays(
string? sourceOverride =
null)
246 RefreshZarrLabelOverlays(SelectedImage, sourceOverride);
249 public void RefreshZarrLabelOverlays(BioImage image,
string? sourceOverride =
null)
254 string source = sourceOverride ?? image.file;
255 if (
string.IsNullOrWhiteSpace(source))
256 source = image.Filename;
258 if (
string.IsNullOrWhiteSpace(source) ||
259 (!source.Contains(
".zarr", StringComparison.OrdinalIgnoreCase) && !Directory.Exists(source)))
264 var overlays =
BioLib.
Zarr.LoadLabelOverlaysAsync(image, source).GetAwaiter().GetResult();
265 if (overlays ==
null || overlays.Count == 0)
271 var zarrType = typeof(BioImage).Assembly.GetType(
"BioLib.Zarr");
272 var method = zarrType?.GetMethod(
274 System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
277 var result = method.Invoke(
null,
new object[] { image, source }) as List<ROI>;
278 if (result !=
null && result.Count > 0)
282 if ((overlays ==
null || overlays.Count == 0) && image.Annotations !=
null)
284 var existing = image.Annotations.Where(IsZarrLabelRoi).ToList();
285 if (existing.Count > 0)
289 SetZarrLabelOverlays(image, overlays);
290 SetZarrLabelOverlays(image, overlays, source);
291 SyncZarrLabelAnnotations(image, overlays);
292 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, source);
299 private List<ROI> GetZarrLabelOverlays(BioImage image)
301 string key = GetOverlayKey(image);
302 if (
string.IsNullOrWhiteSpace(key))
303 return new List<ROI>();
305 if (zarrLabelOverlays.TryGetValue(key, out var overlays) && overlays !=
null && overlays.Count > 0)
308 if (!zarrLabelLoadAttempted.Contains(key))
310 zarrLabelLoadAttempted.Add(key);
311 RefreshZarrLabelOverlays(image, key);
312 if (zarrLabelOverlays.TryGetValue(key, out overlays) && overlays !=
null)
316 if (image?.Annotations !=
null)
318 var synced = image.Annotations.Where(IsZarrLabelRoi).ToList();
319 if (synced.Count > 0)
323 return new List<ROI>();
326 public List<ROI> GetZarrLabelOverlaysForImage(BioImage image)
327 =>
new List<ROI>(GetZarrLabelOverlays(image));
329 private static bool IsZarrLabelRoi(ROI roi)
331 return roi !=
null &&
332 !
string.IsNullOrWhiteSpace(roi.roiID) &&
333 roi.roiID.StartsWith(
"zarr-label:", StringComparison.OrdinalIgnoreCase);
336 private void SyncZarrLabelAnnotations(BioImage image, List<ROI> overlays)
341 image.Annotations.RemoveAll(IsZarrLabelRoi);
343 if (overlays ==
null || overlays.Count == 0)
346 foreach (var overlay
in overlays)
348 if (!IsZarrLabelRoi(overlay))
351 image.Annotations.Add(overlay);
355 private static string GetOverlayKey(BioImage image)
360 if (!
string.IsNullOrWhiteSpace(image.file))
363 return image.Filename ??
string.Empty;
366 double pxWmicron = 5;
367 double pxHmicron = 5;
369 public double PxWmicron
373 if (SelectedImage ==
null)
375 if (SelectedImage.Type == BioImage.ImageType.pyramidal)
378 if (_openSlideBase?.Schema?.Resolutions !=
null && _openSlideBase.Schema.Resolutions.Count > 0)
380 int lev = OpenSlideGTK.TileUtil.GetLevel(_openSlideBase.Schema.Resolutions, Resolution);
381 lev = Math.Clamp(lev, 0, _openSlideBase.Schema.Resolutions.Count - 1);
382 return _openSlideBase.Schema.Resolutions[lev].UnitsPerPixel * pxWmicron;
387 if (_slideBase?.Schema?.Resolutions !=
null && _slideBase.Schema.Resolutions.Count > 0)
389 int lev = OpenSlideGTK.TileUtil.GetLevel(_slideBase.Schema.Resolutions, Resolution);
390 lev = Math.Clamp(lev, 0, _slideBase.Schema.Resolutions.Count - 1);
391 return _slideBase.Schema.Resolutions[lev].UnitsPerPixel * pxWmicron;
402 public double PxHmicron
406 if (SelectedImage ==
null)
408 if (SelectedImage.Type == BioImage.ImageType.pyramidal)
411 if (_openSlideBase?.Schema?.Resolutions !=
null && _openSlideBase.Schema.Resolutions.Count > 0)
413 int lev = OpenSlideGTK.TileUtil.GetLevel(_openSlideBase.Schema.Resolutions, Resolution);
414 lev = Math.Clamp(lev, 0, _openSlideBase.Schema.Resolutions.Count - 1);
415 return _openSlideBase.Schema.Resolutions[lev].UnitsPerPixel * pxHmicron;
420 if (_slideBase?.Schema?.Resolutions !=
null && _slideBase.Schema.Resolutions.Count > 0)
422 int lev = OpenSlideGTK.TileUtil.GetLevel(_slideBase.Schema.Resolutions, Resolution);
423 lev = Math.Clamp(lev, 0, _slideBase.Schema.Resolutions.Count - 1);
424 return _slideBase.Schema.Resolutions[lev].UnitsPerPixel * pxHmicron;
434 bool allowNavigation =
true;
435 public bool AllowNavigation
437 get {
return allowNavigation; }
438 set { allowNavigation = value; }
441 public bool ShowMasks {
get;
set; } =
true;
442 public bool ShowOverview {
get;
set; } =
true;
443 private readonly Dictionary<string, List<ROI>> zarrLabelOverlays =
new Dictionary<string, List<ROI>>();
444 private readonly HashSet<string> zarrLabelLoadAttempted =
new HashSet<string>();
446 public static AForge.Bitmap SelectedBuffer
450 int ind = SelectedImage.GetFrameIndex(SelectedImage.Coordinate.Z, SelectedImage.Coordinate.C, SelectedImage.Coordinate.T);
451 return SelectedImage.Buffers[ind];
454 int selectedIndex = 0;
455 public int SelectedIndex
459 if(selectedIndex >= Images.Count)
460 selectedIndex = Images.Count - 1;
461 return selectedIndex;
465 if (value >= 0 && value < Images.Count)
467 selectedIndex = value;
468 Images[selectedIndex] = SelectedImage;
473 public static List<ROI> selectedAnnotations
477 List<ROI> rois =
new List<ROI>();
478 foreach (var item
in SelectedImage.Annotations)
480 if (item !=
null && item.Selected)
483 if (App.viewer !=
null)
485 foreach (var item
in App.viewer.GetZarrLabelOverlaysForImage(SelectedImage))
487 if (item !=
null && item.Selected)
494#pragma warning disable 649
496 private Gtk.Scale zBar;
498 private Gtk.Scale tBar;
500 private Gtk.Scale cBar;
502 private Gtk.Label statusLabel;
504 private Gtk.Stack rgbStack;
506 private Gtk.Grid grid;
508 private Gtk.Box rgbBox;
510 private Gtk.Box controlsBox;
512 public Gtk.Box mainBox;
514 private Gtk.ComboBox rBox;
516 private Gtk.ComboBox gBox;
518 private Gtk.ComboBox bBox;
520 private Gtk.Scrollbar hScroll;
522 private Gtk.Scrollbar vScroll;
524 public Menu contextMenu;
526 private MenuItem goToOriginMenu;
528 private MenuItem goToImageMenu;
530 private MenuItem overView;
532 private MenuItem roi;
534 private Menu roiMenu;
536 private MenuItem roiShow;
538 private MenuItem roiHide;
540 private MenuItem roiDelete;
542 private MenuItem roiID;
544 private MenuItem copy;
546 private MenuItem paste;
548 private MenuItem draw;
550 private MenuItem fill;
552 private Menu barMenu;
554 private MenuItem play;
556 private MenuItem stop;
558 private MenuItem playSpeed;
560 private MenuItem setValueRange;
562 private MenuItem loop;
563#pragma warning restore 649
567 private SKDrawingArea sk =
new SKDrawingArea();
568 private Widget currentView;
570 public SlideGLArea glArea;
571 public SlideRenderer slideRenderer;
572 public SKSlideRenderer sKSlideRenderer;
573 public GLWindow window;
574 public static bool MacOS =
false;
576 #region Constructors / Destructors
587 string gladePath = System.IO.Path.Combine(
588 System.IO.Path.GetDirectoryName(Environment.ProcessPath)!,
591 if (App.tools ==
null)
593 Builder builder =
new Builder();
594 builder.AddFromFile(gladePath);
596 var imageViewObj = builder.GetObject(
"imageView");
598 v.Title = bm.Filename;
605 protected ImageView(Builder builder, IntPtr handle, BioImage im) : base(handle)
609 builder.Autoconnect(
this);
610 var gameWindowSettings =
new GameWindowSettings()
612 UpdateFrequency = 60,
614 NativeWindowSettings nativeSettings;
616 if (OperatingSystem.IsMacOS())
620 View =
new SKDrawingArea();
621 sKSlideRenderer =
new SKSlideRenderer(sk);
623 else if (OperatingSystem.IsLinux())
625 nativeSettings =
new NativeWindowSettings()
627 ClientSize =
new Vector2i(600, 400),
628 Location =
new Vector2i(0, 0),
629 Title =
"OpenGL Context",
630 StartVisible =
false,
631 API = ContextAPI.OpenGL,
632 Profile = ContextProfile.Core,
633 APIVersion =
new Version(3, 3)
636 Environment.SetEnvironmentVariable(
"GDK_BACKEND",
"x11");
637 Environment.SetEnvironmentVariable(
"GTK_BACKEND",
"x11");
640 window =
new GLWindow(gameWindowSettings, nativeSettings);
641 View =
new SlideGLArea();
642 ((SlideGLArea)View).OnSkiaRender += RenderSkia;
643 slideRenderer =
new SlideRenderer((SlideGLArea)View);
647 View =
new SlideArea();
648 ((SlideArea)View).OnSkiaRender += RenderSkia;
650 GLFWProvider.SetErrorCallback((error, description) =>
655 else if (OperatingSystem.IsWindows())
657 nativeSettings =
new NativeWindowSettings()
659 ClientSize =
new Vector2i(600, 400),
660 Location =
new Vector2i(0, 0),
661 Title =
"OpenGL Context",
662 StartVisible =
false,
663 API = ContextAPI.OpenGL,
664 Profile = ContextProfile.Core,
665 APIVersion =
new Version(3, 3)
669 window =
new GLWindow(gameWindowSettings, nativeSettings);
670 View =
new SlideGLArea();
671 ((SlideGLArea)View).OnSkiaRender += RenderSkia;
672 slideRenderer =
new SlideRenderer((SlideGLArea)View);
676 View =
new SlideArea();
677 ((SlideArea)View).OnSkiaRender += RenderSkia;
679 GLFWProvider.SetErrorCallback((error, description) =>
689 grid.Attach(View, 0, 0, 1, 1);
694 if (im.OpenSlideBase !=
null)
696 sKSlideRenderer.SetSource(im.OpenSlideBase);
698 else if (im.SlideBase !=
null)
700 sKSlideRenderer.SetSource(im.SlideBase);
707 if (slideRenderer !=
null && im.OpenSlideBase !=
null)
709 slideRenderer.SetSource(im.OpenSlideBase);
711 else if (slideRenderer !=
null && im.SlideBase !=
null)
713 slideRenderer.SetSource(im.SlideBase);
718 grid.Attach(View,0,0,1,1);
723 View.WidthRequest = 600;
724 View.HeightRequest = 400;
725 grid.Attach(View, 0, 0, 1, 1);
733 View.WidthRequest = 600;
734 View.HeightRequest = 400;
742 View.WidthRequest = 600;
743 View.HeightRequest = 400;
746 roi.Submenu = roiMenu;
747 UpdateRoiMenuState();
751 pxWmicron = SelectedImage.PhysicalSizeX;
752 pxHmicron = SelectedImage.PhysicalSizeY;
754 EnsureInitialPyramidalRender();
757 Function.InitializeContextMenu();
758 this.Scale =
new SizeF(1, 1);
760 var rendererr =
new CellRendererText();
761 rBox.PackStart(rendererr,
false);
762 rBox.AddAttribute(rendererr,
"text", 0);
763 var rendererg =
new CellRendererText();
764 gBox.PackStart(rendererg,
false);
765 gBox.AddAttribute(rendererg,
"text", 0);
766 var rendererb =
new CellRendererText();
767 bBox.PackStart(rendererb,
false);
768 bBox.AddAttribute(rendererb,
"text", 0);
771 Gtk.Application.Invoke((s, e) =>
773 if (SelectedImage !=
null)
828 private void RenderSkia(SKCanvas canvas,
int width,
int height)
833 canvas.Scale(ScaleFactor);
835 var paint =
new SKPaint
837 Color = SKColors.Gray,
838 BlendMode = SKBlendMode.SrcOver,
840 Style = SKPaintStyle.Fill
844 foreach (BioImage im
in Images)
847 List<ROI> rois = BuildRenderableRois(im);
850 RectangleD rp =
ToScreenSpace(
new RectangleD(im.StageSizeX * im.PhysicalSizeX, im.StageSizeX * im.PhysicalSizeY, im.SizeX * im.PhysicalSizeX, im.SizeY * im.PhysicalSizeY));
852 canvas.DrawImage(SKImages[i], ToRectangle((
float)rp.X, (
float)rp.Y, (
float)rp.W, (
float)rp.H), paint);
855 RenderSkiaAnnotations(canvas, width, height, im);
862 EnsureOverviewCurrent();
863 if (overviewImage !=
null && ShowOverview)
867 Color = SKColors.Gray,
868 BlendMode = SKBlendMode.SrcOver,
870 Style = SKPaintStyle.Fill
873 canvas.DrawImage(overviewImage, 0, 0, paint);
874 paint.Style = SKPaintStyle.Stroke;
875 canvas.DrawRect(0, 0, overview.Width, overview.Height, paint);
877 else if (overviewImage ==
null && ShowOverview)
879 paint.Style = SKPaintStyle.Stroke;
880 paint.Color = SKColors.Gray;
881 paint.StrokeWidth = 2;
882 canvas.DrawRect(0, 0, overview.Width, overview.Height, paint);
884 SKImageInfo inf =
new SKImageInfo(width, height);
885 if(SelectedImage!=
null)
886 if (SelectedImage.isPyramidal)
887 DrawViewportRectangle(canvas, paint);
890 Plugins.Render(
this,
new SkiaSharp.Views.Desktop.SKPaintSurfaceEventArgs(canvas.Surface, inf));
896 Console.WriteLine($
"Annotation render error: {ex.Message}");
899 private PointD ToScreenSpaceForImage(BioImage image, PointD p)
901 if (image !=
null && image.isPyramidal)
903 double resolution = image.Resolution > 0 ? image.Resolution : 1;
904 PointD origin = image.PyramidalOrigin;
905 return new PointD((p.X - origin.X) / resolution, (p.Y - origin.Y) / resolution);
908 double dx = (pxWmicron * (Origin.X)) * Scale.Width;
909 double dy = (pxHmicron * (Origin.Y)) * Scale.Height;
910 return new PointD((PxWmicron * p.X * Scale.Width + dx), (PxHmicron * p.Y * Scale.Height + dy));
913 private RectangleD ToScreenRectForImage(BioImage image,
double x,
double y,
double w,
double h)
915 if (image !=
null && image.isPyramidal)
917 double resolution = image.Resolution > 0 ? image.Resolution : 1;
918 PointD origin = image.PyramidalOrigin;
919 return new RectangleD(
920 (x - origin.X) / resolution,
921 (y - origin.Y) / resolution,
926 double dx = (pxWmicron * (Origin.X)) * Scale.Width;
927 double dy = (pxHmicron * (Origin.Y)) * Scale.Height;
928 return new RectangleD(
929 (PxWmicron * x * Scale.Width + dx),
930 (PxHmicron * y * Scale.Height + dy),
931 (PxWmicron * w * Scale.Width),
932 (PxHmicron * h * Scale.Height));
935 private void RenderSkiaAnnotations(SKCanvas canvas,
int width,
int height, BioImage im)
940 canvas.Scale(ScaleFactor);
942 var paint =
new SKPaint
944 Color = SKColors.Gray,
945 BlendMode = SKBlendMode.SrcOver,
947 Style = SKPaintStyle.Fill
951 List<ROI> rois = BuildRenderableRois(im);
953 ZCT co = im?.Coordinate ??
new ZCT();
956 List<ROI> selectedMasks =
new List<ROI>();
957 foreach (ROI an
in rois)
961 if (Mode == ViewMode.RGBImage)
963 if (!showRROIs && an.coord.C == 0)
965 if (!showGROIs && an.coord.C == 1)
967 if (!showBROIs && an.coord.C == 2)
972 paint.Color = SKColors.Magenta;
975 paint.Color =
new SKColor(an.strokeColor.R, an.strokeColor.G, an.strokeColor.B);
976 paint.StrokeWidth = (float)an.strokeWidth;
977 paint.Style = SKPaintStyle.Stroke;
978 PointF pc =
new PointF((
float)(an.BoundingBox.X + (an.BoundingBox.W / 2)), (
float)(an.BoundingBox.Y + (an.BoundingBox.H / 2)));
979 float widths = ROI.selectBoxSize * (float)Resolution;
981 if (an.type == ROI.Type.Mask && ShowMasks && an.coord == co)
983 if (an.roiMask ==
null)
987 selectedMasks.Add(an);
990 paint.Style = SKPaintStyle.Fill;
991 RectangleD p = ToScreenRectForImage(im, an.X, an.Y, an.W, an.H);
992 if (!IsScreenRectVisible(p, width, height))
994 const byte maskOverlayAlpha = 96;
995 using SKImage sim = MaskToSKImage(an.roiMask, Color.FromArgb(maskOverlayAlpha, an.fillColor.R, an.fillColor.G, an.fillColor.B), maskOverlayAlpha);
996 canvas.DrawImage(sim, ToRectangle((
float)p.X, (
float)p.Y, (
float)p.W, (
float)p.H), paint);
1000 if (an.type == ROI.Type.Point)
1003 canvas.DrawCircle((
float)r1.X, (
float)r1.Y, 3, paint);
1006 if (an.type == ROI.Type.Line)
1008 for (
int p = 0; p < an.Points.Count - 1; p++)
1010 PointD p1 = ToScreenSpaceForImage(im, an.Points[p]);
1011 PointD p2 = ToScreenSpaceForImage(im, an.Points[p + 1]);
1012 canvas.DrawLine(
new SKPoint((
float)p1.X, (
float)p1.Y),
new SKPoint((
float)p2.X, (
float)p2.Y), paint);
1016 if (an.type == ROI.Type.Rectangle)
1018 RectangleD rectt = ToScreenRectForImage(im, an.BoundingBox.X, an.BoundingBox.Y, an.BoundingBox.W, an.BoundingBox.H);
1019 canvas.DrawRect((
float)rectt.X, (
float)rectt.Y, (
float)rectt.W, (
float)rectt.H, paint);
1022 if (an.type == ROI.Type.Ellipse)
1024 RectangleD rect = ToScreenRectForImage(im, an.BoundingBox.X, an.BoundingBox.Y, an.BoundingBox.W, an.BoundingBox.H);
1025 canvas.DrawOval((
float)rect.X, (
float)rect.Y, (
float)rect.W / 2, (
float)rect.H / 2, paint);
1028 if ((an.type == ROI.Type.Polygon && an.closed))
1030 for (
int p = 0; p < an.Points.Count - 1; p++)
1032 PointD p1 = ToScreenSpaceForImage(im, an.Points[p]);
1033 PointD p2 = ToScreenSpaceForImage(im, an.Points[p + 1]);
1034 canvas.DrawLine(
new SKPoint((
float)p1.X, (
float)p1.Y),
new SKPoint((
float)p2.X, (
float)p2.Y), paint);
1036 PointD pp1 = ToScreenSpaceForImage(im, an.Points[0]);
1037 PointD pp2 = ToScreenSpaceForImage(im, an.Points[an.Points.Count - 1]);
1038 canvas.DrawLine(
new SKPoint((
float)pp1.X, (
float)pp1.Y),
new SKPoint((
float)pp2.X, (
float)pp2.Y), paint);
1041 if ((an.type == ROI.Type.Polygon && !an.closed) || an.type == ROI.Type.Polyline)
1043 for (
int p = 0; p < an.Points.Count - 1; p++)
1045 PointD p1 = ToScreenSpaceForImage(im, an.Points[p]);
1046 PointD p2 = ToScreenSpaceForImage(im, an.Points[p + 1]);
1047 canvas.DrawLine(
new SKPoint((
float)p1.X, (
float)p1.Y),
new SKPoint((
float)p2.X, (
float)p2.Y), paint);
1051 if (an.type == ROI.Type.Freeform)
1053 for (
int p = 0; p < an.Points.Count - 1; p++)
1055 PointD p1 = ToScreenSpaceForImage(im, an.Points[p]);
1056 PointD p2 = ToScreenSpaceForImage(im, an.Points[p + 1]);
1057 canvas.DrawLine(
new SKPoint((
float)p1.X, (
float)p1.Y),
new SKPoint((
float)p2.X, (
float)p2.Y), paint);
1059 PointD pp1 = ToScreenSpaceForImage(im, an.Points[0]);
1060 PointD pp2 = ToScreenSpaceForImage(im, an.Points[an.Points.Count - 1]);
1061 canvas.DrawLine(
new SKPoint((
float)pp1.X, (
float)pp1.Y),
new SKPoint((
float)pp2.X, (
float)pp2.Y), paint);
1064 if (an.type == ROI.Type.Label)
1066 PointD p = ToScreenSpaceForImage(im, an.Point);
1067 canvas.DrawText(an.Text, (
float)p.X, (
float)p.Y,
new SKFont(SKTypeface.Default, an.fontSize, 1, 0), paint);
1072 RectangleD p = ToScreenRectForImage(im, an.Point.X, an.Point.Y, 1, 1);
1073 canvas.DrawText(an.Text, (
float)p.X, (
float)p.Y,
new SKFont(SKTypeface.Default, an.fontSize, 1, 0), paint);
1075 if (
ROIManager.showBounds && an.type != ROI.Type.Rectangle && an.type != ROI.Type.Mask && an.type != ROI.Type.Label)
1077 RectangleD rrf = ToScreenRectForImage(im, an.BoundingBox.X, an.BoundingBox.Y, an.BoundingBox.W, an.BoundingBox.H);
1078 canvas.DrawRect((
float)rrf.X, (
float)rrf.Y, (
float)rrf.W, (
float)rrf.H, paint);
1080 paint.Color = SKColors.Red;
1081 if (!(an.type == ROI.Type.Freeform && !an.Selected) && an.type != ROI.Type.Mask)
1082 foreach (RectangleD re
in an.GetSelectBoxes(1))
1084 RectangleD recd = ToScreenRectForImage(im, re.X, re.Y, re.W, re.H);
1085 canvas.DrawRect((
float)recd.X, (
float)recd.Y, (
float)recd.W, (
float)recd.H, paint);
1087 if (an.type != ROI.Type.Mask)
1090 List<RectangleD> rects =
new List<RectangleD>();
1091 RectangleD[] sels = an.GetSelectBoxes(widths);
1092 for (
int p = 0; p < an.selectedPoints.Count; p++)
1094 if (an.selectedPoints[p] < an.GetPointCount())
1096 rects.Add(sels[an.selectedPoints[p]]);
1100 paint.Color = SKColors.Blue;
1101 if (rects.Count > 0)
1104 foreach (RectangleD re
in an.GetSelectBoxes(1))
1106 RectangleD recd = ToScreenRectForImage(im, re.X, re.Y, re.W, re.H);
1107 if (an.selectedPoints.Contains(ind))
1109 canvas.DrawRect((
float)recd.X, (
float)recd.Y, (
float)recd.W, (
float)recd.H, paint);
1118 foreach (ROI an
in selectedMasks)
1120 if (an ==
null || an.roiMask ==
null)
1122 paint.Style = SKPaintStyle.Fill;
1123 RectangleD p = ToScreenRectForImage(im, an.X, an.Y, an.W, an.H);
1124 if (!IsScreenRectVisible(p, width, height))
1126 using SKImage sim = MaskToSKImage(an.roiMask, Color.FromArgb(220, 0, 120, 255), 220);
1127 canvas.DrawImage(sim, ToRectangle((
float)p.X, (
float)p.Y, (
float)p.W, (
float)p.H), paint);
1128 paint.Style = SKPaintStyle.Stroke;
1129 paint.Color = SKColors.Blue;
1130 paint.StrokeWidth = 2;
1131 canvas.DrawRect(ToRectangle((
float)p.X, (
float)p.Y, (
float)p.W, (
float)p.H), paint);
1135 SKImageInfo inf =
new SKImageInfo(width, height);
1137 Plugins.Render(
this,
new SkiaSharp.Views.Desktop.SKPaintSurfaceEventArgs(canvas.Surface, inf));
1141 catch (Exception ex)
1143 Console.WriteLine($
"Annotation render error: {ex.Message}");
1147 private List<ROI> BuildRenderableRois(BioImage image)
1150 return new List<ROI>();
1152 List<ROI> rois = image?.Annotations?.ToList() ??
new List<ROI>();
1153 List<ROI> overlays = GetZarrLabelOverlays(image);
1154 if (overlays.Count == 0)
1157 HashSet<ROI> existing =
new HashSet<ROI>(rois.Where(r => r !=
null));
1158 foreach (ROI overlay
in overlays)
1160 if (overlay !=
null && existing.Add(overlay))
1166 private static bool IsScreenRectVisible(RectangleD rect,
int width,
int height)
1168 if (rect.W <= 0 || rect.H <= 0 || width <= 0 || height <= 0)
1171 return rect.X < width &&
1173 rect.X + rect.W > 0 &&
1174 rect.Y + rect.H > 0;
1176 public static SKImage BitmapToSKImage(Bitmap bmp)
1178 var rect =
new Rectangle(0, 0, bmp.Width, bmp.Height);
1180 var data = bmp.LockBits(
1182 ImageLockMode.ReadOnly,
1183 AForge.PixelFormat.Format32bppArgb);
1187 var info =
new SKImageInfo(
1190 SKColorType.Bgra8888,
1191 SKAlphaType.Premul);
1193 var pixmap =
new SKPixmap(info, data.Scan0, data.Stride);
1195 return SKImage.FromPixels(pixmap);
1199 bmp.UnlockBits(data);
1203 private static SKImage MaskToSKImage(ROI.Mask mask, Color color,
byte alpha)
1205 if (mask ==
null || mask.Width <= 0 || mask.Height <= 0)
1206 return SKImage.FromBitmap(
new SKBitmap(1, 1));
1208 byte[] values = mask.GetBytes();
1209 int width = mask.Width;
1210 int height = mask.Height;
1211 int pixelCount = width * height;
1212 byte[] bgra =
new byte[pixelCount * 4];
1213 int count = Math.Min(pixelCount, values?.Length ?? 0);
1215 for (
int i = 0; i < count; i++)
1221 bgra[d + 0] = color.B;
1222 bgra[d + 1] = color.G;
1223 bgra[d + 2] = color.R;
1224 bgra[d + 3] = alpha;
1227 var info =
new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Unpremul);
1228 var data = SKData.CreateCopy(bgra);
1229 var pixmap =
new SKPixmap(info, data.Data, info.RowBytes);
1230 return SKImage.FromPixels(pixmap, (address, context) => data.Dispose(),
null);
1236 private void DrawOverview(SKCanvas canvas, SKPaint paint)
1238 var ims = BitmapToSKImage(overviewImage.GetImageRGBA(overviewImage.LittleEndian));
1239 paint.Style = SKPaintStyle.Fill;
1240 canvas.DrawImage(ims, 0, 0, paint);
1242 paint.Style = SKPaintStyle.Stroke;
1243 paint.Color = SKColors.Gray;
1244 canvas.DrawRect(overview.X, overview.Y, overview.Width, overview.Height, paint);
1246 // Draw viewport indicator
1247 paint.Color = SKColors.Red;
1248 // ... existing viewport rect calculation ...
1254 public void RequestImmediateRender()
1259 private void CleanupViewerResources()
1261 if (_viewerCleanupDone)
1264 _viewerCleanupDone =
true;
1266 _tileFetchCancellation?.Cancel();
1267 _tileFetchCancellation?.Dispose();
1268 _tileFetchCancellation =
null;
1270 lock (_pendingTileFetches)
1272 _pendingTileFetches.Clear();
1275 overviewImage?.Dispose();
1276 overviewImage =
null;
1277 _overviewRequestId++;
1279 slideRenderer?.ClearCache();
1280 slideRenderer =
null;
1281 sKSlideRenderer?.Dispose();
1282 sKSlideRenderer =
null;
1285 private static SkiaSharp.SKRect ToRectangle(
float x1,
float y1,
float x2,
float y2)
1287 return new SkiaSharp.SKRect()
1289 Location =
new SKPoint(x1, y1),
1290 Size =
new SKSize(x2, y2)
1293 private bool refresh =
false;
1294 SKImage overviewSKImage;
1295 private bool _viewerCleanupDone =
false;
1296 private bool _destroyRequested =
false;
1297 private async Task PrefetchSurroundingTiles(Extent viewportExtent,
int level)
1299 if (_viewerCleanupDone)
1302 _tileFetchCancellation?.Cancel();
1303 _tileFetchCancellation =
new CancellationTokenSource();
1304 var token = _tileFetchCancellation.Token;
1307 var expandedExtent =
new Extent(
1308 viewportExtent.MinX - 256,
1309 viewportExtent.MinY - 256,
1310 viewportExtent.MaxX + 256,
1311 viewportExtent.MaxY + 256
1313 IEnumerable<TileInfo> tileInfos;
1314 if (SelectedImage.OpenSlideBase !=
null)
1315 tileInfos = SelectedImage.OpenSlideBase.Schema.GetTileInfos(expandedExtent, level);
1317 tileInfos = SelectedImage.SlideBase.Schema.GetTileInfos(expandedExtent, level);
1319 var fetchTasks =
new List<Task>();
1320 foreach (var tileInfo
in tileInfos)
1322 if (token.IsCancellationRequested)
break;
1324 var coord = SelectedImage.Coordinate;
1325 var info =
new Info(coord, tileInfo.Index, tileInfo.Extent, level);
1326 string pendingKey = $
"{coord.Z}:{coord.C}:{coord.T}:{tileInfo.Index.Level}:{tileInfo.Index.Col}:{tileInfo.Index.Row}";
1329 if (SelectedImage.OpenSlideBase !=
null)
1331 if (SelectedImage.OpenSlideBase.cache.GetTile(info) !=
null)
1336 BioLib.TileInformation tf =
new TileInformation(tileInfo.Index, tileInfo.Extent, SelectedImage.Coordinate);
1337 if (SelectedImage.SlideBase.cache.GetTile(tf) !=
null)
1342 if (!_pendingTileFetches.ContainsKey(pendingKey))
1344 var fetchTask = Task.Run(async () =>
1348 if (SelectedImage.OpenSlideBase !=
null)
1350 Info inf =
new Info(coord, tileInfo.Index, tileInfo.Extent, tileInfo.Index.Level);
1351 var tile = await SelectedImage.OpenSlideBase.cache.GetTile(inf);
1356 TileInformation tf =
new TileInformation(tileInfo.Index, tileInfo.Extent, coord);
1357 var tile = await SelectedImage.SlideBase.cache.GetTile(tf);
1367 _pendingTileFetches[pendingKey] = fetchTask;
1368 fetchTasks.Add(fetchTask);
1373 _ = Task.WhenAll(fetchTasks).ContinueWith(t =>
1376 lock (_pendingTileFetches)
1378 var completed = _pendingTileFetches
1379 .Where(kvp => kvp.Value.IsCompleted)
1380 .Select(kvp => kvp.Key)
1383 foreach (var key
in completed)
1384 _pendingTileFetches.Remove(key);
1388 if (fetchTasks.Count > 0 && !token.IsCancellationRequested)
1389 RequestDeferredRender();
1392 public void RequestDeferredRender()
1394 if (_viewerCleanupDone)
1396 if (System.Threading.Interlocked.Exchange(ref _deferredRenderQueued, 1) == 1)
1399 GLib.Timeout.Add(16, () =>
1401 if (_viewerCleanupDone)
1403 System.Threading.Interlocked.Exchange(ref _deferredRenderQueued, 0);
1409 ExecuteDeferredRender();
1413 System.Threading.Interlocked.Exchange(ref _deferredRenderQueued, 0);
1418 private void ExecuteDeferredRender()
1420 if (_viewerCleanupDone)
1424 if (Thread.CurrentThread == _gtkMainThread)
1427 Gtk.Application.Invoke((sender, args) => DoRender());
1429 private void DoRender()
1431 if (_viewerCleanupDone)
1433 if (SelectedImage?.isPyramidal !=
true)
return;
1438 if (View.AllocatedWidth <= 1 || View.AllocatedHeight <= 1)
1439 s =
new AForge.Size(600, 400);
1441 s =
new AForge.Size(View.AllocatedWidth, View.AllocatedHeight);
1443 var brutileOrigin =
new PointD(PyramidalOrigin.X, -PyramidalOrigin.Y);
1444 int myGeneration = System.Threading.Interlocked.Increment(ref _renderGeneration);
1446 var renderTask = sKSlideRenderer?.UpdateViewAsync(
1453 if (renderTask !=
null)
1455 _ = renderTask.ContinueWith(t =>
1457 if (!t.IsFaulted && !_viewerCleanupDone && _renderGeneration == myGeneration)
1458 Gtk.Application.Invoke((s2, a2) => View?.QueueDraw());
1464 if (View.AllocatedWidth > 1 && View.AllocatedHeight > 1)
1466 _ = slideRenderer?.UpdateViewAsync(
1468 View.AllocatedWidth,
1469 View.AllocatedHeight,
1476 bool initrend =
false;
1478 private void DrawViewportRectangle(SKCanvas canvas, SKPaint paint)
1480 if (SelectedImage ==
null || !SelectedImage.isPyramidal || !ShowOverview)
1485 TryGetPyramidalViewerBounds(SelectedImage, out
double imageMinX, out
double imageMinY, out
double imageMaxX, out
double imageMaxY);
1486 double imageWorldW = imageMaxX - imageMinX;
1487 double imageWorldH = imageMaxY - imageMinY;
1489 if (imageWorldW <= 0) imageWorldW = 1;
1490 if (imageWorldH <= 0) imageWorldH = 1;
1495 double visibleLeft = PyramidalOrigin.X;
1496 double visibleTop = PyramidalOrigin.Y;
1497 double visibleRight = visibleLeft + (View.AllocatedWidth * Resolution);
1498 double visibleBottom = visibleTop + (View.AllocatedHeight * Resolution);
1500 paint.Color = SKColors.Yellow;
1501 if (visibleLeft <= imageMinX &&
1502 visibleTop <= imageMinY &&
1503 visibleRight >= imageMaxX &&
1504 visibleBottom >= imageMaxY)
1506 paint.Style = SKPaintStyle.Stroke;
1507 paint.StrokeWidth = 1f;
1508 paint.IsAntialias =
true;
1509 canvas.DrawRect(0, 0, overview.Width, overview.Height, paint);
1513 float ovScaleX = (float)overview.Width / (
float)imageWorldW;
1514 float ovScaleY = (float)overview.Height / (
float)imageWorldH;
1516 float ovX = (float)((visibleLeft - imageMinX) * ovScaleX);
1517 float ovY = (float)((visibleTop - imageMinY) * ovScaleY);
1518 float ovW = (float)((visibleRight - visibleLeft) * ovScaleX);
1519 float ovH = (float)((visibleBottom - visibleTop) * ovScaleY);
1522 float rectLeft = Math.Max(0, ovX);
1523 float rectTop = Math.Max(0, ovY);
1524 float rectRight = Math.Min((
float)overview.Width, ovX + ovW);
1525 float rectBottom = Math.Min((
float)overview.Height, ovY + ovH);
1528 if (rectRight > rectLeft && rectBottom > rectTop)
1530 paint.Style = SKPaintStyle.Stroke;
1531 paint.StrokeWidth = 1f;
1532 paint.IsAntialias =
true;
1534 canvas.DrawRect(rectLeft, rectTop, rectRight - rectLeft, rectBottom - rectTop, paint);
1541 private void SetResolution(
double newResolution, PointD mouseScreenPos)
1543 if (newResolution <= 0 || newResolution == Resolution)
1546 double oldResolution = Resolution;
1550 double worldX = PyramidalOrigin.X + mouseScreenPos.X * oldResolution;
1551 double worldY = PyramidalOrigin.Y + mouseScreenPos.Y * oldResolution;
1555 _suppressViewUpdates =
true;
1558 if (SelectedImage.isPyramidal)
1559 SelectedImage.Resolution = newResolution;
1563 _suppressViewUpdates =
false;
1566 double actualResolution = Resolution;
1567 if (actualResolution == oldResolution)
1571 double newOriginX = worldX - mouseScreenPos.X * actualResolution;
1572 double newOriginY = worldY - mouseScreenPos.Y * actualResolution;
1575 TryGetPyramidalViewerBounds(SelectedImage, out
double imageMinX, out
double imageMinY, out
double imageMaxX, out
double imageMaxY);
1576 double viewWorldW = Math.Max(1, View?.AllocatedWidth ?? 1) * actualResolution;
1577 double viewWorldH = Math.Max(1, View?.AllocatedHeight ?? 1) * actualResolution;
1579 PyramidalOrigin =
new PointD(
1580 ClampPyramidalOrigin(newOriginX, imageMinX, imageMaxX, viewWorldW),
1581 ClampPyramidalOrigin(newOriginY, imageMinY, imageMaxY, viewWorldH)
1586 public void SetTitle(
string s)
1591 private void OnViewResized(
object sender, EventArgs e)
1593 if (SelectedImage?.isPyramidal ==
true)
1603 private readonly Thread _gtkMainThread = Thread.CurrentThread;
1606 private volatile int _renderGeneration = 0;
1607 private int _deferredRenderQueued = 0;
1610 private bool _suppressViewUpdates =
false;
1611 private bool _isApplyingGoToImage =
false;
1612 private int? _pendingGoToImageIndex =
null;
1613 private bool _updatingGui =
false;
1614 private readonly HashSet<BioImage> _initializedRgbChannelDefaults =
new HashSet<BioImage>();
1615 private CancellationTokenSource _tileFetchCancellation;
1616 private Dictionary<string, Task<byte[]>> _pendingTileFetches =
new Dictionary<string, Task<byte[]>>();
1617 private List<SKImage> SKImages =
new List<SKImage>();
1618 private List<Bitmap> Bitmaps =
new List<Bitmap>();
1623 if (selectedIndex < 0 || selectedIndex >= Images.Count)
1625 BioImage currentImage = Images[selectedIndex];
1626 if (currentImage ==
null)
1628 EnsureImageChannels(currentImage);
1629 if (zBar.Adjustment.Upper != currentImage.SizeZ - 1 ||
1630 tBar.Adjustment.Upper != currentImage.SizeT - 1)
1636 if (currentImage.isPyramidal && glArea.AllocatedWidth > 1 && glArea.AllocatedHeight > 1)
1639 _ = slideRenderer.UpdateViewAsync(
1640 currentImage.PyramidalOrigin,
1641 glArea.AllocatedWidth,
1642 glArea.AllocatedHeight,
1643 currentImage.Resolution,
1644 currentImage.Coordinate
1647 else if (!currentImage.isPyramidal)
1649 for (
int i = 0; i < SKImages.Count; i++)
1651 SKImages[i].Dispose();
1652 Bitmaps[i].Dispose();
1656 foreach (BioImage b
in Images)
1658 ZCT c = b.Coordinate;
1659 AForge.Bitmap bitmap =
null;
1660 int index = b.GetFrameIndex(c.Z, c.C, c.T);
1661 if (Mode == ViewMode.Filtered)
1663 bitmap = b.GetFiltered(c, b.RChannel.RangeR, b.GChannel.RangeG, b.BChannel.RangeB);
1665 else if (Mode == ViewMode.RGBImage)
1667 bitmap = b.GetRGBBitmap(c, b.RChannel.RangeR, b.GChannel.RangeG, b.BChannel.RangeB);
1669 else if (Mode == ViewMode.Raw)
1671 bitmap = b.Buffers[index];
1675 bitmap = b.GetEmission(c, b.RChannel.RangeR, b.GChannel.RangeG, b.BChannel.RangeB);
1679 var bm = bitmap.GetImageRGBA(bitmap.LittleEndian);
1680 SKImage skim = BitmapToSKImage(bm);
1688 if (currentImage.isPyramidal && sk.AllocatedWidth > 1 && sk.AllocatedHeight > 1)
1691 _ = sKSlideRenderer.UpdateViewAsync(
1692 currentImage.PyramidalOrigin,
1693 currentImage.Resolution,
1694 currentImage.Coordinate,
1699 else if (!currentImage.isPyramidal)
1701 for (
int i = 0; i < SKImages.Count; i++)
1703 SKImages[i].Dispose();
1704 Bitmaps[i].Dispose();
1708 foreach (BioImage b
in Images)
1710 ZCT c = b.Coordinate;
1711 AForge.Bitmap bitmap =
null;
1712 int index = b.GetFrameIndex(c.Z, c.C, c.T);
1713 if (Mode == ViewMode.Filtered)
1715 bitmap = b.GetFiltered(c, b.RChannel.RangeR, b.GChannel.RangeG, b.BChannel.RangeB);
1717 else if (Mode == ViewMode.RGBImage)
1719 bitmap = b.GetRGBBitmap(c, b.RChannel.RangeR, b.GChannel.RangeG, b.BChannel.RangeB);
1721 else if (Mode == ViewMode.Raw)
1723 bitmap = b.Buffers[index];
1727 bitmap = b.GetEmission(c, b.RChannel.RangeR, b.GChannel.RangeG, b.BChannel.RangeB);
1731 var bm = bitmap.GetImageRGBA(bitmap.LittleEndian);
1732 SKImage skim = BitmapToSKImage(bm);
1743 private async Task UpdateWellImageAsync()
1747 var bitmap = await SelectedImage.GetWellFieldBitmap().ConfigureAwait(false);
1748 if (bitmap == null) return;
1750 var tcs = new System.Threading.Tasks.TaskCompletionSource<bool>();
1751 Gtk.Application.Invoke((s, e) =>
1755 for (int i = 0; i < SKImages.Count; i++) { SKImages[i].Dispose(); Bitmaps[i].Dispose(); }
1759 SKImage skim = BitmapToSKImage(bitmap);
1761 Bitmaps.Add(bitmap);
1764 tcs.SetResult(true);
1766 catch (Exception ex) { tcs.SetException(ex); }
1768 await tcs.Task.ConfigureAwait(false);
1770 catch (Exception ex)
1772 Console.WriteLine($"UpdateWellImageAsync error: {ex.Message}");
1780 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false);
1788 if (SelectedImage?.isPyramidal !=
true)
1793 slideRenderer?.InvalidateRenderState(cancelActiveFetches:
false);
1794 sKSlideRenderer?.InvalidateRenderState();
1795 SelectedImage.InvalidateTileCache();
1796 slideRenderer?.ClearCache(resetLevel:
false, cancelActiveFetches:
false);
1797 sKSlideRenderer?.ClearCache();
1798 SelectedImage.ZarrDisplayMax = 0;
1799 _overviewSource =
null;
1800 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, SelectedImage?.file ??
string.Empty);
1803 public Task<bool> PopulateSelectedZarrStatisticsAsync()
1805 var image = SelectedImage;
1806 if (image?.IsZarrSource !=
true || slideRenderer ==
null)
1807 return Task.FromResult(
false);
1809 return slideRenderer.PopulateZarrStatisticsAsync(image, image.Coordinate);
1812 SKImage overviewImage;
1813 private BioImage _overviewSource;
1814 private ZCT _overviewCoordinate;
1815 private int _overviewDisplayMin, _overviewDisplayMax, _overviewWellIndex;
1816 private int _overviewRequestId;
1818 private void EnsureOverviewCurrent()
1820 var image = SelectedImage;
1821 if (_viewerCleanupDone || image?.isPyramidal !=
true || !ShowOverview)
1824 if (!ReferenceEquals(_overviewSource, image) ||
1825 _overviewCoordinate != image.Coordinate ||
1826 _overviewDisplayMin != image.ZarrDisplayMin ||
1827 _overviewDisplayMax != image.ZarrDisplayMax ||
1828 _overviewWellIndex != image.WellIndex)
1831 public int? MacroResolution {
get {
return SelectedImage.MacroResolution; } }
1832 public int? LabelResolution {
get {
return SelectedImage.LabelResolution; } }
1840 private async
void InitPreview()
1842 var image = SelectedImage;
1843 if (_viewerCleanupDone || image ==
null || !image.isPyramidal)
1846 int requestId = ++_overviewRequestId;
1847 _overviewSource = image;
1848 _overviewCoordinate = image.Coordinate;
1849 _overviewDisplayMin = image.ZarrDisplayMin;
1850 _overviewDisplayMax = image.ZarrDisplayMax;
1851 _overviewWellIndex = image.WellIndex;
1852 ZCT coordinate = image.Coordinate;
1856 int OVERVIEW_SIZE = View.ScaleFactor * 160;
1858 var (sourceBitmap, srcW, srcH) = await FetchPreviewBitmap(image, coordinate, OVERVIEW_SIZE);
1859 Gtk.Application.Invoke((s, e) =>
1861 using (sourceBitmap)
1863 if (_viewerCleanupDone || requestId != _overviewRequestId || !ReferenceEquals(image, SelectedImage))
1867 var (ow, oh) = CalcOverviewSize(srcW, srcH, OVERVIEW_SIZE);
1868 var preview = ScaleToOverview(sourceBitmap, srcW, srcH, ow, oh);
1869 overview =
new Rectangle(0, 0, ow, oh);
1870 overviewImage?.Dispose();
1871 overviewImage = preview;
1872 ShowOverview = overviewImage !=
null;
1874 catch (Exception ex)
1876 Console.WriteLine($
"Overview rendering failed: {ex.Message}");
1877 ShowOverview =
false;
1878 overviewImage?.Dispose();
1879 overviewImage =
null;
1885 if (View is SlideGLArea slideGlArea)
1886 slideGlArea.RequestRedraw();
1892 catch (Exception ex)
1894 Gtk.Application.Invoke((s, e) =>
1896 if (_viewerCleanupDone || requestId != _overviewRequestId)
1898 Console.WriteLine($
"Overview loading failed: {ex.Message}");
1899 ShowOverview =
false;
1900 overviewImage?.Dispose();
1901 overviewImage =
null;
1907 private async Task<(Bitmap bitmap,
int w,
int h)> FetchPreviewBitmap(BioImage image, ZCT coordinate,
int overviewSize)
1909 if (image.Type == BioImage.ImageType.well)
1911 var bmp = await image.GetWellFieldBitmap(image.WellIndex, coordinate.Z, coordinate.C, coordinate.T);
1914 return (
null, 0, 0);
1916 return (bmp, bmp.Width, bmp.Height);
1922 if (View is SlideGLArea slideGlArea)
1924 var renderedPreview = await CaptureRenderedPreviewAsync(slideGlArea).ConfigureAwait(
false);
1925 if (renderedPreview.bitmap !=
null)
1926 return renderedPreview;
1929 if (image.OpenSlideBase?.SlideImage !=
null)
1930 return await Task.Run(() => FetchOpenSlidePreview(image, overviewSize)).ConfigureAwait(
false);
1932 int level = FindOverviewLevel(image, overviewSize);
1934 return (
null, 0, 0);
1936 for (
int lev = level; lev >= 0; lev--)
1938 var res = image.Resolutions[lev];
1939 int srcW = res.SizeX, srcH = res.SizeY;
1943 var tileBitmap = await image.GetTile(
1944 image.GetFrameIndex(coordinate.Z, coordinate.C, coordinate.T),
1947 srcW, srcH, coordinate);
1948 if (tileBitmap !=
null && !LooksBlank(tileBitmap))
1949 return (tileBitmap, srcW, srcH);
1950 tileBitmap?.Dispose();
1956 if (image.SlideBase ==
null)
1957 return (
null, 0, 0);
1961 var sliceInfo =
new BioLib.SliceInfo(
1964 image.GetUnitPerPixel(lev),
1967 byte[] bt = await image.SlideBase.GetSlice(
1970 new AForge.Size(srcW, srcH));
1972 var bmp = CreatePreviewBitmap(srcW, srcH, bt);
1973 if (!LooksBlank(bmp))
1974 return (bmp, srcW, srcH);
1982 return (
null, 0, 0);
1985 private static async Task<(Bitmap bitmap,
int w,
int h)> CaptureRenderedPreviewAsync(SlideGLArea slideGlArea)
1987 var completion =
new TaskCompletionSource<(Bitmap bitmap, int w, int h)>(
1988 TaskCreationOptions.RunContinuationsAsynchronously);
1989 Action<byte[], int, int> captureCallback =
null;
1991 captureCallback = (bytes, width, height) =>
1995 completion.TrySetResult((
1996 CreatePreviewBitmap(width, height, bytes), width, height));
1998 catch (Exception ex)
2000 completion.TrySetException(ex);
2003 Gtk.Application.Invoke((s, e) =>
2005 slideGlArea.CaptureNextWholeImageFrame(captureCallback);
2010 Task finished = await Task.WhenAny(completion.Task, Task.Delay(TimeSpan.FromSeconds(15)))
2011 .ConfigureAwait(
false);
2012 if (finished == completion.Task)
2013 return await completion.Task.ConfigureAwait(
false);
2015 Gtk.Application.Invoke((s, e) =>
2016 slideGlArea.CancelWholeImageFrameCapture(captureCallback));
2017 return (
null, 0, 0);
2020 private static (Bitmap bitmap,
int w,
int h) FetchOpenSlidePreview(BioImage image,
int overviewSize)
2022 var slide = image?.OpenSlideBase?.SlideImage;
2023 if (slide ==
null || slide.LevelCount <= 0)
2024 return (
null, 0, 0);
2026 int targetSize = Math.Max(overviewSize * 2, 512);
2027 var levels = Enumerable.Range(0, slide.LevelCount)
2030 var dimensions = slide.GetLevelDimension(level);
2031 int width = (int)Math.Min(
int.MaxValue, dimensions.Width);
2032 int height = (int)Math.Min(
int.MaxValue, dimensions.Height);
2033 long score = Math.Abs((
long)Math.Max(width, height) - targetSize);
2034 return (Level: level, Width: width, Height: height, Score: score);
2036 .Where(item => item.Width > 0 && item.Height > 0)
2037 .OrderBy(item => item.Score);
2039 foreach (var item
in levels)
2043 byte[] bytes = slide.ReadRegion(item.Level, 0, 0, item.Width, item.Height);
2044 Bitmap bitmap = CreatePreviewBitmap(item.Width, item.Height, bytes);
2045 if (!LooksBlank(bitmap, rejectBlack:
true))
2046 return (bitmap, item.Width, item.Height);
2054 return (
null, 0, 0);
2057 private static Bitmap CreatePreviewBitmap(
int width,
int height,
byte[] bytes)
2064 if (bytes.Length == width * height * 3)
2066 byte[] bgra =
new byte[width * height * 4];
2067 for (
int i = 0, j = 0; i < bytes.Length; i += 3, j += 4)
2069 bgra[j + 0] = bytes[i + 2];
2070 bgra[j + 1] = bytes[i + 1];
2071 bgra[j + 2] = bytes[i + 0];
2074 return new Bitmap(width, height, AForge.PixelFormat.Format32bppArgb, bgra,
new ZCT(),
"");
2077 return new Bitmap(width, height, AForge.PixelFormat.Format32bppArgb, bytes,
new ZCT(),
"");
2080 private int FindOverviewLevel(BioImage image,
int overviewSize)
2083 int maxLevel = image.MacroResolution.HasValue
2084 ? image.MacroResolution.Value - 1
2085 : image.Resolutions.Count - 1;
2086 maxLevel = Math.Clamp(maxLevel, 0, image.Resolutions.Count - 1);
2091 int targetSize = Math.Max(overviewSize * 2, 512);
2094 int bestScore =
int.MaxValue;
2096 for (
int lev = maxLevel; lev >= 0; lev--)
2098 var res = image.Resolutions[lev];
2099 int maxDim = Math.Max(res.SizeX, res.SizeY);
2103 int score = Math.Abs(maxDim - targetSize);
2104 if (score < bestScore)
2114 private static (
int w,
int h) CalcOverviewSize(
int srcW,
int srcH,
int maxSize)
2116 double ar = (double)srcW / Math.Max(1, srcH);
2117 int w = ar >= 1.0 ? maxSize : (int)(maxSize * ar);
2118 int h = ar >= 1.0 ? (int)(maxSize / ar) : maxSize;
2119 return (Math.Max(w, 20), Math.Max(h, 20));
2122 private static bool LooksBlank(Bitmap bmp,
bool rejectBlack =
false)
2124 if (bmp ==
null || bmp.Width <= 0 || bmp.Height <= 0)
2127 int stepX = Math.Max(1, bmp.Width / 16);
2128 int stepY = Math.Max(1, bmp.Height / 16);
2133 for (
int y = 0; y < bmp.Height; y += stepY)
2135 for (
int x = 0; x < bmp.Width; x += stepX)
2137 var p = bmp.GetPixel(x, y);
2138 int lum = (p.R + p.G + p.B) / 3;
2141 if (rejectBlack && lum <= 5)
2147 return samples > 0 &&
2148 (bright * 100 / samples > 95 ||
2149 rejectBlack && dark * 100 / samples > 98);
2152 private SKImage ScaleToOverview(Bitmap src,
int srcW,
int srcH,
int destW,
int destH)
2157 Bitmap previewBitmap = NormalizeOverviewBitmap(src,
2158 SelectedImage?.ZarrDisplayMin ?? 0, SelectedImage?.ZarrDisplayMax ?? 0);
2164 using var rgbaBitmap = previewBitmap.GetImageRGBA(previewBitmap.LittleEndian);
2165 using (var fullSK = BioImage.BitmapToSKImage(rgbaBitmap))
2167 using var scaled =
new SKBitmap(destW, destH, SKColorType.Bgra8888, SKAlphaType.Premul);
2168 fullSK.ScalePixels(scaled.PeekPixels(), SKSamplingOptions.Default);
2169 return SKImage.FromBitmap(scaled);
2174 if (!ReferenceEquals(previewBitmap, src))
2175 previewBitmap?.Dispose();
2179 private static Bitmap NormalizeOverviewBitmap(Bitmap src, ushort displayMin,
int displayMax)
2187 if (src.PixelFormat != AForge.PixelFormat.Format16bppGrayScale &&
2188 src.PixelFormat != AForge.PixelFormat.Format48bppRgb)
2191 if (displayMax <= displayMin || displayMax > ushort.MaxValue)
2194 displayMax = ComputeOverviewDisplayMax(src);
2197 if (displayMax == 0)
2200 if (src.PixelFormat == AForge.PixelFormat.Format16bppGrayScale)
2201 return NormalizeGray16ToArgb(src, displayMin, (ushort)displayMax);
2203 return NormalizeRgb48ToArgb(src, displayMin, (ushort)displayMax);
2206 private static Bitmap NormalizeGray16ToArgb(Bitmap src, ushort displayMin, ushort displayMax)
2208 byte[] bytes = src.Bytes;
2209 if (bytes ==
null || bytes.Length == 0)
2212 int pixelCount = src.Width * src.Height;
2213 if (pixelCount <= 0)
2216 byte[] bgra =
new byte[pixelCount * 4];
2217 bool littleEndian = src.LittleEndian;
2218 float scale = 255f / Math.Max(1, displayMax - displayMin);
2220 for (
int srcIndex = 0, dstIndex = 0; srcIndex + 1 < bytes.Length && dstIndex + 3 < bgra.Length; srcIndex += 2, dstIndex += 4)
2222 ushort value = littleEndian
2223 ? (ushort)(bytes[srcIndex] | (bytes[srcIndex + 1] << 8))
2224 : (ushort)((bytes[srcIndex] << 8) | bytes[srcIndex + 1]);
2225 byte gray = (byte)Math.Clamp((value - displayMin) * scale, 0f, 255f);
2226 bgra[dstIndex + 0] = gray;
2227 bgra[dstIndex + 1] = gray;
2228 bgra[dstIndex + 2] = gray;
2229 bgra[dstIndex + 3] = 255;
2232 return new Bitmap(src.Width, src.Height, AForge.PixelFormat.Format32bppArgb, bgra,
new ZCT(),
"");
2235 private static Bitmap NormalizeRgb48ToArgb(Bitmap src, ushort displayMin, ushort displayMax)
2237 byte[] bytes = src.Bytes;
2238 if (bytes ==
null || bytes.Length == 0)
2241 int pixelCount = src.Width * src.Height;
2242 if (pixelCount <= 0)
2245 byte[] bgra =
new byte[pixelCount * 4];
2246 bool littleEndian = src.LittleEndian;
2247 float scale = 255f / Math.Max(1, displayMax - displayMin);
2249 for (
int srcIndex = 0, dstIndex = 0; srcIndex + 5 < bytes.Length && dstIndex + 3 < bgra.Length; srcIndex += 6, dstIndex += 4)
2251 ushort r = littleEndian
2252 ? (ushort)(bytes[srcIndex + 0] | (bytes[srcIndex + 1] << 8))
2253 : (ushort)((bytes[srcIndex + 0] << 8) | bytes[srcIndex + 1]);
2254 ushort g = littleEndian
2255 ? (ushort)(bytes[srcIndex + 2] | (bytes[srcIndex + 3] << 8))
2256 : (ushort)((bytes[srcIndex + 2] << 8) | bytes[srcIndex + 3]);
2257 ushort b = littleEndian
2258 ? (ushort)(bytes[srcIndex + 4] | (bytes[srcIndex + 5] << 8))
2259 : (ushort)((bytes[srcIndex + 4] << 8) | bytes[srcIndex + 5]);
2261 bgra[dstIndex + 0] = (byte)Math.Clamp((b - displayMin) * scale, 0f, 255f);
2262 bgra[dstIndex + 1] = (byte)Math.Clamp((g - displayMin) * scale, 0f, 255f);
2263 bgra[dstIndex + 2] = (byte)Math.Clamp((r - displayMin) * scale, 0f, 255f);
2264 bgra[dstIndex + 3] = 255;
2267 return new Bitmap(src.Width, src.Height, AForge.PixelFormat.Format32bppArgb, bgra,
new ZCT(),
"");
2270 private static ushort ComputeOverviewDisplayMax(Bitmap src)
2272 if (src ==
null || src.Bytes ==
null || src.Bytes.Length == 0)
2275 byte[] bytes = src.Bytes;
2276 bool littleEndian = src.LittleEndian;
2278 if (src.PixelFormat == AForge.PixelFormat.Format16bppGrayScale ||
2279 src.PixelFormat == AForge.PixelFormat.Format48bppRgb)
2282 for (
int i = 0; i + 1 < bytes.Length; i += 2)
2284 ushort value = littleEndian
2285 ? (ushort)(bytes[i] | (bytes[i + 1] << 8))
2286 : (ushort)((bytes[i] << 8) | bytes[i + 1]);
2290 return max == 0 ? (ushort)1 : max;
2293 int stride = bytes.Length >= src.Width * src.Height * 4 ? 4 : 3;
2295 for (
int i = 0; i + stride - 1 < bytes.Length; i += stride)
2297 if (bytes[i + 0] > maxByte) maxByte = bytes[i + 0];
2298 if (bytes[i + 1] > maxByte) maxByte = bytes[i + 1];
2299 if (bytes[i + 2] > maxByte) maxByte = bytes[i + 2];
2302 return maxByte == 0 ? (ushort)1 : maxByte;
2309 View.Realized += View_Realized;
2310 this.Shown += ImageView_Shown;
2311 zBar.ValueChanged += ValueChangedZ;
2312 cBar.ValueChanged += ValueChangedC;
2313 tBar.ValueChanged += ValueChangedT;
2314 zBar.ScrollEvent += ZBar_ScrollEvent;
2315 cBar.ScrollEvent += CBar_ScrollEvent;
2316 tBar.ScrollEvent += TBar_ScrollEvent;
2317 zBar.AddEvents((
int)EventMask.ScrollMask);
2318 cBar.AddEvents((
int)EventMask.ScrollMask);
2319 tBar.AddEvents((
int)EventMask.ScrollMask);
2320 this.ScrollEvent += ImageView_WindowScrollEvent;
2321 this.AddEvents((
int)EventMask.ScrollMask);
2324 View.MotionNotifyEvent += ImageView_MotionNotifyEvent;
2325 View.ButtonPressEvent += ImageView_ButtonPressEvent;
2326 View.ButtonReleaseEvent += ImageView_ButtonReleaseEvent;
2327 View.ScrollEvent += ImageView_ScrollEvent;
2330 ((SKDrawingArea)View).PaintSurface += Sk_PaintSurface;
2331 View.SizeAllocated += PictureBox_SizeAllocated;
2332 View.SetSizeRequest(600, 400);
2333 View.AddEvents((
int)(EventMask.ButtonPressMask
2334 | EventMask.ButtonReleaseMask
2335 | EventMask.KeyPressMask
2336 | EventMask.PointerMotionMask
2337 | EventMask.ScrollMask));
2341 View.MotionNotifyEvent += ImageView_MotionNotifyEvent;
2342 View.ButtonPressEvent += ImageView_ButtonPressEvent;
2343 View.ButtonReleaseEvent += ImageView_ButtonReleaseEvent;
2344 View.ScrollEvent += ImageView_ScrollEvent;
2347 glArea.Render += GlArea_Render;
2348 View.SizeAllocated += PictureBox_SizeAllocated;
2349 View.AddEvents((
int)(EventMask.ButtonPressMask
2350 | EventMask.ButtonReleaseMask
2351 | EventMask.KeyPressMask
2352 | EventMask.PointerMotionMask
2353 | EventMask.ScrollMask));
2355 vScroll.ValueChanged += VScroll_ValueChanged;
2356 hScroll.ValueChanged += HScroll_ValueChanged;
2357 this.KeyPressEvent += ImageView_KeyPressEvent;
2358 this.DestroyEvent += ImageView_DestroyEvent;
2359 this.DeleteEvent += ImageView_DeleteEvent;
2360 this.FocusInEvent += ImageView_FocusInEvent;
2361 rBox.Changed += RBox_Changed;
2362 gBox.Changed += GBox_Changed;
2363 bBox.Changed += BBox_Changed;
2365 goToImageMenu.ButtonPressEvent += GoToImageMenu_ButtonPressEvent;
2366 goToOriginMenu.ButtonPressEvent += GoToOriginMenu_ButtonPressEvent;
2367 overView.ButtonPressEvent += OverView_ButtonPressEvent;
2369 roiShow.ButtonPressEvent += RoiShow_ButtonPressEvent;
2370 roiHide.ButtonPressEvent += RoiHide_ButtonPressEvent;
2371 roiDelete.ButtonPressEvent += RoiDelete_ButtonPressEvent;
2372 roiID.ButtonPressEvent += RoiID_ButtonPressEvent;
2373 copy.ButtonPressEvent += Copy_ButtonPressEvent;
2374 paste.ButtonPressEvent += Paste_ButtonPressEvent;
2375 draw.ButtonPressEvent += Draw_ButtonPressEvent;
2376 fill.ButtonPressEvent += Fill_ButtonPressEvent;
2378 play.ButtonPressEvent += Play_ButtonPressEvent;
2379 stop.ButtonPressEvent += Stop_ButtonPressEvent;
2380 playSpeed.ButtonPressEvent += PlaySpeed_ButtonPressEvent;
2381 setValueRange.ButtonPressEvent += SetValueRange_ButtonPressEvent;
2382 loop.ButtonPressEvent += Loop_ButtonPressEvent;
2384 zBar.ButtonPressEvent += ZBar_ButtonPressEvent;
2385 tBar.ButtonPressEvent += TBar_ButtonPressEvent;
2386 cBar.ButtonPressEvent += CBar_ButtonPressEvent;
2390 private bool _initialPyramidalRenderQueued =
false;
2392 private void EnsureInitialPyramidalRender()
2394 if (_initialPyramidalRenderQueued || SelectedImage?.isPyramidal !=
true)
2397 _initialPyramidalRenderQueued =
true;
2399 GLib.Timeout.Add(75, () =>
2401 if (SelectedImage?.isPyramidal !=
true)
2404 bool hasRenderedImage = MacOS
2405 ? (sKSlideRenderer?.HasValidImage ??
false)
2406 : ((slideRenderer?.CachedTextureCount ?? 0) > 0);
2407 if (hasRenderedImage)
2410 if (View ==
null || !View.IsRealized || View.AllocatedWidth <= 1 || View.AllocatedHeight <= 1)
2415 RequestDeferredRender();
2417 glArea?.RequestRedraw();
2422 return attempts < 20;
2426 private void View_Realized(
object sender, EventArgs e)
2428 EnsureInitialPyramidalRender();
2431 private void ImageView_Shown(
object sender, EventArgs e)
2433 EnsureInitialPyramidalRender();
2436 private void OverView_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2440 ShowOverview =
false;
2441 overView.Label =
"Show Overview";
2445 ShowOverview =
true;
2446 overView.Label =
"Hide Overview";
2451 private void RoiShow_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2454 UpdateRoiMenuState();
2458 private void RoiHide_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2461 UpdateRoiMenuState();
2465 private void HScroll_ValueChanged(
object sender, EventArgs e)
2467 if (_updatingScrollBars)
return;
2469 PyramidalOrigin =
new PointD(hScroll.Value, PyramidalOrigin.Y);
2472 private void VScroll_ValueChanged(
object sender, EventArgs e)
2474 if (_updatingScrollBars)
return;
2476 PyramidalOrigin =
new PointD(PyramidalOrigin.X, vScroll.Value);
2479 private void Sk_PaintSurface(
object sender, SkiaSharp.Views.Desktop.SKPaintSurfaceEventArgs e)
2483 var canvas = e.Surface.Canvas;
2484 var width = e.Info.Width;
2485 var height = e.Info.Height;
2486 if (SelectedImage?.isPyramidal ==
true && sKSlideRenderer !=
null)
2488 if (sKSlideRenderer.HasValidImage)
2489 canvas.Clear(SKColors.Black);
2490 sKSlideRenderer.DrawToCanvas(canvas, width, height);
2491 RenderSkiaAnnotations(canvas, width, height, SelectedImage);
2495 canvas.Clear(SKColors.Black);
2497 RenderSkia(canvas, width, height);
2502 private void GlArea_Render(
object o, RenderArgs args)
2508 private void ImageView_FocusInEvent(
object o, FocusInEventArgs args)
2513 private void ImageView_DestroyEvent(
object o, DestroyEventArgs args)
2515 _destroyRequested =
true;
2519 if (App.tabsView ==
null)
return;
2521 bool stillRegistered =
false;
2522 for (
int v = 0; v < App.tabsView.GetViewerCount(); v++)
2524 if (App.tabsView.GetViewer(v) ==
this)
2526 stillRegistered =
true;
2530 if (!stillRegistered)
return;
2532 App.tabsView.RemoveViewer(
this);
2533 CleanupViewerResources();
2534 foreach (var item
in this.Images)
2536 BioLib.Images.RemoveImage(item);
2541 System.Threading.Thread threadZ =
null;
2542 System.Threading.Thread threadC =
null;
2543 System.Threading.Thread threadT =
null;
2544 static bool playZ =
false;
2545 static bool playC =
false;
2546 static bool playT =
false;
2547 static bool loopZ =
true;
2548 static bool loopC =
true;
2549 static bool loopT =
true;
2550 public static int waitz = 1000;
2551 public static int waitc = 1000;
2552 public static int waitt = 1000;
2553 public static int startz = 0;
2554 public static int startc = 0;
2555 public static int startt = 0;
2556 public static int endz = 0;
2557 public static int endc = 0;
2558 public static int endt = 0;
2562 private static void PlayZ()
2566 ZCT coord = App.viewer.GetCoordinate();
2568 Application.Invoke(delegate
2570 App.viewer.SetCoordinate(coord.Z + 1, coord.C, coord.T);
2572 if (coord.Z == endz)
2577 Application.Invoke(delegate
2579 App.viewer.SetCoordinate(startz, coord.C, coord.T);
2585 System.Threading.Thread.Sleep(waitz);
2592 private static void PlayC()
2596 ZCT coord = App.viewer.GetCoordinate();
2597 Application.Invoke(delegate
2599 App.viewer.SetCoordinate(coord.Z, coord.C + 1, coord.T);
2601 if (coord.C == endc)
2606 Application.Invoke(delegate
2608 App.viewer.SetCoordinate(coord.Z, startc, coord.T);
2614 System.Threading.Thread.Sleep(waitc);
2621 private static void PlayT()
2625 ZCT coord = App.viewer.GetCoordinate();
2626 Application.Invoke(delegate
2628 App.viewer.SetCoordinate(coord.Z, coord.C, coord.T + 1);
2630 if (coord.T == endt)
2635 Application.Invoke(delegate
2637 App.viewer.SetCoordinate(coord.Z, coord.C, startt);
2643 System.Threading.Thread.Sleep(waitt);
2650 private void CBar_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2653 if (args.Event.Button == 3)
2661 private void TBar_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2664 if (args.Event.Button == 3)
2672 private void ZBar_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2675 if (args.Event.Button == 3)
2683 private void Loop_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2713 private void SetValueRange_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2715 Play play = Play.Create();
2723 private void PlaySpeed_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2725 Play play = Play.Create();
2733 private void Stop_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2763 private void Play_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2768 endz = SelectedImage.SizeZ - 1;
2770 threadZ =
new System.Threading.Thread(PlayZ);
2776 endt = SelectedImage.SizeT - 1;
2778 threadT =
new System.Threading.Thread(PlayT);
2784 endc = SelectedImage.SizeC - 1;
2786 threadC =
new System.Threading.Thread(PlayC);
2795 private void Fill_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2797 Bio.Graphics.Graphics g =
Bio.Graphics.Graphics.FromImage(SelectedBuffer);
2798 List<ROI> rois =
new List<ROI>();
2799 rois.AddRange(SelectedImage.AnnotationsR);
2800 rois.AddRange(SelectedImage.AnnotationsG);
2801 rois.AddRange(SelectedImage.AnnotationsB);
2802 foreach (ROI item
in rois)
2804 Bio.Graphics.Pen p =
new Bio.Graphics.Pen(Tools.DrawColor, (
int)Tools.StrokeWidth, SelectedBuffer.BitsPerPixel);
2807 if (item.type == ROI.Type.Line)
2809 PointD pf = SelectedImage.ToImageSpace(item.GetPoint(0));
2810 PointD pf2 = SelectedImage.ToImageSpace(item.GetPoint(1));
2811 g.DrawLine((
int)pf.X, (
int)pf.Y, (
int)pf2.X, (
int)pf2.Y);
2814 if (item.type == ROI.Type.Rectangle)
2816 g.FillRectangle(SelectedImage.ToImageSpace(item.BoundingBox), p.color);
2819 if (item.type == ROI.Type.Ellipse)
2821 g.FillEllipse(SelectedImage.ToImageSpace(item.BoundingBox), p.color);
2824 if (item.type == ROI.Type.Freeform || item.type == ROI.Type.Polygon || item.type == ROI.Type.Polyline)
2826 g.FillPolygon(SelectedImage.ToImageSpace(item.GetPointsF()), SelectedImage.ToImageSpace(item.BoundingBox), p.color);
2838 private void Draw_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2840 Bio.Graphics.Graphics g =
Bio.Graphics.Graphics.FromImage(SelectedBuffer);
2841 List<ROI> rois =
new List<ROI>();
2842 rois.AddRange(SelectedImage.AnnotationsR);
2843 rois.AddRange(SelectedImage.AnnotationsG);
2844 rois.AddRange(SelectedImage.AnnotationsB);
2845 foreach (ROI item
in rois)
2847 Bio.Graphics.Pen p =
new Bio.Graphics.Pen(Tools.DrawColor, (
int)Tools.StrokeWidth, SelectedBuffer.BitsPerPixel);
2851 if (item.type == ROI.Type.Line)
2853 PointD pf = SelectedImage.ToImageSpace(item.GetPoint(0));
2854 PointD pf2 = SelectedImage.ToImageSpace(item.GetPoint(1));
2855 g.DrawLine((
int)pf.X, (
int)pf.Y, (
int)pf2.X, (
int)pf2.Y);
2858 if (item.type == ROI.Type.Rectangle)
2860 g.DrawRectangle(SelectedImage.ToImageSpace(item.BoundingBox));
2863 if (item.type == ROI.Type.Ellipse)
2865 g.DrawEllipse(SelectedImage.ToImageSpace(item.BoundingBox));
2868 if (item.type == ROI.Type.Freeform || item.type == ROI.Type.Polygon || item.type == ROI.Type.Polyline)
2872 for (
int i = 0; i < item.GetPointCount() - 1; i++)
2874 PointD pf = SelectedImage.ToImageSpace(item.GetPoint(i));
2875 PointD pf2 = SelectedImage.ToImageSpace(item.GetPoint(i + 1));
2876 g.DrawLine((
int)pf.X, (
int)pf.Y, (
int)pf2.X, (
int)pf2.Y);
2878 PointD pp = SelectedImage.ToImageSpace(item.GetPoint(0));
2879 PointD p2 = SelectedImage.ToImageSpace(item.GetPoint(item.GetPointCount() - 1));
2880 g.DrawLine((
int)pp.X, (
int)pp.Y, (
int)p2.X, (
int)p2.Y);
2884 for (
int i = 0; i < item.GetPointCount() - 1; i++)
2886 PointD pf = SelectedImage.ToImageSpace(item.GetPoint(i));
2887 PointD pf2 = SelectedImage.ToImageSpace(item.GetPoint(i + 1));
2888 g.DrawLine((
int)pf.X, (
int)pf.Y, (
int)pf2.X, (
int)pf2.Y);
2903 private void Paste_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2913 private void Copy_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2923 private void ImageView_DeleteEvent(
object o, DeleteEventArgs args)
2925 if (_destroyRequested)
2931 _destroyRequested =
true;
2940 App.tabsView.RemoveViewer(
this);
2941 CleanupViewerResources();
2943 for (
int i = 0; i < this.Images.Count; i++)
2945 var item = this.Images[i];
2946 BioLib.Images.RemoveImage(item);
2947 App.tabsView.RemoveTab(item.Filename);
2962 private void RoiID_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2964 TextInput ti = TextInput.Create();
2974 private void RoiDelete_ButtonPressEvent(
object o, ButtonPressEventArgs args)
2976 DeleteSelectedRois();
2980 private void DeleteSelectedRois()
2982 if (SelectedImage ==
null)
2985 var toDelete =
new HashSet<ROI>(selectedAnnotations);
2986 if (toDelete.Count == 0 && Tools.selectedROI !=
null)
2987 toDelete.Add(Tools.selectedROI);
2989 if (toDelete.Count == 0)
2992 foreach (var roi
in toDelete)
2997 if (SelectedImage.Annotations.Contains(roi))
2998 SelectedImage.Annotations.Remove(roi);
3001 string key = GetOverlayKey(SelectedImage);
3002 var overlays = GetZarrLabelOverlays(SelectedImage);
3003 if (overlays !=
null && overlays.Count > 0)
3005 overlays.RemoveAll(r => r !=
null && toDelete.Contains(r));
3006 SetZarrLabelOverlays(SelectedImage, overlays, key);
3009 if (Tools.selectedROI !=
null && toDelete.Contains(Tools.selectedROI))
3010 Tools.selectedROI =
null;
3012 foreach (var roi
in toDelete)
3015 roi.Selected =
false;
3025 private void GoToOriginMenu_ButtonPressEvent(
object o, ButtonPressEventArgs args)
3027 if (SelectedImage?.isPyramidal ==
true)
3028 PyramidalOrigin =
new PointD(0, 0);
3030 Origin =
new PointD(-SelectedImage.Volume.Location.X, -SelectedImage.Volume.Location.Y);
3032 ForceNavigationRefresh();
3040 private void GoToImageMenu_ButtonPressEvent(
object o, ButtonPressEventArgs args)
3042 if (Images.Count == 0)
3045 var picker = NumberPicker.Create(0, Images.Count - 1, SelectedIndex);
3046 picker.Title =
"Go To Image";
3047 if (picker.Run() != (
int)ResponseType.Ok)
3060 private void BBox_Changed(
object sender, EventArgs e)
3062 if (_updatingGui || SelectedImage ==
null)
3064 SelectedImage.rgbChannels[2] = bBox.Active;
3065 RefreshDisplayState(invalidatePyramidalTiles:
true);
3072 private void GBox_Changed(
object sender, EventArgs e)
3074 if (_updatingGui || SelectedImage ==
null)
3076 SelectedImage.rgbChannels[1] = gBox.Active;
3077 RefreshDisplayState(invalidatePyramidalTiles:
true);
3085 private void RBox_Changed(
object sender, EventArgs e)
3087 if (_updatingGui || SelectedImage ==
null)
3089 SelectedImage.rgbChannels[0] = rBox.Active;
3090 RefreshDisplayState(invalidatePyramidalTiles:
true);
3093 private void RefreshDisplayState(
bool invalidatePyramidalTiles =
false)
3095 if (SelectedImage ==
null)
3098 if (invalidatePyramidalTiles && SelectedImage.isPyramidal)
3108 bool initialized =
false;
3109 private int _lastAllocatedWidth = 0;
3110 private int _lastAllocatedHeight = 0;
3112 private void PictureBox_SizeAllocated(
object o, SizeAllocatedArgs args)
3114 if (SelectedImage ==
null)
return;
3116 int newW = MacOS ? View.AllocatedWidth : (SelectedImage.isPyramidal ? View.AllocatedWidth : View.AllocatedWidth);
3117 int newH = MacOS ? View.AllocatedHeight : (SelectedImage.isPyramidal ? View.AllocatedHeight : View.AllocatedHeight);
3120 if (newW <= 1 || newH <= 1)
return;
3122 bool sizeChanged = (newW != _lastAllocatedWidth || newH != _lastAllocatedHeight);
3123 _lastAllocatedWidth = newW;
3124 _lastAllocatedHeight = newH;
3126 if (SelectedImage.isPyramidal)
3128 SelectedImage.PyramidalSize =
new AForge.Size(newW, newH);
3140 if (SelectedImage?.isPyramidal !=
true || View ==
null ||
3141 View.AllocatedWidth <= 1 || View.AllocatedHeight <= 1)
3145 RequestDeferredRender();
3175 private static void DrawEllipse(Cairo.Context g,
double x,
double y,
double width,
double height)
3178 g.MoveTo(x + (width / 2), y);
3180 g.Scale(width / 2.0, height / 2.0);
3181 g.Arc(0.0, 0.0, 1.0, 0.0, 2 * Math.PI);
3193 return new Cairo.Color((
double)color.R / 255, (
double)color.G / 255, (
double)color.B / 255);
3202 private void TBar_ScrollEvent(
object o, ScrollEventArgs args)
3204 if (AdjustSliderFromScroll(tBar, args))
3213 private void CBar_ScrollEvent(
object o, ScrollEventArgs args)
3215 if (AdjustSliderFromScroll(cBar, args))
3224 private void ZBar_ScrollEvent(
object o, ScrollEventArgs args)
3226 if (AdjustSliderFromScroll(zBar, args))
3230 private void ImageView_WindowScrollEvent(
object o, ScrollEventArgs args)
3232 if (args.RetVal is
bool handled && handled)
3235 if (TryAdjustSliderFromPointer(zBar, args) ||
3236 TryAdjustSliderFromPointer(cBar, args) ||
3237 TryAdjustSliderFromPointer(tBar, args))
3243 private bool TryAdjustSliderFromPointer(Gtk.Scale scale, ScrollEventArgs args)
3245 if (scale ==
null || !scale.Visible || !scale.IsSensitive || !scale.IsRealized)
3248 Gdk.Window hostWindow = scale.Window;
3249 if (hostWindow ==
null)
3252 var allocation = scale.Allocation;
3253 hostWindow.GetOrigin(out
int hostX, out
int hostY);
3254 int widgetX = hostX + allocation.X;
3255 int widgetY = hostY + allocation.Y;
3257 double pointerX = args.Event.XRoot;
3258 double pointerY = args.Event.YRoot;
3261 pointerX >= widgetX &&
3262 pointerX <= widgetX + allocation.Width &&
3263 pointerY >= widgetY &&
3264 pointerY <= widgetY + allocation.Height;
3266 return inside && AdjustSliderFromScroll(scale, args);
3269 private bool AdjustSliderFromScroll(Gtk.Scale scale, ScrollEventArgs args)
3271 if (scale?.Adjustment ==
null)
3274 double delta = GetSliderScrollDelta(args);
3278 double lower = scale.Adjustment.Lower;
3279 double upper = scale.Adjustment.Upper;
3280 double newValue = Math.Clamp(scale.Value + delta, lower, upper);
3281 if (Math.Abs(newValue - scale.Value) <
double.Epsilon)
3284 scale.Value = newValue;
3288 private static double GetSliderScrollDelta(ScrollEventArgs args)
3290 if (args.Event.Direction == ScrollDirection.Up)
3292 if (args.Event.Direction == ScrollDirection.Down)
3294 if (args.Event.Direction == ScrollDirection.Smooth)
3296 if (args.Event.DeltaY < 0)
3298 if (args.Event.DeltaY > 0)
3304 private void RefreshAfterCoordinateSliderChange()
3306 if (SelectedImage?.isPyramidal ==
true)
3307 ForceNavigationRefresh();
3311 RequestViewportRefresh();
3315 public static Gdk.Key keyDown = Gdk.Key.Key_3270_Test;
3324 private void ImageView_KeyPressEvent(
object o, KeyPressEventArgs e)
3326 Plugins.KeyDownEvent(o, e);
3327 keyDown = e.Event.Key;
3328 double moveAmount = 5 * Scale.Width;
3329 double movepyr = 50 * (Level + 1);
3330 if (e.Event.Key == Gdk.Key.c && e.Event.State == ModifierType.ControlMask)
3334 if (e.Event.Key == Gdk.Key.v && e.Event.State == ModifierType.ControlMask)
3338 if (e.Event.Key == Gdk.Key.e)
3340 if (SelectedImage.isPyramidal)
3342 SetResolution(Resolution * 1.25,
new PointD(View.AllocatedWidth / 2.0, View.AllocatedHeight / 2.0));
3345 ZoomStackAtPoint(
new PointD(View.AllocatedWidth / 2.0, View.AllocatedHeight / 2.0), 0.9);
3347 if (e.Event.Key == Gdk.Key.q)
3349 if (SelectedImage.isPyramidal)
3351 SetResolution(Resolution * 0.8,
new PointD(View.AllocatedWidth / 2.0, View.AllocatedHeight / 2.0));
3354 ZoomStackAtPoint(
new PointD(View.AllocatedWidth / 2.0, View.AllocatedHeight / 2.0), 1.1);
3356 if (e.Event.Key == Gdk.Key.s || e.Event.Key == Gdk.Key.S)
3358 if (SelectedImage.isPyramidal)
3360 PyramidalOrigin =
new PointD(PyramidalOrigin.X, PyramidalOrigin.Y + movepyr);
3363 Origin =
new PointD(Origin.X, Origin.Y + moveAmount);
3365 if (e.Event.Key == Gdk.Key.w || e.Event.Key == Gdk.Key.W)
3367 if (SelectedImage.isPyramidal)
3369 PyramidalOrigin =
new PointD(PyramidalOrigin.X, PyramidalOrigin.Y - movepyr);
3372 Origin =
new PointD(Origin.X, Origin.Y - moveAmount);
3374 if (e.Event.Key == Gdk.Key.a || e.Event.Key == Gdk.Key.A)
3376 if (SelectedImage.isPyramidal)
3378 PyramidalOrigin =
new PointD(PyramidalOrigin.X - movepyr, PyramidalOrigin.Y);
3381 Origin =
new PointD(Origin.X + moveAmount, Origin.Y);
3383 if (e.Event.Key == Gdk.Key.d || e.Event.Key == Gdk.Key.D)
3385 if (SelectedImage.isPyramidal)
3387 PyramidalOrigin =
new PointD(PyramidalOrigin.X + movepyr, PyramidalOrigin.Y);
3390 Origin =
new PointD(Origin.X - moveAmount, Origin.Y);
3392 foreach (Function item
in Function.Functions.Values)
3394 if (item.Key == e.Event.Key && item.Modifier == e.Event.State)
3396 if (item.FuncType == Function.FunctionType.ImageJ)
3397 item.PerformFunction(
true);
3399 item.PerformFunction(
false);
3402 Scripting.State st =
new Scripting.State();
3403 st.key = e.Event.Key;
3405 st.type = Scripting.Event.Down;
3406 Scripting.UpdateState(st);
3414 private void ImageView_KeyUpEvent(
object o, KeyReleaseEventArgs e)
3416 Plugins.KeyUpEvent(o, e);
3417 keyDown = Gdk.Key.Key_3270_Test;
3418 Scripting.State st =
new Scripting.State();
3419 st.key = e.Event.Key;
3421 st.type = Scripting.Event.Up;
3422 Scripting.UpdateState(st);
3431 private void ImageView_ScrollEvent(
object o, ScrollEventArgs e)
3433 Plugins.ScrollEvent(o, e);
3434 if (SelectedImage.isPyramidal)
3437 PointD anchor = MouseMove;
3438 if (e.Event.Direction == ScrollDirection.Smooth)
3441 double delta = e.Event.DeltaY;
3442 if (delta == 0)
return;
3443 double factor = Math.Pow(2.0, delta * 0.1);
3444 SetResolution(Resolution * factor, anchor);
3446 else if (e.Event.Direction == ScrollDirection.Up)
3447 SetResolution(Resolution * 0.8, anchor);
3448 else if (e.Event.Direction == ScrollDirection.Down)
3449 SetResolution(Resolution * 1.25, anchor);
3453 PointD anchor = MouseMove;
3454 if (e.Event.Direction == ScrollDirection.Smooth)
3456 double delta = e.Event.DeltaY;
3457 if (delta == 0)
return;
3458 double factor = Math.Pow(2.0, delta * 0.1);
3459 ZoomStackAtPoint(anchor, factor);
3461 else if (e.Event.Direction == ScrollDirection.Up)
3462 ZoomStackAtPoint(anchor, 1.1);
3463 else if (e.Event.Direction == ScrollDirection.Down)
3464 ZoomStackAtPoint(anchor, 0.9);
3472 private void ValueChangedZ(
object? sender, EventArgs e)
3474 if (_updatingCoordinate)
3476 SetCoordinate((
int)zBar.Value, (
int)cBar.Value, (
int)tBar.Value);
3482 private void ValueChangedC(
object? sender, EventArgs e)
3484 if (_updatingCoordinate)
3486 SetCoordinate((
int)zBar.Value, (
int)cBar.Value, (
int)tBar.Value);
3492 private void ValueChangedT(
object? sender, EventArgs e)
3494 if (_updatingCoordinate)
3496 SetCoordinate((
int)zBar.Value, (
int)cBar.Value, (
int)tBar.Value);
3498 private void UpdateScrollBars()
3500 if (SelectedImage.isPyramidal)
3518 double viewportW = Math.Max(1, View?.AllocatedWidth ?? 1) * Resolution;
3519 double viewportH = Math.Max(1, View?.AllocatedHeight ?? 1) * Resolution;
3521 var hAdj = hScroll.Adjustment;
3522 var vAdj = vScroll.Adjustment;
3524 TryGetPyramidalViewerBounds(SelectedImage, out
double imageMinX, out
double imageMinY, out
double imageMaxX, out
double imageMaxY);
3526 hAdj.Lower = imageMinX;
3527 hAdj.Upper = imageMaxX;
3528 hAdj.PageSize = Math.Min(viewportW, Math.Max(1, hAdj.Upper - hAdj.Lower));
3530 vAdj.Lower = imageMinY;
3531 vAdj.Upper = imageMaxY;
3532 vAdj.PageSize = Math.Min(viewportH, Math.Max(1, vAdj.Upper - vAdj.Lower));
3534 hAdj.StepIncrement = Math.Max(1, hAdj.PageSize / 20.0);
3535 hAdj.PageIncrement = Math.Max(1, hAdj.PageSize / 2.0);
3536 vAdj.StepIncrement = Math.Max(1, vAdj.PageSize / 20.0);
3537 vAdj.PageIncrement = Math.Max(1, vAdj.PageSize / 2.0);
3548 if (SelectedImage ==
null)
3553 _updatingGui =
true;
3556 EnsureImageChannels(SelectedImage);
3557 cBar.Adjustment.Lower = 0;
3558 cBar.Adjustment.Upper = Images[selectedIndex].SizeC - 1;
3559 zBar.Adjustment.Lower = 0;
3560 zBar.Adjustment.Upper = Images[selectedIndex].SizeZ - 1;
3561 tBar.Adjustment.Lower = 0;
3562 tBar.Adjustment.Upper = Images[selectedIndex].SizeT - 1;
3563 if (endz == 0 || endz > Images[selectedIndex].SizeZ - 1)
3564 endz = Images[selectedIndex].SizeZ - 1;
3565 if (endc == 0 || endc > Images[selectedIndex].SizeC - 1)
3566 endc = Images[selectedIndex].SizeC - 1;
3567 if (endt == 0 || endt > Images[selectedIndex].SizeT - 1)
3568 endt = Images[selectedIndex].SizeT - 1;
3570 var store =
new ListStore(typeof(
string));
3571 for (
int i = 0; i < SelectedImage.Channels.Count; i++)
3573 store.AppendValues(GetChannelDisplayLabel(SelectedImage.Channels[i], i));
3578 if (SelectedImage.Channels.Count > 0)
3580 int lastChannel = SelectedImage.Channels.Count - 1;
3581 rBox.Active = Math.Clamp(SelectedImage.rgbChannels[0], 0, lastChannel);
3582 gBox.Active = Math.Clamp(SelectedImage.rgbChannels[1], 0, lastChannel);
3583 bBox.Active = Math.Clamp(SelectedImage.rgbChannels[2], 0, lastChannel);
3585 imagesMenu =
new Menu();
3586 for (
int i = 0; i < Images.Count; i++)
3588 if (Images[i] ==
null)
3590 if (SelectedIndex >= Images.Count)
3594 MenuItem mi =
new MenuItem(Images[i].Filename);
3595 mi.ButtonPressEvent += Mi_ButtonPressEvent;
3596 imagesMenu.Append(mi);
3598 goToImageMenu.Submenu = imagesMenu;
3599 goToImageMenu.ShowAll();
3603 _updatingGui =
false;
3607 private static string GetChannelDisplayLabel(Channel channel,
int index)
3609 string name = channel?.Name;
3610 if (
string.IsNullOrWhiteSpace(name) ||
3611 (
int.TryParse(name, out
int numericName) && numericName == index))
3612 name = $
"Channel {index + 1}";
3613 return $
"{name} ({index + 1})";
3616 private void EnsureImageChannels(BioImage image)
3618 if (image?.Channels ==
null || image.Channels.Count == 0)
3621 int expectedChannels = Math.Max(1, Math.Max(image.SizeC, image.RGBChannelCount));
3622 if (image.Resolutions !=
null && image.Resolutions.Count > 0)
3624 var pixelFormat = image.Resolutions[0].PixelFormat;
3625 if (pixelFormat == AForge.PixelFormat.Format24bppRgb ||
3626 pixelFormat == AForge.PixelFormat.Format48bppRgb)
3628 expectedChannels = Math.Max(expectedChannels, 3);
3632 if (expectedChannels > image.Channels.Count)
3634 Channel
template = image.Channels[0];
3635 while (image.Channels.Count < expectedChannels)
3637 Channel clone =
new Channel(0, image.bitsPerPixel, 1)
3639 Name = $
"Channel {image.Channels.Count + 1}",
3640 Fluor =
template.Fluor,
3641 Color = template.Color
3643 image.Channels.Add(clone);
3647 for (
int i = 0; i < image.Channels.Count; i++)
3649 if (
string.IsNullOrWhiteSpace(image.Channels[i].Name))
3650 image.Channels[i].Name = $
"Channel {i + 1}";
3653 if (_initializedRgbChannelDefaults.Add(image) &&
3654 image.rgbChannels[0] == 0 &&
3655 image.rgbChannels[1] == 0 &&
3656 image.rgbChannels[2] == 0)
3658 image.rgbChannels[0] = 0;
3659 image.rgbChannels[1] = Math.Min(1, image.Channels.Count - 1);
3660 image.rgbChannels[2] = Math.Min(2, image.Channels.Count - 1);
3663 image.rgbChannels[0] = Math.Clamp(image.rgbChannels[0], 0, image.Channels.Count - 1);
3664 image.rgbChannels[1] = Math.Clamp(image.rgbChannels[1], 0, image.Channels.Count - 1);
3665 image.rgbChannels[2] = Math.Clamp(image.rgbChannels[2], 0, image.Channels.Count - 1);
3676 private void Mi_ButtonPressEvent(
object o, ButtonPressEventArgs args)
3678 MenuItem menuItem = (MenuItem)o;
3680 foreach (BioImage b
in Images)
3682 if (b.Filename == menuItem.Label)
3696 List<ROI> rois =
new List<ROI>();
3697 rois.AddRange(SelectedImage.AnnotationsR);
3698 rois.AddRange(SelectedImage.AnnotationsG);
3699 rois.AddRange(SelectedImage.AnnotationsB);
3700 foreach (ROI item
in rois)
3705 s += BioImage.ROIToString(item);
3708 Clipboard clipboard = Clipboard.Get(Gdk.Selection.Clipboard);
3715 Clipboard clipboard = Clipboard.Get(Gdk.Selection.Clipboard);
3716 string text = clipboard.WaitForText();
3717 string[] sts = text.Split(BioImage.NewLine);
3718 foreach (
string line
in sts)
3720 if (line.Length > 8)
3722 ROI an = BioImage.StringToROI(line);
3725 SelectedImage.Annotations.Add(an);
3732 public enum ViewMode
3739 bool openSlide =
false;
3740 public bool OpenSlide
3742 get {
return openSlide; }
3743 set { openSlide = value; }
3745 public static BioImage SelectedImage
3749 if (App.viewer ==
null)
3751 if (App.viewer.Images.Count == 0)
3753 return App.viewer.Images[App.viewer.SelectedIndex];
3759 if (App.viewer ==
null)
3760 App.viewer = ImageView.Create(value);
3761 App.viewer.Images[App.viewer.SelectedIndex] = value;
3764 private ViewMode viewMode = ViewMode.Filtered;
3766 public ViewMode Mode
3775 if (viewMode == ViewMode.RGBImage)
3777 rgbStack.VisibleChild = rgbStack.Children[1];
3780 if (viewMode == ViewMode.Filtered)
3782 rgbStack.VisibleChild = rgbStack.Children[0];
3785 if (viewMode == ViewMode.Raw)
3787 rgbStack.VisibleChild = rgbStack.Children[0];
3791 rgbStack.VisibleChild = rgbStack.Children[0];
3793 RefreshDisplayState(invalidatePyramidalTiles:
true);
3797 public Channel RChannel
3801 return SelectedImage.Channels[SelectedImage.rgbChannels[0]];
3805 public Channel GChannel
3809 return SelectedImage.Channels[SelectedImage.rgbChannels[1]];
3813 public Channel BChannel
3817 return SelectedImage.Channels[SelectedImage.rgbChannels[2]];
3820 PointD origin =
new PointD(0, 0);
3822 public PointD Origin
3824 get {
return origin; }
3827 if (AllowNavigation)
3833 glArea?.RequestRedraw();
3840 public PointD TopRightOrigin
3844 return new PointD((Origin.X - ((View.AllocatedWidth / 2) * pxWmicron)), (Origin.Y - ((View.AllocatedHeight / 2) * pxHmicron)));
3847 private bool _updatingScrollBars =
false;
3848 public PointD PyramidalOriginTransformed
3850 get {
return new PointD(PyramidalOrigin.X * Resolution, PyramidalOrigin.Y * Resolution); }
3851 set { PyramidalOrigin =
new PointD(value.X / Resolution, value.Y / Resolution); }
3855 public PointD PyramidalOrigin
3859 return SelectedImage.PyramidalOrigin;
3863 if (!AllowNavigation)
3865 SelectedImage.PyramidalOrigin = value;
3868 double viewWorldW = Math.Max(1, View?.AllocatedWidth ?? 1) * Resolution;
3869 double viewWorldH = Math.Max(1, View?.AllocatedHeight ?? 1) * Resolution;
3870 double worldRangeX = Math.Max(0, hScroll.Adjustment.Upper - hScroll.Adjustment.Lower);
3871 double worldRangeY = Math.Max(0, vScroll.Adjustment.Upper - vScroll.Adjustment.Lower);
3872 bool canPanX = SelectedImage?.isPyramidal ==
true && worldRangeX > viewWorldW;
3873 bool canPanY = SelectedImage?.isPyramidal ==
true && worldRangeY > viewWorldH;
3875 double maxX = Math.Max(hScroll.Adjustment.Lower, hScroll.Adjustment.Upper - hScroll.Adjustment.PageSize);
3876 double maxY = Math.Max(vScroll.Adjustment.Lower, vScroll.Adjustment.Upper - vScroll.Adjustment.PageSize);
3877 double clampedX = canPanX
3878 ? Math.Clamp(value.X, hScroll.Adjustment.Lower, maxX)
3880 double clampedY = canPanY
3881 ? Math.Clamp(value.Y, vScroll.Adjustment.Lower, maxY)
3884 _updatingScrollBars =
true;
3885 if (canPanX && hScroll.Value != clampedX)
3886 hScroll.Value = clampedX;
3887 if (canPanY && vScroll.Value != clampedY)
3888 vScroll.Value = clampedY;
3889 _updatingScrollBars =
false;
3891 if (_suppressViewUpdates)
3894 if (SelectedImage?.isPyramidal ==
true)
3901 slideRenderer?.InvalidateViewport();
3905 sKSlideRenderer?.InvalidateRenderState();
3908 else if (View !=
null)
3916 public void OnMouseWheel(
object sender, ScrollEventArgs e)
3918 double mouseX = e?.Event?.X ?? PyramidalOrigin.X;
3919 double mouseY = e?.Event?.Y ?? PyramidalOrigin.Y;
3920 if (e.Event.Direction == ScrollDirection.Up)
3921 ZoomAtPoint(mouseX, mouseY,
false);
3922 else if (e.Event.Direction == ScrollDirection.Down)
3923 ZoomAtPoint(mouseX, mouseY,
true);
3925 public void ZoomAtPoint(
double mouseX,
double mouseY,
bool zoomIn)
3927 if (SelectedImage ==
null || !SelectedImage.isPyramidal)
return;
3929 int currentLevel = Level;
3930 int nextLevel = zoomIn
3931 ? Math.Max(0, currentLevel - 1)
3932 : Math.Min(SelectedImage.Resolutions.Count - 1, currentLevel + 1);
3934 if (nextLevel == currentLevel)
3937 double currentResolution = SelectedImage.GetUnitPerPixel(currentLevel);
3938 double nextResolution = SelectedImage.GetUnitPerPixel(nextLevel);
3939 if (currentResolution <= 0 || nextResolution <= 0)
3944 SetResolution(nextResolution,
new PointD(mouseX, mouseY));
3945 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, mouseX, mouseY, zoomIn);
3947 public double Resolution
3951 if (!SelectedImage.isPyramidal)
3953 return SelectedImage.Resolution;
3957 if (SelectedImage.isPyramidal)
3958 SelectedImage.Resolution = value;
3959 if (!_suppressViewUpdates)
3961 if (SelectedImage?.isPyramidal ==
true)
3963 else if (View !=
null)
3974 glArea?.RequestRedraw();
3982 private void UpdateLevel()
3984 if (!SelectedImage.isPyramidal)
return;
3985 if (l != ImageView.SelectedImage.Level)
3988 for (
int j = 0; j < ImageView.SelectedImage.OpenSlideBase.stitch.gpuTiles.Count; j++)
3990 var tile = ImageView.SelectedImage.OpenSlideBase.stitch.gpuTiles[j];
3991 if (tile.Index.Level != Level)
3994 else if (ImageView.SelectedImage.SlideBase?.stitch !=
null)
3995 for (
int j = 0; j < ImageView.SelectedImage.SlideBase.stitch.gpuTiles.Count; j++)
3997 var tile = ImageView.SelectedImage.SlideBase.stitch.gpuTiles[j];
3998 if (tile.Index.Level != Level)
4001 l = ImageView.SelectedImage.Level;
4010 return SelectedImage.LevelFromResolution(SelectedImage.Resolution);
4016 if (SelectedImage?.Type == BioImage.ImageType.well)
4020 SelectedImage.Level = value;
4032 SelectedImage.Level = l;
4037 SizeF scale =
new SizeF(1, 1);
4053 if (SelectedImage.Buffers.Count == 0 &&
4054 SelectedImage.Type != BioImage.ImageType.well &&
4055 !SelectedImage.IsZarrSource)
4057 string formattedResolution = FormatResolution(Resolution);
4058 if (SelectedImage.Type == BioImage.ImageType.stack)
4059 statusLabel.Text = (zBar.Value + 1) +
"/" + (zBar.Adjustment.Upper + 1) +
", " + (cBar.Value + 1) +
"/" + (cBar.Adjustment.Upper + 1) +
", " + (tBar.Value + 1) +
"/" + (tBar.Adjustment.Upper + 1) +
", " +
4060 mousePoint + mouseColor +
", " + SelectedImage.Resolutions[0].PixelFormat.ToString() +
", (" + SelectedImage.Volume.Location.X.ToString(
"N2") +
", " + SelectedImage.Volume.Location.Y.ToString(
"N2") +
") "
4061 + Origin.X.ToString(
"N2") +
"," + Origin.Y.ToString(
"N2") +
", Res:" + formattedResolution +
" Level:" + Level;
4062 else if (SelectedImage.Type == BioImage.ImageType.pyramidal ||
4063 SelectedImage.Type == BioImage.ImageType.well)
4065 string wellStatus = SelectedImage.Type == BioImage.ImageType.well
4066 ?
", WellIndex:" + SelectedImage.WellIndex
4068 statusLabel.Text = (zBar.Value + 1) +
"/" + (zBar.Adjustment.Upper + 1) +
", " + (cBar.Value + 1) +
"/" + (cBar.Adjustment.Upper + 1) +
", " + (tBar.Value + 1) +
"/" + (tBar.Adjustment.Upper + 1) +
", " +
4069 mousePoint + mouseColor +
", " + SelectedImage.Resolutions[0].PixelFormat.ToString() +
", (" + SelectedImage.Volume.Location.X.ToString(
"N2") +
", " + SelectedImage.Volume.Location.Y.ToString(
"N2") +
") "
4070 + PyramidalOrigin.X.ToString(
"N2") +
"," + PyramidalOrigin.Y.ToString(
"N2") +
" , Level:" + Level +
" Res:" + formattedResolution + wellStatus;
4074 private static string FormatResolution(
double resolution)
4076 double magnitude = Math.Abs(resolution);
4077 if (magnitude >= 1000)
return resolution.ToString(
"F0");
4078 if (magnitude >= 100)
return resolution.ToString(
"F1");
4079 if (magnitude >= 10)
return resolution.ToString(
"F2");
4080 if (magnitude >= 1)
return resolution.ToString(
"F3");
4081 if (magnitude >= 0.1)
return resolution.ToString(
"F4");
4082 if (magnitude >= 0.01)
return resolution.ToString(
"F5");
4083 return resolution.ToString(
"G4");
4089 if (SelectedImage?.isPyramidal ==
true)
4091 RequestDeferredRender();
4096 RequestViewportRefresh();
4098 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, updateImages);
4100 private string mousePoint =
"";
4101 private string mouseColor =
"";
4102 private int _cursorPixelSampleRequestId;
4104 private void UpdateCursorPixelSample(PointD imagePoint)
4106 var image = SelectedImage;
4110 int pixelX = (int)Math.Floor(imagePoint.X);
4111 int pixelY = (int)Math.Floor(imagePoint.Y);
4112 if (pixelX < 0 || pixelY < 0 || pixelX >= image.SizeX || pixelY >= image.SizeY)
4114 Interlocked.Increment(ref _cursorPixelSampleRequestId);
4115 mouseColor =
string.Empty;
4119 if (image.IsZarrSource)
4121 QueueCursorPixelSample(imagePoint);
4127 ZCT coordinate = image.Coordinate;
4130 double red = image.GetValueRGB(coordinate.Z, RChannel.Index, coordinate.T,
4132 double green = image.GetValueRGB(coordinate.Z, GChannel.Index, coordinate.T,
4134 double blue = image.GetValueRGB(coordinate.Z, BChannel.Index, coordinate.T,
4136 mouseColor = $
", Val: ({FormatPixelValue(red)}, {FormatPixelValue(green)}, {FormatPixelValue(blue)})";
4140 double value = image.GetValueRGB(coordinate.Z, coordinate.C, coordinate.T,
4142 mouseColor = $
", Val: {FormatPixelValue(value)}";
4147 mouseColor =
string.Empty;
4151 private static string FormatPixelValue(
double value)
4153 return Math.Abs(value - Math.Round(value)) < 0.000001
4154 ? value.ToString(
"F0")
4155 : value.ToString(
"G6");
4158 private void QueueCursorPixelSample(PointD imagePoint)
4160 var image = SelectedImage;
4161 if (image?.IsZarrSource !=
true || slideRenderer ==
null)
4164 int pixelX = (int)Math.Floor(imagePoint.X);
4165 int pixelY = (int)Math.Floor(imagePoint.Y);
4166 int requestId = Interlocked.Increment(ref _cursorPixelSampleRequestId);
4167 if (pixelX < 0 || pixelY < 0 || pixelX >= image.SizeX || pixelY >= image.SizeY)
4169 mouseColor =
string.Empty;
4173 ZCT coordinate = image.Coordinate;
4174 _ = UpdateCursorPixelValueAsync(image, coordinate, pixelX, pixelY, requestId);
4177 private async Task UpdateCursorPixelValueAsync(
4188 await Task.Delay(60).ConfigureAwait(
false);
4189 if (requestId != _cursorPixelSampleRequestId || !ReferenceEquals(image, SelectedImage))
4192 double? value = await slideRenderer.SampleZarrPixelAsync(
4193 image, coordinate, pixelX, pixelY).ConfigureAwait(
false);
4194 if (!value.HasValue || requestId != _cursorPixelSampleRequestId ||
4195 !ReferenceEquals(image, SelectedImage))
4198 Gtk.Application.Invoke((s, e) =>
4200 if (requestId != _cursorPixelSampleRequestId || !ReferenceEquals(image, SelectedImage))
4202 mouseColor = $
", Val: {FormatPixelValue(value.Value)}";
4206 catch (Exception ex)
4208 AppLog.Append($
"Cursor Zarr sample failed: {ex.Message}");
4212 public bool showRROIs =
true;
4213 public bool showGROIs =
true;
4214 public bool showBROIs =
true;
4215 public bool ShowROIs {
get;
private set; } =
true;
4217 private void UpdateRoiMenuState()
4219 if (roiShow !=
null)
4220 roiShow.Sensitive = !ShowROIs;
4221 if (roiHide !=
null)
4222 roiHide.Sensitive = ShowROIs;
4225 public double GetScale()
4227 return ToViewSizeW(ROI.selectBoxSize / Scale.Width);
4229 public static bool x1State;
4230 public static bool x2State;
4231 public static bool mouseLeftState;
4232 public static ModifierType Modifiers;
4233 PointD mouseD =
new PointD(0, 0);
4235 PointD mouseDownInt =
new PointD(0, 0);
4236 PointD mouseMoveInt =
new PointD(0, 0);
4237 PointD mouseUpInt =
new PointD(0, 0);
4238 PointD mouseUp =
new PointD(0, 0);
4239 PointD mouseDown =
new PointD(0, 0);
4240 PointD mouseMove =
new PointD(0, 0);
4241 public ROI selectedROI =
new ROI();
4243 public PointD MouseDownInt
4245 get {
return mouseDownInt; }
4246 set { mouseDownInt = value; }
4248 public PointD MouseMoveInt
4250 get {
return mouseMoveInt; }
4251 set { mouseMoveInt = value; }
4253 public PointD MouseUpInt
4255 get {
return mouseUpInt; }
4256 set { mouseUpInt = value; }
4258 public PointD MouseDown
4260 get {
return mouseDown; }
4261 set { mouseDown = value; }
4263 public PointD MouseUp
4265 get {
return mouseUp; }
4266 set { mouseUp = value; }
4268 public PointD MouseMove
4270 get {
return mouseMove; }
4271 set { mouseMove = value; }
4274 List<ROI> copys =
new List<ROI>();
4275 public List<ROI> GetSelectedROIs()
4277 List<ROI> roi =
new List<ROI>();
4278 List<ROI> rois =
new List<ROI>();
4279 rois.AddRange(SelectedImage.AnnotationsR);
4280 rois.AddRange(SelectedImage.AnnotationsG);
4281 rois.AddRange(SelectedImage.AnnotationsB);
4282 foreach (ROI r
in rois)
4291 public void ZoomAtPoint(
float mouseX,
float mouseY,
bool zoomIn)
4293 if (SelectedImage ==
null)
return;
4296 float zoomFactor = zoomIn ? 2.0f : 0.5f;
4300 double currentLevelScale = (double)SelectedImage.Resolutions[SelectedImage.Level].SizeX / SelectedImage.SizeX;
4304 double worldX = (PyramidalOrigin.X + mouseX) / currentLevelScale;
4305 double worldY = (PyramidalOrigin.Y + mouseY) / currentLevelScale;
4312 double newLevelScale = (double)SelectedImage.Resolutions[SelectedImage.Level].SizeX / SelectedImage.SizeX;
4317 float newOriginX = (float)(worldX * newLevelScale - mouseX);
4318 float newOriginY = (float)(worldY * newLevelScale - mouseY);
4321 TryGetPyramidalViewerBounds(SelectedImage, out
double imageMinX, out
double imageMinY, out
double imageMaxX, out
double imageMaxY);
4322 double viewWorldW = Math.Max(1, View?.AllocatedWidth ?? 1) * Resolution;
4323 double viewWorldH = Math.Max(1, View?.AllocatedHeight ?? 1) * Resolution;
4324 PyramidalOrigin =
new PointD(
4325 ClampPyramidalOrigin(newOriginX, imageMinX, imageMaxX, viewWorldW),
4326 ClampPyramidalOrigin(newOriginY, imageMinY, imageMaxY, viewWorldH)
4328 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, mouseX, mouseY, zoomIn);
4331 private void ZoomStackAtPoint(PointD mouseScreenPos,
double factor)
4333 if (SelectedImage ==
null || SelectedImage.isPyramidal)
4335 if (factor <= 0 ||
double.IsNaN(factor) ||
double.IsInfinity(factor))
4338 double oldScaleX = Math.Max(
double.Epsilon, Scale.Width);
4339 double oldScaleY = Math.Max(
double.Epsilon, Scale.Height);
4342 double newScaleX = Math.Clamp(oldScaleX * factor, 1e-6, 4096.0);
4343 double newScaleY = Math.Clamp(oldScaleY * factor, 1e-6, 4096.0);
4345 double viewCenterX = View?.AllocatedWidth > 0 ? View.AllocatedWidth / 2.0 : 0;
4346 double viewCenterY = View?.AllocatedHeight > 0 ? View.AllocatedHeight / 2.0 : 0;
4348 double worldX = ((mouseScreenPos.X - viewCenterX) / (PxWmicron * oldScaleX)) - Origin.X;
4349 double worldY = ((mouseScreenPos.Y - viewCenterY) / (PxHmicron * oldScaleY)) - Origin.Y;
4351 Scale =
new SizeF((
float)newScaleX, (
float)newScaleY);
4353 double newOriginX = ((mouseScreenPos.X - viewCenterX) / (PxWmicron * newScaleX)) - worldX;
4354 double newOriginY = ((mouseScreenPos.Y - viewCenterY) / (PxHmicron * newScaleY)) - worldY;
4356 Origin =
new PointD(newOriginX, newOriginY);
4358 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, mouseScreenPos.X, mouseScreenPos.Y, factor);
4364 private void ImageView_MotionNotifyEvent(
object o, MotionNotifyEventArgs e)
4367 Modifiers = e.Event.State;
4370 PointD prevScreenPos = MouseMoveInt;
4372 MouseMove =
new PointD(e.Event.X, e.Event.Y);
4373 MouseMoveInt =
new PointD((
int)e.Event.X, (
int)e.Event.Y);
4377 mousePoint =
"(" + (p.X.ToString(
"F")) +
", " + (p.Y.ToString(
"F")) +
")";
4378 if (SelectedImage ==
null)
4380 mouseD = SelectedImage.ToImageSpace(p);
4381 UpdateCursorPixelSample(mouseD);
4382 if (e.Event.State.HasFlag(ModifierType.Button2Mask) &&
4383 Tools.currentTool.type != Tools.Tool.Type.pan)
4385 Tools.currentTool = Tools.GetTool(
"pan");
4389 if (e.Event.State.HasFlag(ModifierType.Button1Mask) &&
4390 TryNavigateFromOverview(e.Event.X, e.Event.Y))
4397 App.tools.ToolMove(p, e);
4402 private bool TryNavigateFromOverview(
double eventX,
double eventY)
4404 if (SelectedImage?.isPyramidal !=
true || !ShowOverview || overview.Width <= 0 || overview.Height <= 0)
4407 double overviewScale = Math.Max(1, View?.ScaleFactor ?? 1);
4408 double overviewX = eventX * overviewScale;
4409 double overviewY = eventY * overviewScale;
4410 if (!overview.IntersectsWith(overviewX, overviewY))
4413 TryGetPyramidalViewerBounds(SelectedImage, out
double imageMinX, out
double imageMinY, out
double imageMaxX, out
double imageMaxY);
4414 double fullResWidth = Math.Max(1, imageMaxX - imageMinX);
4415 double fullResHeight = Math.Max(1, imageMaxY - imageMinY);
4416 double viewWorldW = View.AllocatedWidth * Resolution;
4417 double viewWorldH = View.AllocatedHeight * Resolution;
4419 double fullResX = imageMinX + (((overviewX - overview.X) / overview.Width) * fullResWidth);
4420 double fullResY = imageMinY + (((overviewY - overview.Y) / overview.Height) * fullResHeight);
4422 double originX = fullResX - (viewWorldW / 2.0);
4423 double originY = fullResY - (viewWorldH / 2.0);
4425 PyramidalOrigin =
new PointD(
4426 ClampPyramidalOrigin(originX, imageMinX, imageMaxX, viewWorldW),
4427 ClampPyramidalOrigin(originY, imageMinY, imageMaxY, viewWorldH));
4434 private void ImageView_ButtonReleaseEvent(
object o, ButtonReleaseEventArgs e)
4436 Modifiers = e.Event.State;
4437 MouseUpInt =
new PointD((
int)e.Event.X, (
int)e.Event.Y);
4439 if (Tools.currentTool.type == Tools.Tool.Type.pan && e.Event.Button == 2)
4440 Tools.currentTool = Tools.GetTool(
"move");
4441 if (e.Event.Button == 1)
4442 mouseLeftState =
false;
4443 if (SelectedImage ==
null)
4449 if (SelectedImage ==
null)
4452 App.tools.ToolUp(pointer, e);
4458 private void ImageView_ButtonPressEvent(
object o, ButtonPressEventArgs e)
4460 Modifiers = e.Event.State;
4462 if (e.Event.Button == 1)
4463 mouseLeftState =
true;
4465 mouseLeftState =
false;
4466 if (SelectedImage ==
null)
4470 MouseDownInt =
new PointD(e.Event.X, e.Event.Y);
4473 if (e.Event.Button == 2)
4474 Tools.currentTool = Tools.GetTool(
"pan");
4475 if (SelectedImage ==
null)
4478 mouseD = SelectedImage.ToImageSpace(pd);
4480 if (e.Event.Button == 1 && TryNavigateFromOverview(e.Event.X, e.Event.Y))
4483 if (e.Event.Button == 1)
4484 TrySelectVisibleMaskAt(pointer.X, pointer.Y, e.Event.State.HasFlag(ModifierType.ControlMask));
4487 if (ImageView.SelectedImage.Type == BioImage.ImageType.well)
4489 if (e.Event.Button == 4)
4491 Level = ImageView.SelectedImage.Level - 1;
4493 else if (e.Event.Button == 5)
4495 Level = ImageView.SelectedImage.Level + 1;
4500 if (e.Event.Button == 3)
4502 contextMenu.Popup();
4507 if (e.Event.Button == 4 && Mode != ViewMode.RGBImage)
4509 if (cBar.Value < cBar.Adjustment.Upper)
4513 else if (e.Event.Button == 4 && e.Event.State == ModifierType.ControlMask)
4515 if (tBar.Value < tBar.Adjustment.Upper)
4520 if (e.Event.State != ModifierType.Button4Mask)
4523 if (e.Event.Button == 5 && Mode != ViewMode.RGBImage)
4525 if (cBar.Value > cBar.Adjustment.Lower)
4529 else if (e.Event.Button == 5 && Modifiers == ModifierType.ControlMask && Mode != ViewMode.RGBImage)
4531 if (tBar.Value > cBar.Adjustment.Lower)
4536 if (e.Event.State != ModifierType.Button5Mask)
4541 foreach (BioImage b
in Images)
4543 RectangleD r =
new RectangleD(b.Volume.Location.X, b.Volume.Location.Y,
4544 b.Volume.Width, b.Volume.Height);
4545 if (r.IntersectsWith(pointer))
4547 selectedIndex = ind;
4555 if (e.Event.Button == 1)
4556 UpdateCursorPixelSample(mouseD);
4559 App.tools.ToolDown(pointer, e);
4562 if (!(MacOS && SelectedImage?.isPyramidal ==
true))
4565 protected override void OnDestroyed()
4576 return View.AllocatedWidth;
4583 return View.AllocatedHeight;
4587 public static ROI SelectedAnnotation
4591 foreach (var item
in SelectedImage.Annotations)
4600 public double ImageViewWidth
4604 double d = View.AllocatedWidth / Resolution;
4608 public double ImageViewHeight
4612 double d = View.AllocatedHeight / Resolution;
4624 if (SelectedImage ==
null)
4626 if (SelectedImage.isPyramidal)
4631 return new PointD(PyramidalOrigin.X + (x * Resolution),
4632 PyramidalOrigin.Y + (y * Resolution));
4645 return new PointF((
float)d.X, (
float)d.Y);
4660 if (currentView !=
null)
4668 currentView = value;
4669 if (value is SKDrawingArea drawingArea)
4671 if (value is SlideGLArea slideGlArea)
4672 glArea = slideGlArea;
4677 if (SelectedImage ==
null)
4679 double dx = (ToViewSizeW(x - (View.AllocatedWidth / 2)) / Scale.Width) - Origin.X;
4680 double dy = (
ToViewSizeH(y - (View.AllocatedHeight / 2)) / Scale.Height) - Origin.Y;
4681 return new PointD(dx, dy);
4683 if (SelectedImage.isPyramidal)
4687 return new PointD((x - PyramidalOrigin.X) / Resolution,
4688 (y - PyramidalOrigin.Y) / Resolution);
4692 double dx = (ToViewSizeW(x - (View.AllocatedWidth / 2)) / Scale.Width) - Origin.X;
4693 double dy = (
ToViewSizeH(y - (View.AllocatedHeight / 2)) / Scale.Height) - Origin.Y;
4694 return new PointD(dx, dy);
4703 private double ToViewSizeW(
double d)
4705 if (SelectedImage !=
null)
4706 if (SelectedImage.isPyramidal)
4708 return d / Resolution;
4710 double x = (double)(d / PxWmicron);
4720 if (SelectedImage !=
null)
4721 if (SelectedImage.isPyramidal)
4723 return d / Resolution;
4725 double y = (double)(d / PxHmicron);
4735 if (SelectedImage !=
null)
4736 if (SelectedImage.isPyramidal)
4738 return d / Resolution;
4740 double x = (double)(d / PxWmicron) / scale.Width;
4750 if (SelectedImage !=
null)
4751 if (SelectedImage.isPyramidal)
4753 return d / Resolution;
4755 double y = (double)(d / PxHmicron) / scale.Height;
4767 return new PointD(f.X, f.Y);
4786 return new PointF((
float)pd.X, (
float)pd.Y);
4795 PointF[] pf =
new PointF[p.Length];
4796 for (
int i = 0; i < p.Length; i++)
4810 return new PointF((
float)pd.X, (
float)pd.Y);
4819 return (
float)(-x * PxWmicron * Scale.Width);
4828 return (
float)(-y * PxHmicron * Scale.Height);
4839 return new PointF(x, y);
4851 if (SelectedImage ==
null)
4853 double dx = (pxWmicron * (-Origin.X)) * Scale.Width;
4854 double dy = (pxHmicron * (-Origin.Y)) * Scale.Height;
4855 RectangleD rf =
new RectangleD((PxWmicron * x * Scale.Width + dx), (PxHmicron * y * Scale.Height + dy), (PxWmicron * w * Scale.Width), (PxHmicron * h * Scale.Height));
4858 if (SelectedImage.isPyramidal)
4861 double dw = ToViewSizeW(w);
4863 return new RectangleD(d.X, d.Y, dw, dh);
4867 double dx = (pxWmicron * (Origin.X)) * Scale.Width;
4868 double dy = (pxHmicron * (Origin.Y)) * Scale.Height;
4869 RectangleD rf =
new RectangleD((PxWmicron * x * Scale.Width + dx), (PxHmicron * y * Scale.Height + dy), (PxWmicron * w * Scale.Width), (PxHmicron * h * Scale.Height));
4899 RectangleD[] rs =
new RectangleD[p.Length];
4900 for (
int i = 0; i < p.Length; i++)
4914 PointD[] rs =
new PointD[p.Length];
4915 for (
int i = 0; i < p.Length; i++)
4918 rs[i] =
new PointD((
float)pd.X, (
float)pd.Y);
4929 return (
float)(x * PxWmicron);
4938 return (
float)(y * PxHmicron);
4946 private static bool IsZarrSource(BioImage image)
4951 string source = image.file;
4952 if (
string.IsNullOrWhiteSpace(source))
4953 source = image.Filename;
4955 return source.Contains(
".zarr", StringComparison.OrdinalIgnoreCase) ||
4956 source.StartsWith(
"https://", StringComparison.OrdinalIgnoreCase) ||
4957 source.StartsWith(
"s3://", StringComparison.OrdinalIgnoreCase);
4960 private bool TryGetPyramidalViewerBounds(BioImage image, out
double minX, out
double minY, out
double maxX, out
double maxY)
4970 minX = image.Volume.Location.X;
4971 minY = image.Volume.Location.Y;
4972 maxX = minX + image.Volume.Width;
4973 maxY = minY + image.Volume.Height;
4975 ITileSchema schema =
null;
4976 if (image == SelectedImage)
4979 ? _openSlideBase?.Schema
4980 : _slideBase?.Schema;
4983 schema ??= image.OpenSlideBase?.Schema;
4984 schema ??= image.SlideBase?.Schema;
4986 if (schema?.Extent !=
null)
4988 minX = schema.Extent.MinX;
4989 maxX = schema.Extent.MaxX;
4990 minY = -schema.Extent.MaxY;
4991 maxY = -schema.Extent.MinY;
5002 private static void NormalizeOsmSchemaOrigin(SlideBase slideBase, BioImage image =
null)
5004 if (slideBase?.Schema is not TileSchema schema)
5006 if (schema.YAxis != YAxis.OSM)
5009 BioImage bioImage = image ?? slideBase.Image?.BioImage;
5010 if (bioImage?.Type != BioImage.ImageType.well &&
5011 bioImage?.Resolutions !=
null &&
5012 bioImage.Resolutions.Count > 0 &&
5013 schema.Resolutions !=
null &&
5014 schema.Resolutions.Count > 0)
5016 var baseResolution = bioImage.Resolutions[0];
5017 double unitsPerPixel = schema.Resolutions.ContainsKey(0)
5018 ? schema.Resolutions[0].UnitsPerPixel
5019 : schema.Resolutions.OrderBy(kv => kv.Key).First().Value.UnitsPerPixel;
5021 if (unitsPerPixel > 0 && baseResolution.SizeX > 0 && baseResolution.SizeY > 0)
5023 double expectedWidth = baseResolution.SizeX * unitsPerPixel;
5024 double expectedHeight = baseResolution.SizeY * unitsPerPixel;
5025 double extentWidth = schema.Extent.MaxX - schema.Extent.MinX;
5026 double extentHeight = schema.Extent.MaxY - schema.Extent.MinY;
5028 if (Math.Abs(extentWidth - expectedWidth) > Math.Max(1e-6, expectedWidth * 0.001) ||
5029 Math.Abs(extentHeight - expectedHeight) > Math.Max(1e-6, expectedHeight * 0.001))
5031 double stageX = baseResolution.StageSizeX;
5032 double stageY = baseResolution.StageSizeY;
5033 schema.Extent =
new Extent(
5035 -(stageY + expectedHeight),
5036 stageX + expectedWidth,
5042 schema.OriginX = schema.Extent.MinX;
5043 schema.OriginY = schema.Extent.MaxY;
5046 private static double ClampPyramidalOrigin(
double requestedOrigin,
double imageMin,
double imageMax,
double viewWorldSize)
5048 double imageSize = Math.Max(1, imageMax - imageMin);
5049 if (viewWorldSize >= imageSize)
5050 return imageMin - ((viewWorldSize - imageSize) / 2.0);
5052 double maxOrigin = imageMax - viewWorldSize;
5053 return Math.Max(imageMin, Math.Min(requestedOrigin, maxOrigin));
5056 private void ForceNavigationRefresh()
5058 if (SelectedImage?.isPyramidal ==
true)
5062 slideRenderer?.InvalidateViewport();
5063 sKSlideRenderer?.InvalidateRenderState();
5064 ExecuteDeferredRender();
5065 ScheduleNavigationRenderRetry(50);
5066 ScheduleNavigationRenderRetry(150);
5072 glArea?.RequestRedraw();
5075 private void ScheduleNavigationRenderRetry(uint delayMs)
5077 GLib.Timeout.Add(delayMs, () =>
5079 if (_viewerCleanupDone || SelectedImage?.isPyramidal !=
true)
5082 ExecuteDeferredRender();
5085 glArea?.RequestRedraw();
5093 if (i < 0 || Images.Count <= i)
5095 if (_isApplyingGoToImage)
5097 _pendingGoToImageIndex = i;
5102 int vpW = View.AllocatedWidth;
5103 int vpH = View.AllocatedHeight;
5105 if (vpW < 10 || vpH < 10)
5107 void OnAllocated(
object sender, SizeAllocatedArgs args)
5109 if (args.Allocation.Width < 10 || args.Allocation.Height < 10)
5112 View.SizeAllocated -= OnAllocated;
5116 View.SizeAllocated += OnAllocated;
5120 bool imageChanged = selectedIndex != i;
5121 _isApplyingGoToImage =
true;
5122 _suppressViewUpdates =
true;
5133 BioImage currentImage = Images[i];
5134 if (currentImage ==
null)
5137 if (currentImage.isPyramidal)
5139 TryGetPyramidalViewerBounds(currentImage, out
double imageMinX, out
double imageMinY, out
double imageMaxX, out
double imageMaxY);
5141 double imageWorldW = imageMaxX - imageMinX;
5142 double imageWorldH = imageMaxY - imageMinY;
5143 if (imageWorldW <= 0) imageWorldW = 1;
5144 if (imageWorldH <= 0) imageWorldH = 1;
5146 double targetRes = Math.Max(imageWorldW / vpW, imageWorldH / vpH);
5147 if (targetRes <= 0) targetRes = 1;
5149 Resolution = targetRes;
5152 double viewWorldW = vpW * Resolution;
5153 double viewWorldH = vpH * Resolution;
5154 double originX = imageMinX - ((viewWorldW - imageWorldW) / 2.0);
5155 double originY = imageMinY - ((viewWorldH - imageWorldH) / 2.0);
5156 PyramidalOrigin =
new PointD(originX, originY);
5160 double imageWorldW = Math.Max(currentImage.Volume.Width, currentImage.SizeX * currentImage.PhysicalSizeX);
5161 double imageWorldH = Math.Max(currentImage.Volume.Height, currentImage.SizeY * currentImage.PhysicalSizeY);
5162 if (imageWorldW <= 0) imageWorldW = 1;
5163 if (imageWorldH <= 0) imageWorldH = 1;
5165 double wx = vpW / (PxWmicron * imageWorldW);
5166 double wy = vpH / (PxHmicron * imageWorldH);
5168 float fitScale = (float)Math.Min(wx, wy);
5169 if (fitScale <= 0) fitScale = 1;
5171 Scale =
new SizeF(fitScale, fitScale);
5173 double viewWorldW = vpW / (PxWmicron * fitScale);
5174 double viewWorldH = vpH / (PxHmicron * fitScale);
5175 double originX = -currentImage.Volume.Location.X + ((viewWorldW - imageWorldW) / 2.0);
5176 double originY = -currentImage.Volume.Location.Y + ((viewWorldH - imageWorldH) / 2.0);
5177 Origin =
new PointD(originX, originY);
5182 _suppressViewUpdates =
false;
5183 _isApplyingGoToImage =
false;
5186 ForceNavigationRefresh();
5187 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, i);
5189 if (_pendingGoToImageIndex.HasValue)
5191 int pendingIndex = _pendingGoToImageIndex.Value;
5192 _pendingGoToImageIndex =
null;
5193 if (pendingIndex != i)
5203 private double GetViewportFitResolution(
double targetResolution)
5205 if (!SelectedImage.isPyramidal || targetResolution <= 0)
5206 return targetResolution <= 0 ? 1 : targetResolution;
5208 var schema = openSlide
5209 ? _openSlideBase?.Schema
5210 : _slideBase?.Schema;
5212 if (schema?.Resolutions !=
null && schema.Resolutions.Count > 0)
5214 double? smallest =
null;
5215 double? best =
null;
5216 foreach (var kv
in schema.Resolutions.OrderBy(kv => kv.Value.UnitsPerPixel))
5218 double upx = kv.Value.UnitsPerPixel;
5220 if (upx >= targetResolution)
5229 if (schema.Resolutions.Count > 0)
5230 return schema.Resolutions.OrderByDescending(kv => kv.Value.UnitsPerPixel).First().Value.UnitsPerPixel;
5233 double fallback = SelectedImage.GetUnitPerPixel(SelectedImage.Resolutions.Count - 1);
5234 return fallback > 0 ? fallback : targetResolution;
5237 private double PickInitialResolution()
5240 int maxLevel = MacroResolution.HasValue
5241 ? MacroResolution.Value - 1
5242 : SelectedImage.Resolutions.Count - 1;
5243 maxLevel = Math.Clamp(maxLevel, 0, SelectedImage.Resolutions.Count - 1);
5246 var schema = openSlide
5247 ? (ITileSchema)_openSlideBase?.Schema
5248 : _slideBase?.Schema;
5250 if (schema?.Resolutions !=
null && schema.Resolutions.Count > 0)
5254 int schemaMax = Math.Min(maxLevel, schema.Resolutions.Keys.Max());
5255 return schema.Resolutions[schemaMax].UnitsPerPixel;
5259 return SelectedImage.GetUnitPerPixel(maxLevel);
5265 private OpenSlideBase _openSlideBase;
5266 private OpenSlideGTK.ISlideSource _openSlideSource;
5267 private BioLib.ISlideSource _slideSource;
5268 private SlideBase _slideBase;
5281 if (SelectedImage?.Type != BioImage.ImageType.well)
return;
5282 if (_slideBase ==
null)
return;
5284 _slideBase.RebuildSchemaForWell(SelectedImage);
5285 NormalizeOsmSchemaOrigin(_slideBase, SelectedImage);
5287 if (MacOS) sKSlideRenderer?.SetSource(_slideBase);
5288 else slideRenderer?.SetSource(_slideBase);
5292 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, SelectedImage?.file ??
string.Empty, SelectedImage.WellIndex);
5295 private void Initialize()
5300 if (SelectedImage?.isPyramidal ==
true)
5302 bool useSlideBase = SelectedImage.SlideBase !=
null;
5303 bool useOpenSlideBase = !useSlideBase && SelectedImage.OpenSlideBase !=
null;
5308 var sb = SelectedImage.SlideBase;
5309 _slideSource = SelectedImage.SlideBase;
5311 NormalizeOsmSchemaOrigin(sb, SelectedImage);
5312 var sbResolutions = sb.Schema?.Resolutions;
5313 if (sbResolutions !=
null && sbResolutions.Count > 0)
5314 Resolution = sbResolutions[sbResolutions.Count - 1].UnitsPerPixel;
5315 if (MacOS) sKSlideRenderer?.SetSource(sb);
5316 else slideRenderer?.SetSource(sb);
5318 if (SelectedImage.Type == BioImage.ImageType.well)
5320 RebuildSchemaForWell(SelectedImage);
5321 NormalizeOsmSchemaOrigin(sb, SelectedImage);
5324 else if (useOpenSlideBase)
5327 var sb = SelectedImage.OpenSlideBase;
5328 _openSlideSource = SelectedImage.OpenSlideBase;
5329 _openSlideBase = sb;
5330 var sbResolutions = sb.Schema?.Resolutions;
5331 if (sbResolutions !=
null && sbResolutions.Count > 0)
5332 Resolution = sbResolutions[sbResolutions.Count - 1].UnitsPerPixel;
5334 if (MacOS) sKSlideRenderer?.SetSource(sb);
5335 else slideRenderer?.SetSource(sb);
5337 if (SelectedImage.Type == BioImage.ImageType.well)
5339 RebuildSchemaForWell(SelectedImage);
5350 if (!SelectedImage.isPyramidal)
5351 Resolution = PickInitialResolution();
5354 public void RebuildSchemaForWell(BioImage b)
5356 if (b.ZarrWellLevels ==
null || b.ZarrWellLevels.Count == 0)
5360 int fi = Math.Clamp(b.Level, 0, b.ZarrWellLevels.Count - 1);
5361 var fieldLevels = b.ZarrWellLevels[fi];
5362 if (fieldLevels ==
null || fieldLevels.Count == 0)
5366 fieldLevels = b.ZarrWellLevels.FirstOrDefault(l => l !=
null && l.Count > 0);
5367 if (fieldLevels ==
null)
5374 GetFieldDims(fieldLevels[0], out w0, out h0);
5376 var newSchema =
new BruTile.TileSchema
5378 YAxis = BruTile.YAxis.OSM,
5380 Extent =
new BruTile.Extent(0, -h0, w0, 0),
5385 for (
int lev = 0; lev < fieldLevels.Count; lev++)
5387 GetFieldDims(fieldLevels[lev], out
int wL, out
int hL);
5388 double downsample = w0 > 0 ? (double)w0 / Math.Max(1, wL) : Math.Pow(2, lev);
5389 newSchema.Resolutions[lev] =
new BruTile.Resolution(lev, downsample, 256, 256);
5392 var slideBase = SelectedImage?.OpenSlideBase;
5393 if (slideBase ==
null)
5398 slideBase.Schema = newSchema;
5399 BioLib.Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, b?.file ??
string.Empty, b?.Level ?? 0);
5402 private static void GetFieldDims(ResolutionLevelNode node, out
int w, out
int h)
5405 var axes = node.EffectiveAxes;
5406 var shape = node.Shape;
5407 for (
int i = 0; i < axes.Length; i++)
5409 switch (axes[i].Name.ToLowerInvariant())
5411 case "x": w = (int)shape[i];
break;
5412 case "y": h = (int)shape[i];
break;