BioCore  5.0.0
A Windows .NET microscopy library and application for viewing, editing, and annotating images.
Loading...
Searching...
No Matches
ImageView.Overview.cs
Go to the documentation of this file.
1using AForge;
2using System.ComponentModel;
3using Drawing = System.Drawing;
4
5namespace BioCore
6{
7 public partial class ImageView
8 {
9 private readonly PictureBox overviewPanel = new PictureBox();
10 private BioImage overviewSource;
11 private BioImage previewRequestImage;
12 private (int Z, int C, int T) previewRequestCoordinate;
13 private int previewVersion;
14 private bool overviewDragging;
15 private bool showOverview = true;
16
17 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
18 public bool ShowOverview
19 {
20 get => showOverview;
21 set
22 {
23 showOverview = value;
24 UpdateOverviewVisibility();
25 }
26 }
27
28 private void InitializeOverview()
29 {
30 overviewPanel.Name = "overviewPanel";
31 overviewPanel.BackColor = Drawing.Color.Black;
32 overviewPanel.Cursor = Cursors.Cross;
33 overviewPanel.Visible = false;
34 overviewPanel.ContextMenuStrip = contextMenuStrip;
35 overviewPanel.Paint += OverviewPaint;
36 overviewPanel.MouseDown += OverviewMouseDown;
37 overviewPanel.MouseMove += OverviewMouseMove;
38 overviewPanel.MouseUp += OverviewMouseUp;
39 overviewPanel.MouseWheel += ImageView_MouseWheel;
40 overviewPanel.MouseCaptureChanged += (_, _) =>
41 {
42 if (!overviewPanel.Capture)
43 overviewDragging = false;
44 };
45 viewport.Controls.Add(overviewPanel);
46 Disposed += (_, _) =>
47 {
48 previewVersion++;
49 overviewImage?.Dispose();
50 overviewImage = null;
51 };
52 }
53
54 private BioImage OverviewSelectedImage =>
55 SelectedIndex >= 0 && SelectedIndex < Images.Count ? Images[SelectedIndex] : null;
56
57 private void UpdateOverviewVisibility()
58 {
59 if (overviewPanel.IsDisposed)
60 return;
61 overviewPanel.Visible = ShowOverview && overviewImage != null &&
62 overviewSource == OverviewSelectedImage;
63 if (overviewPanel.Visible)
64 overviewPanel.BringToFront();
65 }
66
67 private void SetOverviewPreview(Drawing.Image source, BioImage image)
68 {
69 int width = image.isPyramidal ? image.Resolutions[0].SizeX : source.Width;
70 int height = image.isPyramidal ? image.Resolutions[0].SizeY : source.Height;
71 double ratio = 160.0 / Math.Max(width, height);
72 int previewWidth = Math.Max(1, (int)Math.Round(width * ratio));
73 int previewHeight = Math.Max(1, (int)Math.Round(height * ratio));
74 var thumbnail = new Drawing.Bitmap(previewWidth, previewHeight);
75 using (var graphics = Drawing.Graphics.FromImage(thumbnail))
76 {
77 graphics.InterpolationMode = Drawing.Drawing2D.InterpolationMode.HighQualityBilinear;
78 graphics.DrawImage(source, 0, 0, previewWidth, previewHeight);
79 }
80 overviewImage?.Dispose();
81 overviewImage = thumbnail;
82 overviewSource = image;
83 overview = new AForge.Rectangle(8, 8, previewWidth, previewHeight);
84 overviewPanel.SetBounds(overview.X, overview.Y, overview.Width, overview.Height);
85 UpdateOverviewVisibility();
86 overviewPanel.Invalidate();
87 }
88
89 private void UpdateRegularOverview()
90 {
91 BioImage image = OverviewSelectedImage;
92 if (image == null || image.isPyramidal || SelectedIndex >= Bitmaps.Count)
93 return;
94 previewVersion++;
95 previewRequestImage = null;
96 SetOverviewPreview(Bitmaps[SelectedIndex], image);
97 }
98
99 private async void InitPreview()
100 {
101 BioImage image = OverviewSelectedImage;
102 if (image == null || !image.isPyramidal)
103 return;
104 var coordinate = (image.Coordinate.Z, image.Coordinate.C, image.Coordinate.T);
105 if (previewRequestImage == image && previewRequestCoordinate == coordinate)
106 return;
107 previewRequestImage = image;
108 previewRequestCoordinate = coordinate;
109 int version = ++previewVersion;
110 UpdateOverviewVisibility();
111 try
112 {
113 if (image.OpenSlideBase != null)
114 {
115 // OpenSlideBase.GetTile returns a schema-sized tile, not an arbitrary
116 // region. Choose a level that retains enough pixels for the overview.
117 var slide = image.OpenSlideBase;
118 var levels = slide.Schema.Resolutions.OrderBy(r => r.Value.UnitsPerPixel).ToArray();
119 var levelInfo = levels.Where(r =>
120 Math.Max(slide.Schema.Extent.Width, slide.Schema.Extent.Height) /
121 r.Value.UnitsPerPixel >= 160).DefaultIfEmpty(levels[0]).Last();
122 int width = (int)Math.Ceiling(slide.Schema.Extent.Width / levelInfo.Value.UnitsPerPixel);
123 int height = (int)Math.Ceiling(slide.Schema.Extent.Height / levelInfo.Value.UnitsPerPixel);
124 if ((long)width * height > 16_000_000)
125 return;
126 using Drawing.Bitmap slidePreview = SlideOverviewBuilder.Build(width, height,
127 levelInfo.Value.TileWidth, levelInfo.Value.TileHeight,
128 (column, row) => slide.GetTile(new BruTile.TileInfo
129 {
130 Index = new BruTile.TileIndex(column, row, levelInfo.Key)
131 }));
132 if (version == previewVersion && !IsDisposed && !Disposing && image == OverviewSelectedImage)
133 SetOverviewPreview(slidePreview, image);
134 return;
135 }
136 // Use the smallest slide level, never the label or macro image.
137 int level = image.Resolutions.Count - 1;
138 var resolution = image.Resolutions[level];
139 if ((long)resolution.SizeX * resolution.SizeY > 16_000_000)
140 return;
141 int frame = image.GetFrameIndex(coordinate.Z, coordinate.C, coordinate.T);
142 using Bitmap tile = await image.GetTile(frame, level, 0, 0, resolution.SizeX, resolution.SizeY);
143 if (tile == null || version != previewVersion || IsDisposed || Disposing ||
144 image != OverviewSelectedImage)
145 return;
146 using Drawing.Bitmap display = ToSysBitmap(tile);
147 SetOverviewPreview(display, image);
148 }
149 catch (Exception ex)
150 {
151 System.Diagnostics.Trace.WriteLine($"Could not load slide overview: {ex.Message}");
152 }
153 }
154
155 private RectangleD OverviewWorldBounds(BioImage image)
156 {
157 if (image.isPyramidal)
158 {
159 // The renderer addresses the tile schema's world coordinates, which
160 // may use a different calibration from BioImage's metadata.
161 var extent = image.OpenSlideBase?.Schema.Extent ?? image.SlideBase?.Schema.Extent;
162 if (extent is { } schemaExtent)
163 return new RectangleD(schemaExtent.MinX, -schemaExtent.MaxY, schemaExtent.Width, schemaExtent.Height);
164 double units = image.GetUnitPerPixel(0);
165 return new RectangleD(0, 0, image.Resolutions[0].SizeX * units,
166 image.Resolutions[0].SizeY * units);
167 }
168 return new RectangleD(image.Volume.Location.X, image.Volume.Location.Y,
169 image.Volume.Width, image.Volume.Height);
170 }
171
172 private void OverviewPaint(object sender, PaintEventArgs e)
173 {
174 BioImage image = OverviewSelectedImage;
175 if (overviewImage == null || image == null || overviewSource != image)
176 return;
177 e.Graphics.DrawImageUnscaled(overviewImage, 0, 0);
178 RectangleD bounds = OverviewWorldBounds(image);
179 if (bounds.W <= 0 || bounds.H <= 0)
180 return;
181 // Use this viewer's transform even when a different tab is active.
182 PointD topLeft = image.isPyramidal
183 ? new PointD(image.PyramidalOrigin.X * image.Resolution, image.PyramidalOrigin.Y * image.Resolution)
184 : new PointD(-ViewWidth / 2 / (PxWmicron * Scale.Width) - Origin.X,
185 -ViewHeight / 2 / (PxHmicron * Scale.Height) - Origin.Y);
186 double viewWidth = image.isPyramidal ? ViewWidth * image.Resolution : ViewWidth / (PxWmicron * Scale.Width);
187 double viewHeight = image.isPyramidal ? ViewHeight * image.Resolution : ViewHeight / (PxHmicron * Scale.Height);
188 double left = Math.Clamp((topLeft.X - bounds.X) / bounds.W, 0, 1);
189 double top = Math.Clamp((topLeft.Y - bounds.Y) / bounds.H, 0, 1);
190 double right = Math.Clamp((topLeft.X + viewWidth - bounds.X) / bounds.W, 0, 1);
191 double bottom = Math.Clamp((topLeft.Y + viewHeight - bounds.Y) / bounds.H, 0, 1);
192 if (right > left && bottom > top)
193 {
194 using var pen = new Drawing.Pen(Drawing.Color.OrangeRed, 2);
195 e.Graphics.DrawRectangle(pen, (float)(left * (overview.Width - 1)),
196 (float)(top * (overview.Height - 1)), (float)((right - left) * (overview.Width - 1)),
197 (float)((bottom - top) * (overview.Height - 1)));
198 }
199 e.Graphics.DrawRectangle(Drawing.Pens.White, 0, 0, overview.Width - 1, overview.Height - 1);
200 }
201
202 private void NavigateOverview(int x, int y)
203 {
204 BioImage image = OverviewSelectedImage;
205 if (image == null || overviewImage == null || overviewSource != image)
206 return;
207 App.viewer = this;
208 RectangleD bounds = OverviewWorldBounds(image);
209 double targetX = bounds.X + Math.Clamp(x / (double)overview.Width, 0, 1) * bounds.W;
210 double targetY = bounds.Y + Math.Clamp(y / (double)overview.Height, 0, 1) * bounds.H;
211 if (image.isPyramidal)
212 {
213 PyramidalOrigin = new PointD(targetX / Resolution - ViewWidth / 2.0,
214 targetY / Resolution - ViewHeight / 2.0);
215 UpdateImages();
216 }
217 else
218 Origin = new PointD(-targetX, -targetY);
219 UpdateView();
220 }
221
222 private MouseEventArgs OverviewCanvasEvent(MouseEventArgs e) =>
223 new MouseEventArgs(e.Button, e.Clicks, e.X + overview.X, e.Y + overview.Y, e.Delta);
224
225 private void OverviewMouseDown(object sender, MouseEventArgs e)
226 {
227 if (e.Button == MouseButtons.Left)
228 {
229 overviewDragging = true;
230 overviewPanel.Capture = true;
231 NavigateOverview(e.X, e.Y);
232 }
233 else if (e.Button == MouseButtons.Middle)
234 pictureBox_MouseDown(sender, OverviewCanvasEvent(e));
235 }
236
237 private void OverviewMouseMove(object sender, MouseEventArgs e)
238 {
239 if (overviewDragging && e.Button.HasFlag(MouseButtons.Left))
240 NavigateOverview(e.X, e.Y);
241 else if (e.Button == MouseButtons.Middle)
242 rgbPictureBox_MouseMove(sender, OverviewCanvasEvent(e));
243 }
244
245 private void OverviewMouseUp(object sender, MouseEventArgs e)
246 {
247 if (e.Button == MouseButtons.Left)
248 {
249 overviewDragging = false;
250 overviewPanel.Capture = false;
251 }
252 else if (e.Button == MouseButtons.Middle)
253 pictureBox_MouseUp(sender, OverviewCanvasEvent(e));
254 }
255 }
256}
System.Drawing Drawing
AForge.RectangleD RectangleD
Definition ImageView.cs:17
System.Drawing.Bitmap overviewImage
Definition ImageView.cs:365
new AForge.SizeF Scale
Scale when opening image stacks.
Definition ImageView.cs:805
PointD PyramidalOrigin
Setting the value of the pyramidalOrigin variable. *‍/.
Definition ImageView.cs:755
List< BioImage > Images
Definition ImageView.cs:359
double PxWmicron
Only used when opening stacks.
Definition ImageView.cs:599
double Resolution
A property of the class ImageViewer. It is a getter and setter for the resolution of the image.
Definition ImageView.cs:770
PointD Origin
A property of the class PointD. *‍/.
Definition ImageView.cs:743
async void UpdateImages(bool updatePyramidal=true)
double PxHmicron
Only used when opening stacks.
Definition ImageView.cs:614
int SelectedIndex
A property that is used to set the selected index of the image. *‍/.
Definition ImageView.cs:406