BioCore  5.0.0
A Windows .NET microscopy library and application for viewing, editing, and annotating images.
Loading...
Searching...
No Matches
ZarrChannelView.cs
Go to the documentation of this file.
1using BioLib;
2using AForge;
3using ZarrNET.Core.Helpers;
4using ZarrNET.Core.Nodes;
5
6namespace BioCore
7{
8 internal static class ZarrChannelView
9 {
10 private const int HistogramSampleSide = 1024;
11
12 internal static void InitializeChannels(BioImage image)
13 {
14 if (!image.IsZarrSource || image.levels == null || image.levels.Count == 0)
15 return;
16 var level = image.levels[0];
17 int axis = Array.FindIndex(level.EffectiveAxes, a => string.Equals(a.Name, "c", StringComparison.OrdinalIgnoreCase));
18 int count = axis >= 0 ? checked((int)level.Shape[axis]) : 1;
19 int bits = level.DataType.Equals("uint16", StringComparison.OrdinalIgnoreCase) ? 16 : 8;
20 while (image.Channels.Count < count)
21 image.Channels.Add(new Channel(image.Channels.Count, bits, 1));
22 }
23
24 internal static async Task<byte[]> ReadRgbTileAsync(BioImage image, int levelIndex, int x, int y,
25 int width, int height, AForge.ZCT coordinate, int[] channels)
26 {
27 var levels = image.Type == BioImage.ImageType.well && image.ZarrWellLevels.Count > 0
28 ? image.ZarrWellLevels[Math.Clamp(image.WellIndex, 0, image.ZarrWellLevels.Count - 1)]
29 : image.levels;
30 var level = levels[levelIndex];
31 int axisX = Array.FindIndex(level.EffectiveAxes, a => string.Equals(a.Name, "x", StringComparison.OrdinalIgnoreCase));
32 int axisY = Array.FindIndex(level.EffectiveAxes, a => string.Equals(a.Name, "y", StringComparison.OrdinalIgnoreCase));
33 if (axisX < 0 || axisY < 0)
34 throw new InvalidDataException("The Zarr image has no X or Y axis.");
35 int readWidth = Math.Min(width, checked((int)level.Shape[axisX]) - x);
36 int readHeight = Math.Min(height, checked((int)level.Shape[axisY]) - y);
37 var result = new byte[checked(width * height * 4)];
38 if (readWidth <= 0 || readHeight <= 0)
39 return result;
40 bool sixteenBit = level.DataType.Equals("uint16", StringComparison.OrdinalIgnoreCase);
41 if (!sixteenBit && !level.DataType.Equals("uint8", StringComparison.OrdinalIgnoreCase))
42 throw new NotSupportedException($"Zarr RGB display does not support {level.DataType}.");
43 for (int color = 0; color < 3; color++)
44 {
45 if (channels[color] < 0)
46 continue;
47 var plane = await level.ReadTileAsync(x, y, readWidth, readHeight,
48 z: coordinate.Z, c: channels[color], t: coordinate.T).ConfigureAwait(false);
49 int bytesPerSample = sixteenBit ? 2 : 1;
50 if (plane?.Data == null || plane.Data.Length < checked(readWidth * readHeight * bytesPerSample))
51 throw new InvalidDataException("The Zarr channel tile is incomplete.");
52 for (int row = 0; row < readHeight; row++)
53 for (int col = 0; col < readWidth; col++)
54 {
55 int source = (row * readWidth + col) * bytesPerSample;
56 result[(row * width + col) * 4 + color] = plane.Data[source + (sixteenBit ? 1 : 0)];
57 }
58 }
59 for (int row = 0; row < readHeight; row++)
60 for (int col = 0; col < readWidth; col++)
61 result[(row * width + col) * 4 + 3] = 255;
62 return result;
63 }
64
65 internal static async Task EnsureHistogramStatisticsAsync(BioImage image, ZCT coordinate)
66 {
67 if (!image.IsZarrSource || image.levels == null || image.levels.Count == 0)
68 {
69 await Task.Run(() => CalculateBufferedFrameStatistics(image, coordinate));
70 return;
71 }
72
73 var levels = image.Type == BioImage.ImageType.well && image.ZarrWellLevels.Count > 0
74 ? image.ZarrWellLevels[Math.Clamp(image.WellIndex, 0, image.ZarrWellLevels.Count - 1)]
75 : image.levels;
76 if (levels == null || levels.Count == 0)
77 return;
78
79 ResolutionLevelNode level = SelectHistogramLevel(levels);
80 int axisX = Array.FindIndex(level.EffectiveAxes,
81 a => string.Equals(a.Name, "x", StringComparison.OrdinalIgnoreCase));
82 int axisY = Array.FindIndex(level.EffectiveAxes,
83 a => string.Equals(a.Name, "y", StringComparison.OrdinalIgnoreCase));
84 int axisC = Array.FindIndex(level.EffectiveAxes,
85 a => string.Equals(a.Name, "c", StringComparison.OrdinalIgnoreCase));
86 if (axisX < 0 || axisY < 0)
87 return;
88
89 int levelWidth = checked((int)level.Shape[axisX]);
90 int levelHeight = checked((int)level.Shape[axisY]);
91 int width = Math.Min(levelWidth, HistogramSampleSide);
92 int height = Math.Min(levelHeight, HistogramSampleSide);
93 int x = Math.Max(0, (levelWidth - width) / 2);
94 int y = Math.Max(0, (levelHeight - height) / 2);
95 int sourceChannels = axisC >= 0 ? checked((int)level.Shape[axisC]) : 1;
96 bool sixteenBit = level.DataType.Equals("uint16", StringComparison.OrdinalIgnoreCase);
97 if (!sixteenBit && !level.DataType.Equals("uint8", StringComparison.OrdinalIgnoreCase))
98 return;
99
100 int bits = sixteenBit ? 16 : 8;
101 int stride = checked(width * (sixteenBit ? 2 : 1));
102 AForge.PixelFormat pixelFormat = sixteenBit
103 ? AForge.PixelFormat.Format16bppGrayScale
104 : AForge.PixelFormat.Format8bppIndexed;
105 var combined = new Statistics(bits);
106 var sourceStatistics = new Statistics[sourceChannels];
107 int z = ClampCoordinate(level, "z", coordinate.Z);
108 int t = ClampCoordinate(level, "t", coordinate.T);
109
110 for (int sourceChannel = 0; sourceChannel < sourceChannels; sourceChannel++)
111 {
112 var plane = await level.ReadTileAsync(x, y, width, height,
113 z: z, c: sourceChannel, t: t);
114 if (plane?.Data == null || plane.Data.Length < checked(stride * height))
115 continue;
116
117 Statistics[] planeStatistics = Statistics.FromBytes(plane.Data, width, height,
118 1, bits, stride, pixelFormat);
119 if (planeStatistics == null || planeStatistics.Length == 0)
120 continue;
121
122 var aggregate = new Statistics(bits);
123 aggregate.AddStatistics(planeStatistics[0]);
124 aggregate.MeanHistogram();
125 sourceStatistics[sourceChannel] = aggregate;
126 combined.AddStatistics(planeStatistics[0]);
127 }
128
129 if (sourceStatistics.Any(s => s == null))
130 return;
131
132 combined.MeanHistogram();
133 for (int i = 0; i < image.Channels.Count; i++)
134 {
135 int sampleCount = Math.Max(1, image.Channels[i].range?.Length ?? 0);
136 var statistics = new Statistics[sampleCount];
137 for (int sample = 0; sample < sampleCount; sample++)
138 {
139 int sourceChannel = sampleCount > 1 ? sample : i;
140 statistics[sample] = sourceStatistics[
141 Math.Clamp(sourceChannel, 0, sourceStatistics.Length - 1)];
142 }
143 image.Channels[i].stats = statistics;
144 for (int sample = 0; sample < Math.Min(statistics.Length, image.Channels[i].range.Length); sample++)
145 {
146 image.Channels[i].range[sample].Min = (int)statistics[sample].StackMin;
147 image.Channels[i].range[sample].Max = (int)statistics[sample].StackMax;
148 }
149 }
150 image.Statistics = combined;
151 }
152
153 private static void CalculateBufferedFrameStatistics(BioImage image, ZCT coordinate)
154 {
155 if (image.Buffers.Count == 0 || image.Channels.Count == 0)
156 return;
157
158 var combined = new Statistics(image.bitsPerPixel);
159 bool found = false;
160 for (int channelIndex = 0; channelIndex < image.Channels.Count; channelIndex++)
161 {
162 int frameIndex = image.GetFrameIndex(
163 Math.Clamp(coordinate.Z, 0, Math.Max(0, image.SizeZ - 1)),
164 Math.Clamp(channelIndex, 0, Math.Max(0, image.SizeC - 1)),
165 Math.Clamp(coordinate.T, 0, Math.Max(0, image.SizeT - 1)));
166 if (frameIndex < 0 || frameIndex >= image.Buffers.Count || image.Buffers[frameIndex] == null)
167 continue;
168
169 var buffer = image.Buffers[frameIndex];
170 buffer.Stats ??= Statistics.FromBytes(buffer);
171 if (buffer.Stats == null || buffer.Stats.Length == 0)
172 continue;
173
174 int sampleCount = Math.Max(1, image.Channels[channelIndex].range?.Length ?? 0);
175 var statistics = new Statistics[sampleCount];
176 for (int sample = 0; sample < sampleCount; sample++)
177 {
178 Statistics raw = buffer.Stats[Math.Clamp(sample, 0, buffer.Stats.Length - 1)];
179 var aggregate = new Statistics(image.bitsPerPixel);
180 aggregate.AddStatistics(raw);
181 aggregate.MeanHistogram();
182 statistics[sample] = aggregate;
183 combined.AddStatistics(raw);
184 image.Channels[channelIndex].range[sample].Min = (int)aggregate.StackMin;
185 image.Channels[channelIndex].range[sample].Max = (int)aggregate.StackMax;
186 found = true;
187 }
188 image.Channels[channelIndex].stats = statistics;
189 }
190 if (found)
191 {
192 combined.MeanHistogram();
193 image.Statistics = combined;
194 }
195 }
196
197 internal static bool HasHistogramStatistics(BioImage image)
198 {
199 return image?.Statistics?.StackValues != null &&
200 image.Channels.Count > 0 &&
201 image.Channels.All(channel => channel.range?.Length > 0 &&
202 channel.stats != null && channel.stats.Length >= channel.range.Length &&
203 channel.stats.All(stat => stat?.StackValues != null));
204 }
205
206 private static ResolutionLevelNode SelectHistogramLevel(IReadOnlyList<ResolutionLevelNode> levels)
207 {
208 for (int i = 0; i < levels.Count; i++)
209 {
210 var level = levels[i];
211 int axisX = Array.FindIndex(level.EffectiveAxes,
212 a => string.Equals(a.Name, "x", StringComparison.OrdinalIgnoreCase));
213 int axisY = Array.FindIndex(level.EffectiveAxes,
214 a => string.Equals(a.Name, "y", StringComparison.OrdinalIgnoreCase));
215 if (axisX >= 0 && axisY >= 0 &&
216 level.Shape[axisX] <= HistogramSampleSide &&
217 level.Shape[axisY] <= HistogramSampleSide)
218 return level;
219 }
220 return levels[^1];
221 }
222
223 private static int ClampCoordinate(ResolutionLevelNode level, string axisName, int value)
224 {
225 int axis = Array.FindIndex(level.EffectiveAxes,
226 a => string.Equals(a.Name, axisName, StringComparison.OrdinalIgnoreCase));
227 return axis < 0 ? 0 : Math.Clamp(value, 0, checked((int)level.Shape[axis]) - 1);
228 }
229 }
230}