24 private Builder _builder;
26#pragma warning disable 649
28 private MenuItem setMin;
30 private MenuItem setMax;
32 private MenuItem setMinAll;
34 private MenuItem setMaxAll;
36 private DrawingArea view;
39#pragma warning restore 649
43 #region Constructors / Destructors
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);
49 protected HistogramControl(Builder builder, IntPtr handle, Channel channel) : base(handle)
52 this.channel = channel;
53 builder.Autoconnect(
this);
58 public Channel channel =
null;
59 private const double HistogramBarOpacity = 0.55;
60 private readonly Dictionary<Channel, Cairo.Color> _generatedChannelColors =
new();
64 if (
ImageView.SelectedImage.bitsPerPixel == 8)
71 graphMax = ushort.MaxValue;
74 view.Drawn += HistogramControl_Drawn;
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;
84 (EventMask.ButtonPressMask
85 | EventMask.ButtonReleaseMask
86 | EventMask.KeyPressMask
87 | EventMask.PointerMotionMask | EventMask.ScrollMask));
97 private void HistogramControl_DeleteEvent(
object o, DeleteEventArgs args)
108 private void HistogramControl_ScrollEvent(
object o, ScrollEventArgs args)
110 if(args.Event.Direction == Gdk.ScrollDirection.Up)
114 else if (args.Event.Direction == Gdk.ScrollDirection.Down)
126 private void SetMaxAll_ButtonPressEvent(
object o, ButtonPressEventArgs args)
128 foreach (Channel c
in ImageView.SelectedImage.Channels)
130 for (
int i = 0; i < c.range.Length; i++)
132 c.range[i].Max = (int)MouseValX;
136 App.viewer.UpdateImage();
144 private void SetMinAll_ButtonPressEvent(
object o, ButtonPressEventArgs args)
146 foreach (Channel c
in ImageView.SelectedImage.Channels)
148 for (
int i = 0; i < c.range.Length; i++)
150 c.range[i].Min = (int)MouseValX;
154 App.viewer.UpdateImage();
163 private void SetMax_ButtonPressEvent(
object o, ButtonPressEventArgs args)
165 App.channelsTool.SelectedChannel.range[App.channelsTool.SelectedSample].Max = (int)MouseValX;
167 App.viewer.UpdateImage();
176 private void SetMin_ButtonPressEvent(
object o, ButtonPressEventArgs args)
178 App.channelsTool.SelectedChannel.range[App.channelsTool.SelectedSample].Min = (int)MouseValX;
180 App.viewer.UpdateImage();
189 mouseX = (int)args.Event.X;
190 mouseY = (int)args.Event.Y;
192 if(args.Event.Button == 3)
203 mouseValX = (float)args.Event.X / fx;
204 mouseValY = (float)args.Event.Y / fy;
207 public Cairo.Color ToCairoColor(Color c)
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);
216 private void HistogramControl_Drawn(
object o, DrawnArgs args)
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);
225 if (!HasUsableStatistics(image))
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.");
236 graphMax = ushort.MaxValue;
237 g.SetSourceColor(ToCairoColor(Color.LightGray));
242 fx = ((float)drawingWidth) / ((
float)graphMax);
245 for (
int cc = 0; cc < image.Channels.Count; cc++)
247 for (
int i = 0; i < image.Channels[cc].stats.Length; i++)
249 Statistics channelStatistics = image.Channels[cc].stats[i];
250 if (channelStatistics ==
null)
252 float f = GetHistogramMaximum(channelStatistics.StackValues);
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.");
269 fy = ((float)drawingHeight) / maxmedian;
270 for (
int c = 0; c < image.Channels.Count; c++)
274 g.SetSourceColor(ToCairoColor(Color.Black));
276 g.LineTo(mouseX, drawingHeight);
280 Channel channel = image.Channels[c];
281 for (
int i = 0; i < channel.range.Length; i++)
284 int statisticsIndex = image.Channels.Count == 1 && image.RGBChannelCount > 1
285 ? Math.Clamp(i, 0, channel.stats.Length - 1)
287 stat = channel.stats[statisticsIndex];
289 Cairo.Color blackd =
new Cairo.Color(0, 0, 0, 0.35);
290 if (ImageView.SelectedImage.bitsPerPixel <= 8)
293 Cairo.Color barColor = GetChannelBarColor(image, channel, c, i);
295 int plotWidth = drawingWidth;
296 int plotHeight = drawingHeight;
297 int valueRange = Math.Max(1, gmax - gmin);
299 if (StackHistogram && c == image.Channels.Count - 1 && i == 0 &&
300 image.Statistics?.StackValues !=
null)
302 g.SetSourceColor(blackd);
303 DrawBinnedHistogram(g, image.Statistics.StackValues,
304 gmin, valueRange, plotWidth, plotHeight, maxmedian);
310 g.SetSourceColor(barColor);
311 DrawBinnedHistogram(g, stat.StackValues,
312 gmin, valueRange, plotWidth, plotHeight, maxmedian);
313 Cairo.Color markerColor = barColor;
315 g.SetSourceColor(markerColor);
316 g.MoveTo((fx * channel.range[i].Max), 0);
317 g.LineTo((fx * channel.range[i].Max), drawingHeight);
319 g.MoveTo((fx * channel.range[i].Min), 0);
320 g.LineTo((fx * channel.range[i].Min), drawingHeight);
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)
335 double x = (double)(value - graphMin) * drawingWidth / axisRange;
341 string label = value.ToString();
342 TextExtents extents = g.TextExtents(label);
343 g.MoveTo(x - (extents.Width / 2), tick + 11);
350 int hoverBin = graphMin + (int)((
long)Math.Max(0, mouseX) *
351 Math.Max(1, graphMax - graphMin) / drawingWidth);
352 if (image.RGBChannelCount == 3)
357 if (image.Channels.Count > 2)
360 red = GetHistogramValue(image.Channels[2], 0, hoverBin);
361 green = GetHistogramValue(image.Channels[1], 0, hoverBin);
362 blue = GetHistogramValue(image.Channels[0], 0, hoverBin);
368 red = GetHistogramValue(image.Channels[0], 2, hoverBin);
369 green = GetHistogramValue(image.Channels[0], 1, hoverBin);
370 blue = GetHistogramValue(image.Channels[0], 0, hoverBin);
372 st = $
"({hoverBin},R:{red},G:{green},B:{blue})";
376 st = $
"({hoverBin},{GetHistogramValue(image.Channels[0], 0, hoverBin)})";
378 g.MoveTo(mouseX, mouseY);
383 private static float GetHistogramMaximum(
float[] values)
385 if (values ==
null || values.Length == 0)
389 for (
int i = 0; i < values.Length; i++)
390 if (values[i] > maximum)
395 private Cairo.Color GetChannelBarColor(
397 Channel imageChannel,
402 if (image.Channels.Count == 1 && image.RGBChannelCount == 3)
404 return sampleIndex
switch
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)
412 if (imageChannel.Color.HasValue)
414 Color specified = imageChannel.Color.Value;
415 return new Cairo.Color(
419 HistogramBarOpacity);
422 if (_generatedChannelColors.TryGetValue(imageChannel, out Cairo.Color generated))
425 generated = CreateRandomVisibleColor();
426 _generatedChannelColors[imageChannel] = generated;
430 private static Cairo.Color CreateRandomVisibleColor()
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));
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)
452 private static int GetNiceTickStep(
int range,
int targetTickCount)
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));
461 private static void DrawBinnedHistogram(
462 Cairo.Context context,
468 float histogramMaximum)
470 if (values ==
null || values.Length == 0 || histogramMaximum <= 0)
473 double logMaximum = Math.Log(1.0 + histogramMaximum);
474 for (
int pixelX = 0; pixelX < plotWidth; pixelX++)
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);
482 for (
int binIndex = start; binIndex < end; binIndex++)
483 if (values[binIndex] > value)
484 value = values[binIndex];
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);
497 private static float GetHistogramValue(Channel histogramChannel,
int statisticsIndex,
int bin)
499 if (histogramChannel?.stats ==
null || histogramChannel.stats.Length == 0)
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)
507 return values[Math.Clamp(bin, 0, values.Length - 1)];
510 private static bool HasUsableStatistics(BioImage image)
512 if (image?.Channels ==
null || image.Channels.Count == 0)
515 foreach (Channel imageChannel
in image.Channels)
517 if (imageChannel?.stats ==
null || imageChannel.stats.Length == 0)
520 int requiredStats = image.Channels.Count == 1 && image.RGBChannelCount > 1
521 ? image.RGBChannelCount
523 if (imageChannel.stats.Length < requiredStats)
526 for (
int i = 0; i < requiredStats; i++)
528 if (imageChannel.stats[i]?.StackValues ==
null ||
529 imageChannel.stats[i].StackValues.Length == 0)
537 private float bin = 10;
556 channel.range[App.channelsTool.SelectedSample].Min = (int)value;
567 channel.range[App.channelsTool.SelectedSample].Max = (int)value;
571 private int graphMax = ushort.MaxValue;
574 get {
return graphMax; }
575 set { graphMax = value; }
577 private int graphMin = 0;
580 get {
return graphMin; }
581 set { graphMin = value; }
583 private bool stackHistogram =
true;
584 public bool StackHistogram
588 return stackHistogram;
592 stackHistogram = value;
595 private int mouseX = 0;
596 private int mouseY = 0;
611 private float mouseValX = 0;
612 private float mouseValY = 0;
613 public float MouseValX
620 public float MouseValY
628 private bool axisNumbers =
true;
629 public bool AxisNumbers
631 get {
return axisNumbers;}
632 set { axisNumbers = value; }
635 private bool axisTicks =
true;
636 public bool AxisTicks
638 get {
return axisTicks; }
639 set { axisTicks = value; }
642 private float fx = 0;
643 private float fy = 0;
644 private AForge.Bitmap bm;
666 Color SpectralColor(
double l)
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); }
685 return Color.FromArgb(255, (
int)r, (
int)g, (
int)b);