BioGTK  6.7.0
A .NET library & program for annotating, editing various microscopy imaging formats using Bioformats supported images. Including whole slide, pyramidal, and series.
Loading...
Searching...
No Matches
HistogramControl.cs
1using Gtk;
2using System;
3using System.Collections.Generic;
4using System.ComponentModel;
5using System.Data;
6using System.Linq;
7using System.Text;
8using System.Threading.Tasks;
9using AForge;
10
11using Cairo;
12using Color = AForge.Color;
13using Gdk;
14using System.IO;
15
16namespace BioGTK
17{
18 public class HistogramControl : Gtk.Window
19 {
20
21 #region Properties
22
24 private Builder _builder;
25
26#pragma warning disable 649
27 [Builder.Object]
28 private MenuItem setMin;
29 [Builder.Object]
30 private MenuItem setMax;
31 [Builder.Object]
32 private MenuItem setMinAll;
33 [Builder.Object]
34 private MenuItem setMaxAll;
35 [Builder.Object]
36 private DrawingArea view;
37 [Builder.Object]
38 private Menu menu;
39#pragma warning restore 649
40
41 #endregion
42
43 #region Constructors / Destructors
44 public static HistogramControl Create(Channel channel)
45 {
46 Builder builder = new Builder(new FileStream(System.IO.Path.GetDirectoryName(Environment.ProcessPath) + "/" + "Glade/Histogram.glade", FileMode.Open));
47 return new HistogramControl(builder, builder.GetObject("histogram").Handle,channel);
48 }
49 protected HistogramControl(Builder builder, IntPtr handle, Channel channel) : base(handle)
50 {
51 _builder = builder;
52 this.channel = channel;
53 builder.Autoconnect(this);
54 Init();
55 }
56 #endregion
57
58 public Channel channel = null;
59 private const double HistogramBarOpacity = 0.55;
60 private readonly Dictionary<Channel, Cairo.Color> _generatedChannelColors = new();
62 public void Init()
63 {
64 if (ImageView.SelectedImage.bitsPerPixel == 8)
65 {
66 graphMax = 255;
67 Bin = 1;
68 }
69 else
70 {
71 graphMax = ushort.MaxValue;
72 Bin = 1;
73 }
74 view.Drawn += HistogramControl_Drawn;
75 this.MotionNotifyEvent += HistogramControl_MotionNotifyEvent;
76 this.ButtonPressEvent += HistogramControl_ButtonPressEvent;
77 this.ScrollEvent += HistogramControl_ScrollEvent;
78 this.DeleteEvent += HistogramControl_DeleteEvent;
79 setMin.ButtonPressEvent += SetMin_ButtonPressEvent;
80 setMax.ButtonPressEvent += SetMax_ButtonPressEvent;
81 setMinAll.ButtonPressEvent += SetMinAll_ButtonPressEvent;
82 setMaxAll.ButtonPressEvent += SetMaxAll_ButtonPressEvent;
83 this.AddEvents((int)
84 (EventMask.ButtonPressMask
85 | EventMask.ButtonReleaseMask
86 | EventMask.KeyPressMask
87 | EventMask.PointerMotionMask | EventMask.ScrollMask));
88 }
89
97 private void HistogramControl_DeleteEvent(object o, DeleteEventArgs args)
98 {
99 args.RetVal = true;
100 Hide();
101 }
102
108 private void HistogramControl_ScrollEvent(object o, ScrollEventArgs args)
109 {
110 if(args.Event.Direction == Gdk.ScrollDirection.Up)
111 {
112 graphMax += 400;
113 }
114 else if (args.Event.Direction == Gdk.ScrollDirection.Down)
115 {
116 graphMax -= 400;
117 }
118 UpdateView();
119 }
120
126 private void SetMaxAll_ButtonPressEvent(object o, ButtonPressEventArgs args)
127 {
128 foreach (Channel c in ImageView.SelectedImage.Channels)
129 {
130 for (int i = 0; i < c.range.Length; i++)
131 {
132 c.range[i].Max = (int)MouseValX;
133 }
134 }
135 this.QueueDraw();
136 App.viewer.UpdateImage();
137 }
138
144 private void SetMinAll_ButtonPressEvent(object o, ButtonPressEventArgs args)
145 {
146 foreach (Channel c in ImageView.SelectedImage.Channels)
147 {
148 for (int i = 0; i < c.range.Length; i++)
149 {
150 c.range[i].Min = (int)MouseValX;
151 }
152 }
153 this.QueueDraw();
154 App.viewer.UpdateImage();
155 }
156
163 private void SetMax_ButtonPressEvent(object o, ButtonPressEventArgs args)
164 {
165 App.channelsTool.SelectedChannel.range[App.channelsTool.SelectedSample].Max = (int)MouseValX;
166 this.QueueDraw();
167 App.viewer.UpdateImage();
168 }
169
176 private void SetMin_ButtonPressEvent(object o, ButtonPressEventArgs args)
177 {
178 App.channelsTool.SelectedChannel.range[App.channelsTool.SelectedSample].Min = (int)MouseValX;
179 this.QueueDraw();
180 App.viewer.UpdateImage();
181 }
182
187 public void HistogramControl_ButtonPressEvent(object o, ButtonPressEventArgs args)
188 {
189 mouseX = (int)args.Event.X;
190 mouseY = (int)args.Event.Y;
191 this.QueueDraw();
192 if(args.Event.Button == 3)
193 menu.Popup();
194 }
195
201 public void HistogramControl_MotionNotifyEvent(object o, MotionNotifyEventArgs args)
202 {
203 mouseValX = (float)args.Event.X / fx;
204 mouseValY = (float)args.Event.Y / fy;
205 }
206
207 public Cairo.Color ToCairoColor(Color c)
208 {
209 return new Cairo.Color((double)c.R / 255.0, (double)c.G / 255.0, (double)c.B / 255.0, (double)c.A / 255.0);
210 }
211
216 private void HistogramControl_Drawn(object o, DrawnArgs args)
217 {
218 Cairo.Context g = args.Cr;
219 var image = ImageView.SelectedImage;
220 int drawingWidth = Math.Max(1, view?.AllocatedWidth ?? AllocatedWidth);
221 int drawingHeight = Math.Max(1, view?.AllocatedHeight ?? AllocatedHeight);
222 g.SetSourceRGB(0.82, 0.82, 0.82);
223 g.Paint();
224
225 if (!HasUsableStatistics(image))
226 {
227 g.SetSourceRGB(0.2, 0.2, 0.2);
228 g.MoveTo(12, Math.Max(22, drawingHeight / 2));
229 g.ShowText("Statistics are not available for this image.");
230 g.Stroke();
231 args.RetVal = true;
232 return;
233 }
234
235 if (graphMax == 0)
236 graphMax = ushort.MaxValue;
237 g.SetSourceColor(ToCairoColor(Color.LightGray));
238 //g.Restore();
239 //g.Translate(-graphMin, 0);
240 g.LineWidth = 1;
241 string st = "";
242 fx = ((float)drawingWidth) / ((float)graphMax);
243 float maxmedian = 0;
244 int maxChan = 0;
245 for (int cc = 0; cc < image.Channels.Count; cc++)
246 {
247 for (int i = 0; i < image.Channels[cc].stats.Length; i++)
248 {
249 Statistics channelStatistics = image.Channels[cc].stats[i];
250 if (channelStatistics == null)
251 continue;
252 float f = GetHistogramMaximum(channelStatistics.StackValues);
253 if (maxmedian < f)
254 {
255 maxmedian = f;
256 maxChan = cc;
257 }
258 }
259 }
260 if (maxmedian <= 0)
261 {
262 g.SetSourceRGB(0.2, 0.2, 0.2);
263 g.MoveTo(12, Math.Max(22, drawingHeight / 2));
264 g.ShowText("Histogram contains no sample counts.");
265 g.Stroke();
266 args.RetVal = true;
267 return;
268 }
269 fy = ((float)drawingHeight) / maxmedian;
270 for (int c = 0; c < image.Channels.Count; c++)
271 {
272 //We draw the mouse line
273
274 g.SetSourceColor(ToCairoColor(Color.Black));
275 g.MoveTo(mouseX, 0);
276 g.LineTo(mouseX, drawingHeight);
277 g.Stroke();
278 int gmax = graphMax;
279 int gmin = graphMin;
280 Channel channel = image.Channels[c];
281 for (int i = 0; i < channel.range.Length; i++)
282 {
283 Statistics stat;
284 int statisticsIndex = image.Channels.Count == 1 && image.RGBChannelCount > 1
285 ? Math.Clamp(i, 0, channel.stats.Length - 1)
286 : 0;
287 stat = channel.stats[statisticsIndex];
288 g.LineWidth = 1;// bin * fx;
289 Cairo.Color blackd = new Cairo.Color(0, 0, 0, 0.35);
290 if (ImageView.SelectedImage.bitsPerPixel <= 8)
291 gmax = 255;
292
293 Cairo.Color barColor = GetChannelBarColor(image, channel, c, i);
294
295 int plotWidth = drawingWidth;
296 int plotHeight = drawingHeight;
297 int valueRange = Math.Max(1, gmax - gmin);
298
299 if (StackHistogram && c == image.Channels.Count - 1 && i == 0 &&
300 image.Statistics?.StackValues != null)
301 {
302 g.SetSourceColor(blackd);
303 DrawBinnedHistogram(g, image.Statistics.StackValues,
304 gmin, valueRange, plotWidth, plotHeight, maxmedian);
305 }
306
307 // Aggregate every source bin covered by a screen pixel. The
308 // former loop sampled one isolated value per pixel and skipped
309 // nearly all peaks in a 16-bit (65,536-bin) histogram.
310 g.SetSourceColor(barColor);
311 DrawBinnedHistogram(g, stat.StackValues,
312 gmin, valueRange, plotWidth, plotHeight, maxmedian);
313 Cairo.Color markerColor = barColor;
314 markerColor.A = 1.0;
315 g.SetSourceColor(markerColor);
316 g.MoveTo((fx * channel.range[i].Max), 0);
317 g.LineTo((fx * channel.range[i].Max), drawingHeight);
318 g.Stroke();
319 g.MoveTo((fx * channel.range[i].Min), 0);
320 g.LineTo((fx * channel.range[i].Min), drawingHeight);
321 g.Stroke();
322 }
323
324 }
325
326 float tick = 6;
327 if (axisTicks)
328 {
329 g.SetSourceRGB(0, 0, 0);
330 int axisRange = Math.Max(1, graphMax - graphMin);
331 int majorStep = GetNiceTickStep(axisRange, 6);
332 int firstTick = (int)Math.Ceiling((double)graphMin / majorStep) * majorStep;
333 for (int value = firstTick; value <= graphMax; value += majorStep)
334 {
335 double x = (double)(value - graphMin) * drawingWidth / axisRange;
336 g.MoveTo(x, 0);
337 g.LineTo(x, tick);
338 g.Stroke();
339 if (axisNumbers)
340 {
341 string label = value.ToString();
342 TextExtents extents = g.TextExtents(label);
343 g.MoveTo(x - (extents.Width / 2), tick + 11);
344 g.ShowText(label);
345 g.Stroke();
346 }
347 }
348 }
349
350 int hoverBin = graphMin + (int)((long)Math.Max(0, mouseX) *
351 Math.Max(1, graphMax - graphMin) / drawingWidth);
352 if (image.RGBChannelCount == 3)
353 {
354 float red;
355 float green;
356 float blue;
357 if (image.Channels.Count > 2)
358 {
359 // Separate logical channels each carry one statistics plane.
360 red = GetHistogramValue(image.Channels[2], 0, hoverBin);
361 green = GetHistogramValue(image.Channels[1], 0, hoverBin);
362 blue = GetHistogramValue(image.Channels[0], 0, hoverBin);
363 }
364 else
365 {
366 // Packed/planar RGB is represented by one logical channel with
367 // three statistics planes in B, G, R order.
368 red = GetHistogramValue(image.Channels[0], 2, hoverBin);
369 green = GetHistogramValue(image.Channels[0], 1, hoverBin);
370 blue = GetHistogramValue(image.Channels[0], 0, hoverBin);
371 }
372 st = $"({hoverBin},R:{red},G:{green},B:{blue})";
373 }
374 else
375 {
376 st = $"({hoverBin},{GetHistogramValue(image.Channels[0], 0, hoverBin)})";
377 }
378 g.MoveTo(mouseX, mouseY);
379 g.ShowText(st);
380 g.Stroke();
381 }
382
383 private static float GetHistogramMaximum(float[] values)
384 {
385 if (values == null || values.Length == 0)
386 return 0;
387
388 float maximum = 0;
389 for (int i = 0; i < values.Length; i++)
390 if (values[i] > maximum)
391 maximum = values[i];
392 return maximum;
393 }
394
395 private Cairo.Color GetChannelBarColor(
396 BioImage image,
397 Channel imageChannel,
398 int channelIndex,
399 int sampleIndex)
400 {
401 // A single packed RGB channel contains three component histograms.
402 if (image.Channels.Count == 1 && image.RGBChannelCount == 3)
403 {
404 return sampleIndex switch
405 {
406 0 => new Cairo.Color(0.1, 0.35, 1.0, HistogramBarOpacity),
407 1 => new Cairo.Color(0.1, 0.85, 0.2, HistogramBarOpacity),
408 _ => new Cairo.Color(1.0, 0.15, 0.1, HistogramBarOpacity)
409 };
410 }
411
412 if (imageChannel.Color.HasValue)
413 {
414 Color specified = imageChannel.Color.Value;
415 return new Cairo.Color(
416 specified.R / 255.0,
417 specified.G / 255.0,
418 specified.B / 255.0,
419 HistogramBarOpacity);
420 }
421
422 if (_generatedChannelColors.TryGetValue(imageChannel, out Cairo.Color generated))
423 return generated;
424
425 generated = CreateRandomVisibleColor();
426 _generatedChannelColors[imageChannel] = generated;
427 return generated;
428 }
429
430 private static Cairo.Color CreateRandomVisibleColor()
431 {
432 double hue = Random.Shared.NextDouble() * 6.0;
433 int sector = (int)Math.Floor(hue);
434 double fraction = hue - sector;
435 const double value = 0.9;
436 const double saturation = 0.75;
437 double low = value * (1.0 - saturation);
438 double falling = value * (1.0 - saturation * fraction);
439 double rising = value * (1.0 - saturation * (1.0 - fraction));
440
441 return sector switch
442 {
443 0 => new Cairo.Color(value, rising, low, HistogramBarOpacity),
444 1 => new Cairo.Color(falling, value, low, HistogramBarOpacity),
445 2 => new Cairo.Color(low, value, rising, HistogramBarOpacity),
446 3 => new Cairo.Color(low, falling, value, HistogramBarOpacity),
447 4 => new Cairo.Color(rising, low, value, HistogramBarOpacity),
448 _ => new Cairo.Color(value, low, falling, HistogramBarOpacity)
449 };
450 }
451
452 private static int GetNiceTickStep(int range, int targetTickCount)
453 {
454 double roughStep = Math.Max(1, (double)range / Math.Max(1, targetTickCount));
455 double magnitude = Math.Pow(10, Math.Floor(Math.Log10(roughStep)));
456 double normalized = roughStep / magnitude;
457 double nice = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
458 return Math.Max(1, (int)(nice * magnitude));
459 }
460
461 private static void DrawBinnedHistogram(
462 Cairo.Context context,
463 float[] values,
464 int graphMinimum,
465 int graphRange,
466 int plotWidth,
467 int plotHeight,
468 float histogramMaximum)
469 {
470 if (values == null || values.Length == 0 || histogramMaximum <= 0)
471 return;
472
473 double logMaximum = Math.Log(1.0 + histogramMaximum);
474 for (int pixelX = 0; pixelX < plotWidth; pixelX++)
475 {
476 int start = graphMinimum + (int)((long)pixelX * graphRange / plotWidth);
477 int end = graphMinimum + (int)((long)(pixelX + 1) * graphRange / plotWidth);
478 start = Math.Clamp(start, 0, values.Length - 1);
479 end = Math.Clamp(Math.Max(start + 1, end), 1, values.Length);
480
481 float value = 0;
482 for (int binIndex = start; binIndex < end; binIndex++)
483 if (values[binIndex] > value)
484 value = values[binIndex];
485
486 if (value <= 0)
487 continue;
488
489 double scaled = Math.Min(1.0, Math.Log(1.0 + value) / logMaximum);
490 double y = plotHeight - (scaled * plotHeight);
491 context.MoveTo(pixelX + 0.5, plotHeight);
492 context.LineTo(pixelX + 0.5, y);
493 }
494 context.Stroke();
495 }
496
497 private static float GetHistogramValue(Channel histogramChannel, int statisticsIndex, int bin)
498 {
499 if (histogramChannel?.stats == null || histogramChannel.stats.Length == 0)
500 return 0;
501
502 int safeStatisticsIndex = Math.Clamp(statisticsIndex, 0, histogramChannel.stats.Length - 1);
503 float[] values = histogramChannel.stats[safeStatisticsIndex]?.StackValues;
504 if (values == null || values.Length == 0)
505 return 0;
506
507 return values[Math.Clamp(bin, 0, values.Length - 1)];
508 }
509
510 private static bool HasUsableStatistics(BioImage image)
511 {
512 if (image?.Channels == null || image.Channels.Count == 0)
513 return false;
514
515 foreach (Channel imageChannel in image.Channels)
516 {
517 if (imageChannel?.stats == null || imageChannel.stats.Length == 0)
518 return false;
519
520 int requiredStats = image.Channels.Count == 1 && image.RGBChannelCount > 1
521 ? image.RGBChannelCount
522 : 1;
523 if (imageChannel.stats.Length < requiredStats)
524 return false;
525
526 for (int i = 0; i < requiredStats; i++)
527 {
528 if (imageChannel.stats[i]?.StackValues == null ||
529 imageChannel.stats[i].StackValues.Length == 0)
530 return false;
531 }
532 }
533
534 return true;
535 }
536
537 private float bin = 10;
538 public float Bin
539 {
540 get
541 {
542 return bin;
543 }
544 set
545 {
546 bin = value;
547 }
548 }
549 private int min = 0;
550 public float Min
551 {
552 get { return min; }
553 set
554 {
555 if(channel!=null)
556 channel.range[App.channelsTool.SelectedSample].Min = (int)value;
557 min = (int)value;
558 }
559 }
560 private int max = 0;
561 public float Max
562 {
563 get { return max; }
564 set
565 {
566 if (channel != null)
567 channel.range[App.channelsTool.SelectedSample].Max = (int)value;
568 max = (int)value;
569 }
570 }
571 private int graphMax = ushort.MaxValue;
572 public int GraphMax
573 {
574 get { return graphMax; }
575 set { graphMax = value; }
576 }
577 private int graphMin = 0;
578 public int GraphMin
579 {
580 get { return graphMin; }
581 set { graphMin = value; }
582 }
583 private bool stackHistogram = true;
584 public bool StackHistogram
585 {
586 get
587 {
588 return stackHistogram;
589 }
590 set
591 {
592 stackHistogram = value;
593 }
594 }
595 private int mouseX = 0;
596 private int mouseY = 0;
597 public int MouseX
598 {
599 get
600 {
601 return mouseX;
602 }
603 }
604 public int MouseY
605 {
606 get
607 {
608 return mouseX;
609 }
610 }
611 private float mouseValX = 0;
612 private float mouseValY = 0;
613 public float MouseValX
614 {
615 get
616 {
617 return mouseValX;
618 }
619 }
620 public float MouseValY
621 {
622 get
623 {
624 return mouseValY;
625 }
626 }
627
628 private bool axisNumbers = true;
629 public bool AxisNumbers
630 {
631 get { return axisNumbers;}
632 set { axisNumbers = value; }
633 }
634
635 private bool axisTicks = true;
636 public bool AxisTicks
637 {
638 get { return axisTicks; }
639 set { axisTicks = value; }
640 }
641
642 private float fx = 0;
643 private float fy = 0;
644 private AForge.Bitmap bm;
645
649 public void UpdateChannel(Channel c)
650 {
651 channel = c;
652 }
656 public void UpdateView()
657 {
658 view.QueueDraw();
659 }
660
666 Color SpectralColor(double l) // RGB <0,1> <- lambda l <400,700> [nm]
667 {
668 double t;
669 double r = 0;
670 double g = 0;
671 double b = 0;
672 if ((l >= 400.0) && (l < 410.0)) { t = (l - 400.0) / (410.0 - 400.0); r = +(0.33 * t) - (0.20 * t * t); }
673 else if ((l >= 410.0) && (l < 475.0)) { t = (l - 410.0) / (475.0 - 410.0); r = 0.14 - (0.13 * t * t); }
674 else if ((l >= 545.0) && (l < 595.0)) { t = (l - 545.0) / (595.0 - 545.0); r = +(1.98 * t) - (t * t); }
675 else if ((l >= 595.0) && (l < 650.0)) { t = (l - 595.0) / (650.0 - 595.0); r = 0.98 + (0.06 * t) - (0.40 * t * t); }
676 else if ((l >= 650.0) && (l < 700.0)) { t = (l - 650.0) / (700.0 - 650.0); r = 0.65 - (0.84 * t) + (0.20 * t * t); }
677 if ((l >= 415.0) && (l < 475.0)) { t = (l - 415.0) / (475.0 - 415.0); g = +(0.80 * t * t); }
678 else if ((l >= 475.0) && (l < 590.0)) { t = (l - 475.0) / (590.0 - 475.0); g = 0.8 + (0.76 * t) - (0.80 * t * t); }
679 else if ((l >= 585.0) && (l < 639.0)) { t = (l - 585.0) / (639.0 - 585.0); g = 0.84 - (0.84 * t); }
680 if ((l >= 400.0) && (l < 475.0)) { t = (l - 400.0) / (475.0 - 400.0); b = +(2.20 * t) - (1.50 * t * t); }
681 else if ((l >= 475.0) && (l < 560.0)) { t = (l - 475.0) / (560.0 - 475.0); b = 0.7 - (t) + (0.30 * t * t); }
682 r *= 255;
683 g *= 255;
684 b *= 255;
685 return Color.FromArgb(255, (int)r, (int)g, (int)b);
686 }
687 }
688}
void HistogramControl_ButtonPressEvent(object o, ButtonPressEventArgs args)
void Init()
This function initializes the histogram control.
void HistogramControl_MotionNotifyEvent(object o, MotionNotifyEventArgs args)
void UpdateChannel(Channel c)
Definition ML.cs:17