35 public static class Zarr
37 private static readonly
object s_labelColorLock =
new();
38 private static readonly Random s_labelColorRandom =
new(unchecked((
int)(DateTime.UtcNow.Ticks ^ Environment.TickCount)));
39 private static readonly Dictionary<int, Color> s_generatedLabelColors =
new();
41 private sealed record ExportLevelInfo(
45 ResolutionLevelDescriptor Descriptor,
53 public double AverageScale => (ScaleX + ScaleY) / 2.0;
56 private sealed record AssociatedLevelInfo(
62 private static double NormalizePhysicalSize(
double value)
64 return value > 0 && !
double.IsNaN(value) && !
double.IsInfinity(value)
69 private static double GetRootPhysicalSizeX(BioImage image)
72 image.Resolutions.Count > 0 &&
73 image.Resolutions[0].PhysicalSizeX > 0)
74 return NormalizePhysicalSize(image.Resolutions[0].PhysicalSizeX);
76 return NormalizePhysicalSize(image?.PhysicalSizeX ?? 0);
79 private static double GetRootPhysicalSizeY(BioImage image)
82 image.Resolutions.Count > 0 &&
83 image.Resolutions[0].PhysicalSizeY > 0)
84 return NormalizePhysicalSize(image.Resolutions[0].PhysicalSizeY);
86 return NormalizePhysicalSize(image?.PhysicalSizeY ?? 0);
89 private static double GetRootPhysicalSizeZ(BioImage image)
92 image.Resolutions.Count > 0 &&
93 image.Resolutions[0].PhysicalSizeZ > 0)
94 return NormalizePhysicalSize(image.Resolutions[0].PhysicalSizeZ);
96 return NormalizePhysicalSize(image?.PhysicalSizeZ ?? 0);
108 public static void SaveZarr(BioImage b,
string outputDir)
116 if (b.SizeX <= 0 || b.SizeY <= 0 || b.SizeZ <= 0 || b.SizeC <= 0 || b.SizeT <= 0)
117 throw new InvalidOperationException(
118 $
"BioImage has invalid dimensions: X={b.SizeX} Y={b.SizeY} Z={b.SizeZ} C={b.SizeC} T={b.SizeT}. " +
119 "Ensure the image is fully loaded before saving as Zarr.");
121 int bitsPerSample = DetermineSourceBitsPerSample(b);
122 var sourcePixelFormat = DetermineSourcePixelFormat(b);
123 bool sourceIsInterleavedRgb = IsInterleavedRgbFormat(sourcePixelFormat);
124 int sourceChannelCount = sourceIsInterleavedRgb ? BioImage.GetBands(sourcePixelFormat) : 1;
125 bool sourceIsBGRA = IsBGRAFormat(sourcePixelFormat);
126 int exportChannelCount = sourceIsInterleavedRgb && b.SizeC == 1
127 ? Math.Min(3, sourceChannelCount)
130 var bytesPerSample = bitsPerSample / 8;
131 var zarrDataType = MapDataType(bitsPerSample);
132 var totalChannels = exportChannelCount;
144 var exportLevels = BuildExportLevels(b);
145 var levelDescriptors = exportLevels.Select(l => l.Descriptor).ToList();
146 if (levelDescriptors.Count == 0)
148 levelDescriptors.Add(
new ResolutionLevelDescriptor(b.SizeX, b.SizeY, 1.0));
149 exportLevels.Add(
new ExportLevelInfo(
157 GetRootPhysicalSizeX(b),
158 GetRootPhysicalSizeY(b),
159 GetRootPhysicalSizeZ(b)));
166 var coord =
new ZarrNET.Core.ZCT(b.SizeZ, totalChannels, b.SizeT);
168 var baseDescriptor =
new BioImageDescriptor(b.SizeX, b.SizeY, coord)
170 Name = Path.GetFileNameWithoutExtension(b.Filename),
171 DataType = zarrDataType,
172 PhysicalSizeX = exportLevels.Count > 0 ? exportLevels[0].PhysicalSizeX : GetRootPhysicalSizeX(b),
173 PhysicalSizeY = exportLevels.Count > 0 ? exportLevels[0].PhysicalSizeY : GetRootPhysicalSizeY(b),
174 PhysicalSizeZ = GetRootPhysicalSizeZ(b),
184 var writer = OmeZarrWriter.CreateAsync(outputDir, baseDescriptor, levelDescriptors).Result;
191 for (
int exportLevelIndex = 0; exportLevelIndex < exportLevels.Count; exportLevelIndex++)
193 var exportLevel = exportLevels[exportLevelIndex];
194 var lvlDesc = exportLevel.Descriptor;
195 for (
int t = 0; t < b.SizeT; t++)
197 for (
int z = 0; z < b.SizeZ; z++)
199 for (
int c = 0; c < exportChannelCount; c++)
201 if (sourceIsInterleavedRgb)
206 bytesPerSample, tileW, tileH,
207 sourceLevelIndex: exportLevel.SourceIndex,
208 scaleX: exportLevel.ScaleX,
209 scaleY: exportLevel.ScaleY,
210 levelIndex: exportLevelIndex,
212 srcChannelCount: sourceChannelCount,
213 isBGRA: sourceIsBGRA,
214 logicalC: 0).GetAwaiter().GetResult();
221 bytesPerSample, tileW, tileH,
222 sourceLevelIndex: exportLevel.SourceIndex,
223 scaleX: exportLevel.ScaleX,
224 scaleY: exportLevel.ScaleY,
225 levelIndex: exportLevelIndex).GetAwaiter().GetResult();
235 SaveAssociatedResolutionImages(
244 sourceIsInterleavedRgb,
249 WriteRootMetadata(outputDir, b, exportLevels);
253 Console.WriteLine(e.ToString());
256 File.WriteAllText(Path.Combine(outputDir,
"save-error.txt"), e.ToString());
263 Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, b);
275 private static async Task WritePlaneInTiles(
276 OmeZarrWriter writer,
278 ResolutionLevelDescriptor lvlDesc,
281 int tileW,
int tileH,
282 int sourceLevelIndex = 0,
286 int rgbChannelIndex = -1,
287 int srcChannelCount = 1,
291 bool needsDeinterleave = rgbChannelIndex >= 0;
292 int srcC = needsDeinterleave ? logicalC : c;
294 for (
int tileY = 0; tileY < lvlDesc.SizeY; tileY += tileH)
296 for (
int tileX = 0; tileX < lvlDesc.SizeX; tileX += tileW)
299 int actualW = Math.Min(tileW, lvlDesc.SizeX - tileX);
300 int actualH = Math.Min(tileH, lvlDesc.SizeY - tileY);
301 int pixelCount = actualW * actualH;
302 int interleavedBytes = pixelCount * srcChannelCount * bytesPerSample;
303 int singleChannelBytes = pixelCount * bytesPerSample;
305 int frameIndex = b.GetFrameIndex(z, srcC, t);
307 byte[]? pixelBytes =
null;
308 AForge.PixelFormat tilePixelFormat = DetermineSourcePixelFormat(b);
309 bool useDirectSourceRead = sourceLevelIndex <= 0 ||
310 (Math.Abs(scaleX - 1.0) < 0.0001 && Math.Abs(scaleY - 1.0) < 0.0001);
312 if (needsDeinterleave)
314 if (useDirectSourceRead)
316 using Bitmap tileBitmap = await b.GetTile(
317 frameIndex, Math.Max(0, sourceLevelIndex), tileX, tileY, actualW, actualH,
318 new AForge.ZCT(z, srcC, t),
true).ConfigureAwait(
false);
320 tilePixelFormat = tileBitmap.PixelFormat;
321 pixelBytes = tileBitmap.Bytes;
325 int srcX = Math.Max(0, (
int)Math.Round(tileX * scaleX));
326 int srcY = Math.Max(0, (
int)Math.Round(tileY * scaleY));
327 int srcW = Math.Max(1, (
int)Math.Round(actualW * scaleX));
328 int srcH = Math.Max(1, (
int)Math.Round(actualH * scaleY));
330 using Bitmap tileBitmap = await b.GetTile(
331 frameIndex, 0, srcX, srcY, srcW, srcH,
332 new AForge.ZCT(z, srcC, t),
true).ConfigureAwait(
false);
334 tilePixelFormat = tileBitmap.PixelFormat;
335 pixelBytes = DownsampleBgraNearest(tileBitmap.Bytes, srcW, srcH, actualW, actualH);
340 if (useDirectSourceRead)
342 pixelBytes = await b.GetTileBytesRaw(
343 frameIndex, Math.Max(0, sourceLevelIndex), tileX, tileY, actualW, actualH,
344 new AForge.ZCT(z, srcC, t)).ConfigureAwait(
false);
348 int srcX = Math.Max(0, (
int)Math.Round(tileX * scaleX));
349 int srcY = Math.Max(0, (
int)Math.Round(tileY * scaleY));
350 int srcW = Math.Max(1, (
int)Math.Round(actualW * scaleX));
351 int srcH = Math.Max(1, (
int)Math.Round(actualH * scaleY));
353 var srcBytes = await b.GetTileBytesRaw(
354 frameIndex, 0, srcX, srcY, srcW, srcH,
355 new AForge.ZCT(z, srcC, t)).ConfigureAwait(
false);
356 pixelBytes = DownsampleRawNearest(srcBytes, srcW, srcH, actualW, actualH, bytesPerSample);
360 if (pixelBytes ==
null || pixelBytes.Length == 0)
362 pixelBytes =
new byte[needsDeinterleave ? interleavedBytes : singleChannelBytes];
365 if (needsDeinterleave)
367 int inferredChannelCount = srcChannelCount;
368 if (pixelCount > 0 && pixelBytes.Length > 0)
370 int inferred = pixelBytes.Length / (pixelCount * bytesPerSample);
371 if (inferred >= 1 && inferred <= 4)
372 inferredChannelCount = inferred;
377 int expectedInterleavedBytes = pixelCount * inferredChannelCount * bytesPerSample;
378 if (pixelBytes.Length != expectedInterleavedBytes)
380 var trimmed =
new byte[expectedInterleavedBytes];
381 Buffer.BlockCopy(pixelBytes, 0, trimmed, 0,
382 Math.Min(pixelBytes.Length, expectedInterleavedBytes));
383 pixelBytes = trimmed;
389 bool actualIsBGRA = isBGRA || inferredChannelCount == 4 || IsBGRAFormat(tilePixelFormat);
390 int srcByteIndex = actualIsBGRA ? (2 - rgbChannelIndex) : rgbChannelIndex;
392 pixelBytes = DeinterleaveChannel(
393 pixelBytes, srcByteIndex, inferredChannelCount,
394 bytesPerSample, pixelCount);
399 if (pixelBytes.Length != singleChannelBytes)
401 var trimmed =
new byte[singleChannelBytes];
402 Buffer.BlockCopy(pixelBytes, 0, trimmed, 0,
403 Math.Min(pixelBytes.Length, singleChannelBytes));
404 pixelBytes = trimmed;
408 await writer.WriteRegionAsync(
413 levelIndex: levelIndex).ConfigureAwait(
false);
423 private static byte[] ExtractRawPixels(
424 Bitmap tileBitmap,
int width,
int height,
int bytesPerSample)
426 int rowBytes = width * bytesPerSample;
427 int totalBytes = rowBytes * height;
428 var sourceBytes = tileBitmap.Bytes;
430 if (sourceBytes.Length == totalBytes)
433 var output =
new byte[totalBytes];
434 int sourceStride = tileBitmap.Stride;
436 for (
int row = 0; row < height; row++)
438 Buffer.BlockCopy(sourceBytes, row * sourceStride, output, row * rowBytes, rowBytes);
451 private static string MapDataType(
int bitsPerSample)
453 return bitsPerSample
switch
462 private static int DetermineSourceBitsPerSample(BioImage b)
464 if (b.Resolutions.Count > 0)
466 return b.Resolutions[0].PixelFormat
switch
468 AForge.PixelFormat.Format8bppIndexed => 8,
469 AForge.PixelFormat.Format24bppRgb => 8,
470 AForge.PixelFormat.Format32bppArgb => 8,
471 AForge.PixelFormat.Format32bppRgb => 8,
472 AForge.PixelFormat.Format16bppGrayScale => 16,
473 AForge.PixelFormat.Format48bppRgb => 16,
474 AForge.PixelFormat.Format64bppArgb => 16,
475 AForge.PixelFormat.Format64bppPArgb => 16,
476 _ => Math.Max(8, b.bitsPerPixel)
480 return Math.Max(8, b.bitsPerPixel);
483 private static AForge.PixelFormat DetermineSourcePixelFormat(BioImage image)
485 if (image.Resolutions.Count > 0)
486 return image.Resolutions[0].PixelFormat;
488 if (image.Buffers.Count > 0)
489 return image.Buffers[0].PixelFormat;
491 return AForge.PixelFormat.Format16bppGrayScale;
494 private static bool IsInterleavedRgbFormat(AForge.PixelFormat format)
496 return format == AForge.PixelFormat.Format24bppRgb ||
497 format == AForge.PixelFormat.Format32bppArgb ||
498 format == AForge.PixelFormat.Format32bppPArgb ||
499 format == AForge.PixelFormat.Format32bppRgb ||
500 format == AForge.PixelFormat.Format48bppRgb ||
501 format == AForge.PixelFormat.Format64bppArgb ||
502 format == AForge.PixelFormat.Format64bppPArgb;
505 private static bool IsBGRAFormat(AForge.PixelFormat format)
507 return format == AForge.PixelFormat.Format24bppRgb ||
508 format == AForge.PixelFormat.Format32bppArgb ||
509 format == AForge.PixelFormat.Format32bppPArgb ||
510 format == AForge.PixelFormat.Format32bppRgb ||
511 format == AForge.PixelFormat.Format64bppArgb ||
512 format == AForge.PixelFormat.Format64bppPArgb;
523 public const string RoiFileName =
"rois.json";
530 private sealed class RoiDto
532 public string RoiId {
get;
set; } =
"";
533 public string RoiName {
get;
set; } =
"";
534 public string Type {
get;
set; } =
"";
535 public string Id {
get;
set; } =
"";
536 public int ShapeIndex {
get;
set; }
537 public string Text {
get;
set; } =
"";
538 public int Serie {
get;
set; }
539 public int Z {
get;
set; }
540 public int C {
get;
set; }
541 public int T {
get;
set; }
542 public double X {
get;
set; }
543 public double Y {
get;
set; }
544 public double W {
get;
set; }
545 public double H {
get;
set; }
547 public string Points {
get;
set; } =
"";
549 public string StrokeColor {
get;
set; } =
"";
550 public double StrokeWidth {
get;
set; }
552 public string FillColor {
get;
set; } =
"";
553 public float FontSize {
get;
set; }
563 public static void SaveROIs(BioImage b,
string outputDir)
565 var dtos =
new List<RoiDto>(b.Annotations.Count);
567 foreach (ROI an
in b.Annotations)
572 if (!
string.IsNullOrWhiteSpace(an.roiID) &&
573 an.roiID.StartsWith(
"zarr-label:", StringComparison.OrdinalIgnoreCase))
580 PointD[] pts = an.GetPoints();
581 var ptSb =
new System.Text.StringBuilder();
582 for (
int i = 0; i < pts.Length; i++)
584 if (i > 0) ptSb.Append(
' ');
585 ptSb.Append(pts[i].X.ToString(CultureInfo.InvariantCulture));
587 ptSb.Append(pts[i].Y.ToString(CultureInfo.InvariantCulture));
593 RoiName = an.roiName,
594 Type = an.type.ToString(),
596 ShapeIndex = an.shapeIndex,
606 Points = ptSb.ToString(),
607 StrokeColor = $
"{an.strokeColor.A},{an.strokeColor.R},{an.strokeColor.G},{an.strokeColor.B}",
608 StrokeWidth = an.strokeWidth,
609 FillColor = $
"{an.fillColor.A},{an.fillColor.R},{an.fillColor.G},{an.fillColor.B}",
610 FontSize = an.fontSize,
614 var opts =
new JsonSerializerOptions { WriteIndented =
true };
615 string json = JsonSerializer.Serialize(dtos, opts);
616 File.WriteAllText(Path.Combine(outputDir, RoiFileName), json);
618 Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, b, outputDir);
627 public static void SaveV2Compatibility(BioImage image,
string outputDir)
632 if (
string.IsNullOrWhiteSpace(outputDir))
635 if (!Directory.Exists(outputDir) && !outputDir.EndsWith(
".zarr", StringComparison.OrdinalIgnoreCase))
640 var sourcePixelFormat = DetermineSourcePixelFormat(image);
641 bool sourceIsInterleavedRgb = IsInterleavedRgbFormat(sourcePixelFormat);
642 int sourceChannelCount = sourceIsInterleavedRgb ? BioImage.GetBands(sourcePixelFormat) : 1;
643 int exportChannelCount = sourceIsInterleavedRgb && image.SizeC == 1
644 ? Math.Min(3, sourceChannelCount)
647 WriteText(Path.Combine(outputDir,
".zgroup"), JsonSerializer.Serialize(
new { zarr_format = 2 },
new JsonSerializerOptions { WriteIndented = true }));
649 var exportLevels = BuildExportLevels(image);
650 var multiscales =
new[]
655 name = Path.GetFileNameWithoutExtension(image.Filename),
658 new { name =
"t", type =
"time" },
659 new { name =
"c", type =
"channel" },
660 new { name =
"z", type =
"space", unit =
"micrometer" },
661 new { name =
"y", type =
"space", unit =
"micrometer" },
662 new { name =
"x", type =
"space", unit =
"micrometer" }
664 datasets = BuildV2Datasets(image, exportLevels)
668 var omeroNode = ZarrMetadataFixup.CreateRgbDisplayMetadata(image);
669 WriteText(Path.Combine(outputDir,
".zattrs"), JsonSerializer.Serialize(
new { multiscales, omero = omeroNode },
new JsonSerializerOptions { WriteIndented = true }));
671 for (
int i = 0; i < exportLevels.Count; i++)
673 var exportLevel = exportLevels[i];
674 var dtype = MapV2Dtype(sourcePixelFormat);
679 Math.Max(1, image.SizeZ),
680 Math.Min(512, exportLevel.SizeY),
681 Math.Min(512, exportLevel.SizeX)
687 shape =
new long[] { image.SizeT, exportChannelCount, image.SizeZ, exportLevel.SizeY, exportLevel.SizeX },
690 compressor = (
object?)
null,
693 filters = (
object?)
null,
694 dimension_separator =
"/"
697 var levelDir = Path.Combine(outputDir, i.ToString());
698 WriteText(Path.Combine(levelDir,
".zarray"), JsonSerializer.Serialize(arrayDoc,
new JsonSerializerOptions { WriteIndented = true }));
699 WriteText(Path.Combine(levelDir,
".zattrs"), JsonSerializer.Serialize(
new { omero = omeroNode },
new JsonSerializerOptions { WriteIndented = true }));
702 WriteV2Chunks(outputDir, image, exportLevels, sourcePixelFormat, sourceIsInterleavedRgb, sourceChannelCount, exportChannelCount);
703 ZarrMetadataFixup.AddRgbDisplayMetadata(outputDir, image);
707 Console.WriteLine($
"Save Zarr v2 compatibility failed: {ex.Message}");
710 File.WriteAllText(Path.Combine(outputDir,
"save-v2-error.txt"), ex.ToString());
718 private static void SaveAssociatedResolutionImages(
722 BioImageDescriptor baseDescriptor,
726 AForge.PixelFormat sourcePixelFormat,
727 bool sourceIsInterleavedRgb,
728 int sourceChannelCount,
729 int exportChannelCount,
732 var associatedLevels = BuildAssociatedLevelInfos(image);
733 if (associatedLevels.Count == 0)
736 var manifest =
new List<object>();
737 foreach (var associated
in associatedLevels)
741 string associatedDir = Path.Combine(outputDir, associated.Name);
742 var associatedDescriptor =
new BioImageDescriptor(associated.SizeX, associated.SizeY,
new ZarrNET.Core.ZCT(image.SizeZ, exportChannelCount, image.SizeT))
744 Name = $
"{Path.GetFileNameWithoutExtension(image.Filename)}_{associated.Name}",
746 PhysicalSizeX = baseDescriptor.PhysicalSizeX,
747 PhysicalSizeY = baseDescriptor.PhysicalSizeY,
748 PhysicalSizeZ = baseDescriptor.PhysicalSizeZ,
753 var associatedWriter = OmeZarrWriter.CreateAsync(
755 associatedDescriptor,
758 new ResolutionLevelDescriptor(associated.SizeX, associated.SizeY, 1.0)
763 for (
int t = 0; t < image.SizeT; t++)
765 for (
int z = 0; z < image.SizeZ; z++)
767 for (
int c = 0; c < exportChannelCount; c++)
769 if (sourceIsInterleavedRgb)
772 associatedWriter, image,
new ResolutionLevelDescriptor(associated.SizeX, associated.SizeY, 1.0),
774 bytesPerSample, tileW, tileH,
775 sourceLevelIndex: associated.SourceIndex,
780 srcChannelCount: sourceChannelCount,
781 isBGRA: sourceIsBGRA,
782 logicalC: 0).GetAwaiter().GetResult();
787 associatedWriter, image,
new ResolutionLevelDescriptor(associated.SizeX, associated.SizeY, 1.0),
789 bytesPerSample, tileW, tileH,
790 sourceLevelIndex: associated.SourceIndex,
793 levelIndex: 0).GetAwaiter().GetResult();
801 associatedWriter.DisposeAsync().GetAwaiter().GetResult();
804 ZarrMetadataFixup.AddRgbDisplayMetadata(associatedDir, image);
807 name = associated.Name,
808 path = associated.Name,
809 sourceIndex = associated.SourceIndex,
810 sizeX = associated.SizeX,
811 sizeY = associated.SizeY
816 Console.WriteLine($
"Save associated Zarr '{associated.Name}' failed: {ex.Message}");
819 File.WriteAllText(Path.Combine(outputDir, $
"save-associated-{associated.Name}-error.txt"), ex.ToString());
827 if (manifest.Count > 0)
830 Path.Combine(outputDir,
"associated-images.json"),
831 JsonSerializer.Serialize(manifest,
new JsonSerializerOptions { WriteIndented = true }));
835 private static List<AssociatedLevelInfo> BuildAssociatedLevelInfos(BioImage image)
837 var levels =
new List<AssociatedLevelInfo>();
838 if (image ==
null || image.Resolutions ==
null || image.Resolutions.Count == 0)
841 void AddLevel(
string name,
int? index)
846 int sourceIndex = index.Value;
847 if (sourceIndex < 0 || sourceIndex >= image.Resolutions.Count)
850 if (levels.Any(l => l.SourceIndex == sourceIndex))
853 var res = image.Resolutions[sourceIndex];
854 if (res.SizeX <= 0 || res.SizeY <= 0)
857 levels.Add(
new AssociatedLevelInfo(name, sourceIndex, res.SizeX, res.SizeY));
860 AddLevel(
"macro", image.MacroResolution);
861 AddLevel(
"label", image.LabelResolution);
865 private static void WriteV2Chunks(
868 List<ExportLevelInfo> exportLevels,
869 AForge.PixelFormat sourcePixelFormat,
870 bool sourceIsInterleavedRgb,
871 int sourceChannelCount,
872 int exportChannelCount)
874 for (
int exportLevelIndex = 0; exportLevelIndex < exportLevels.Count; exportLevelIndex++)
876 var exportLevel = exportLevels[exportLevelIndex];
877 int bytesPerSample = Math.Max(1, DetermineSourceBitsPerSample(image) / 8);
879 for (
int t = 0; t < image.SizeT; t++)
881 for (
int z = 0; z < image.SizeZ; z++)
883 for (
int tileY = 0; tileY < exportLevel.SizeY; tileY += 512)
885 for (
int tileX = 0; tileX < exportLevel.SizeX; tileX += 512)
887 int actualW = Math.Min(512, exportLevel.SizeX - tileX);
888 int actualH = Math.Min(512, exportLevel.SizeY - tileY);
889 if (actualW <= 0 || actualH <= 0)
892 for (
int c = 0; c < exportChannelCount; c++)
894 byte[] planeBytes = GetV2PlaneBytes(
895 image, exportLevel.SourceIndex, t, z, c,
896 tileX, tileY, actualW, actualH,
897 exportLevel.ScaleX, exportLevel.ScaleY,
898 sourceIsInterleavedRgb, sourceChannelCount, bytesPerSample, sourcePixelFormat).GetAwaiter().GetResult();
900 byte[] chunkBytes = BuildV2ChunkBytes(
908 string chunkPath = Path.Combine(
910 exportLevelIndex.ToString(),
914 (tileY / 512).ToString(),
915 (tileX / 512).ToString());
916 string? chunkDir = Path.GetDirectoryName(chunkPath);
917 if (!
string.IsNullOrWhiteSpace(chunkDir))
918 Directory.CreateDirectory(chunkDir);
920 File.WriteAllBytes(chunkPath, chunkBytes);
929 private static async Task<byte[]> GetV2PlaneBytes(
941 bool sourceIsInterleavedRgb,
942 int sourceChannelCount,
944 AForge.PixelFormat sourcePixelFormat)
946 if (!sourceIsInterleavedRgb)
948 bool useDirectLevel0RawRead = levelIndex <= 0 ||
949 (Math.Abs(scaleX - 1.0) < 0.0001 && Math.Abs(scaleY - 1.0) < 0.0001);
950 if (useDirectLevel0RawRead)
952 return await image.GetTileBytesRaw(
953 image.GetFrameIndex(z, c, t),
959 new AForge.ZCT(z, c, t)).ConfigureAwait(
false);
962 int srcX = Math.Max(0, (
int)Math.Round(tileX * scaleX));
963 int srcY = Math.Max(0, (
int)Math.Round(tileY * scaleY));
964 int srcW = Math.Max(1, (
int)Math.Round(actualW * scaleX));
965 int srcH = Math.Max(1, (
int)Math.Round(actualH * scaleY));
966 return await image.GetTileBytesRaw(
967 image.GetFrameIndex(z, c, t),
973 new AForge.ZCT(z, c, t)).ConfigureAwait(
false);
976 int frameIndex = image.GetFrameIndex(z, 0, t);
977 byte[]? pixelBytes =
null;
978 AForge.PixelFormat tilePixelFormat = sourcePixelFormat;
979 bool useDirectLevel0RgbRead = levelIndex <= 0 ||
980 (Math.Abs(scaleX - 1.0) < 0.0001 && Math.Abs(scaleY - 1.0) < 0.0001);
981 if (useDirectLevel0RgbRead)
983 using Bitmap tileBitmap = await image.GetTile(
990 new AForge.ZCT(z, 0, t),
991 true).ConfigureAwait(
false);
992 tilePixelFormat = tileBitmap.PixelFormat;
993 pixelBytes = tileBitmap.Bytes;
997 int srcX = Math.Max(0, (
int)Math.Round(tileX * scaleX));
998 int srcY = Math.Max(0, (
int)Math.Round(tileY * scaleY));
999 int srcW = Math.Max(1, (
int)Math.Round(actualW * scaleX));
1000 int srcH = Math.Max(1, (
int)Math.Round(actualH * scaleY));
1001 using Bitmap tileBitmap = await image.GetTile(
1008 new AForge.ZCT(z, 0, t),
1009 true).ConfigureAwait(
false);
1010 tilePixelFormat = tileBitmap.PixelFormat;
1011 pixelBytes = DownsampleBgraNearest(tileBitmap.Bytes, srcW, srcH, actualW, actualH);
1014 pixelBytes ??= Array.Empty<
byte>();
1016 int pixelCount = actualW * actualH;
1017 if (pixelBytes ==
null || pixelBytes.Length == 0)
1018 return new byte[pixelCount * bytesPerSample];
1020 int singleChannelBytes = pixelCount * bytesPerSample;
1021 if (pixelBytes.Length == singleChannelBytes)
1024 int inferredChannelCount = sourceChannelCount;
1025 if (pixelCount > 0 && pixelBytes.Length > 0)
1027 int inferred = pixelBytes.Length / (pixelCount * bytesPerSample);
1028 if (inferred >= 1 && inferred <= 4)
1029 inferredChannelCount = inferred;
1032 int expectedInterleavedBytes = pixelCount * inferredChannelCount * bytesPerSample;
1033 if (pixelBytes.Length != expectedInterleavedBytes)
1035 var trimmed =
new byte[expectedInterleavedBytes];
1036 Buffer.BlockCopy(pixelBytes, 0, trimmed, 0, Math.Min(pixelBytes.Length, expectedInterleavedBytes));
1037 pixelBytes = trimmed;
1040 bool actualIsBGRA = IsBGRAFormat(sourcePixelFormat) || inferredChannelCount == 4 || IsBGRAFormat(tilePixelFormat);
1041 int srcByteIndex = actualIsBGRA ? (2 - c) : c;
1042 return DeinterleaveChannel(pixelBytes, srcByteIndex, inferredChannelCount, bytesPerSample, pixelCount);
1045 private static byte[] DownsampleBgraNearest(
byte[] source,
int sourceWidth,
int sourceHeight,
int targetWidth,
int targetHeight)
1047 if (targetWidth <= 0 || targetHeight <= 0)
1048 return Array.Empty<
byte>();
1050 int targetBytes = targetWidth * targetHeight * 4;
1051 if (source ==
null || source.Length == 0 || sourceWidth <= 0 || sourceHeight <= 0)
1052 return new byte[targetBytes];
1054 if (sourceWidth == targetWidth && sourceHeight == targetHeight)
1056 if (source.Length == targetBytes)
1059 var exact =
new byte[targetBytes];
1060 Buffer.BlockCopy(source, 0, exact, 0, Math.Min(source.Length, targetBytes));
1064 var dst =
new byte[targetBytes];
1065 double scaleX = (double)sourceWidth / targetWidth;
1066 double scaleY = (double)sourceHeight / targetHeight;
1068 for (
int y = 0; y < targetHeight; y++)
1070 int srcY = Math.Min(sourceHeight - 1, (
int)Math.Floor(y * scaleY));
1071 int srcRow = srcY * sourceWidth * 4;
1072 int dstRow = y * targetWidth * 4;
1073 for (
int x = 0; x < targetWidth; x++)
1075 int srcX = Math.Min(sourceWidth - 1, (
int)Math.Floor(x * scaleX));
1076 int srcOff = srcRow + srcX * 4;
1077 int dstOff = dstRow + x * 4;
1078 if (srcOff + 3 < source.Length)
1080 dst[dstOff + 0] = source[srcOff + 0];
1081 dst[dstOff + 1] = source[srcOff + 1];
1082 dst[dstOff + 2] = source[srcOff + 2];
1083 dst[dstOff + 3] = source[srcOff + 3];
1091 private static byte[] DownsampleRawNearest(
byte[] source,
int sourceWidth,
int sourceHeight,
int targetWidth,
int targetHeight,
int bytesPerSample)
1093 if (targetWidth <= 0 || targetHeight <= 0 || bytesPerSample <= 0)
1094 return Array.Empty<
byte>();
1096 int targetBytes = targetWidth * targetHeight * bytesPerSample;
1097 if (source ==
null || source.Length == 0 || sourceWidth <= 0 || sourceHeight <= 0)
1098 return new byte[targetBytes];
1100 if (sourceWidth == targetWidth && sourceHeight == targetHeight)
1102 if (source.Length == targetBytes)
1105 var exact =
new byte[targetBytes];
1106 Buffer.BlockCopy(source, 0, exact, 0, Math.Min(source.Length, targetBytes));
1110 var dst =
new byte[targetBytes];
1111 double scaleX = (double)sourceWidth / targetWidth;
1112 double scaleY = (double)sourceHeight / targetHeight;
1114 for (
int y = 0; y < targetHeight; y++)
1116 int srcY = Math.Min(sourceHeight - 1, (
int)Math.Floor(y * scaleY));
1117 int srcRow = srcY * sourceWidth * bytesPerSample;
1118 int dstRow = y * targetWidth * bytesPerSample;
1119 for (
int x = 0; x < targetWidth; x++)
1121 int srcX = Math.Min(sourceWidth - 1, (
int)Math.Floor(x * scaleX));
1122 int srcOff = srcRow + srcX * bytesPerSample;
1123 int dstOff = dstRow + x * bytesPerSample;
1124 if (srcOff + bytesPerSample <= source.Length)
1125 Buffer.BlockCopy(source, srcOff, dst, dstOff, bytesPerSample);
1132 private static Task<byte[]?> TryGetRawTileFromBufferAsync(
1142 var method = typeof(BioImage).GetMethod(
1143 "TryGetRawTileFromBuffer",
1144 System.Reflection.BindingFlags.Instance |
1145 System.Reflection.BindingFlags.NonPublic);
1148 return Task.FromResult<
byte[]?>(
null);
1150 var result = method.Invoke(image,
new object[] { index, tileX, tileY, actualW, actualH });
1151 return Task.FromResult(result as
byte[]);
1155 return Task.FromResult<
byte[]?>(
null);
1159 private static byte[] BuildV2ChunkBytes(
1167 if (planeBytes ==
null)
1168 return Array.Empty<
byte>();
1170 int chunkPixels = Math.Max(0, chunkW) * Math.Max(0, chunkH);
1171 var output =
new byte[chunkPixels * bytesPerSample];
1173 int actualRowBytes = Math.Max(0, actualW) * bytesPerSample;
1174 int paddedRowBytes = Math.Max(0, chunkW) * bytesPerSample;
1175 int copyHeight = Math.Min(actualH, chunkH);
1176 int copyWidth = Math.Min(actualW, chunkW);
1178 for (
int row = 0; row < copyHeight; row++)
1180 int srcOffset = row * actualRowBytes;
1181 int dstOffset = row * paddedRowBytes;
1182 int copyBytes = Math.Min(copyWidth * bytesPerSample, Math.Max(0, planeBytes.Length - srcOffset));
1184 Buffer.BlockCopy(planeBytes, srcOffset, output, dstOffset, copyBytes);
1195 public static List<ROI> LoadROIs(
string zarrDir)
1197 var result =
new List<ROI>();
1198 string path = Path.Combine(zarrDir, RoiFileName);
1200 if (!File.Exists(path))
1203 string json = File.ReadAllText(path);
1204 var dtos = JsonSerializer.Deserialize<List<RoiDto>>(json);
1208 foreach (var dto
in dtos)
1210 if (!
string.IsNullOrWhiteSpace(dto.RoiId) &&
1211 dto.RoiId.StartsWith(
"zarr-label:", StringComparison.OrdinalIgnoreCase))
1219 roiName = dto.RoiName,
1220 type = (ROI.Type)Enum.Parse(typeof(ROI.Type), dto.Type),
1222 shapeIndex = dto.ShapeIndex,
1225 coord =
new AForge.ZCT(dto.Z, dto.C, dto.T),
1226 strokeWidth= dto.StrokeWidth,
1227 fontSize = dto.FontSize,
1231 if (an.type == ROI.Type.Freeform || an.type == ROI.Type.Polygon)
1235 if (!
string.IsNullOrEmpty(dto.StrokeColor))
1237 var sc = dto.StrokeColor.Split(
',');
1238 an.strokeColor = AForge.Color.FromArgb(
1239 int.Parse(sc[0]),
int.Parse(sc[1]),
1240 int.Parse(sc[2]),
int.Parse(sc[3]));
1242 if (!
string.IsNullOrEmpty(dto.FillColor))
1244 var fc = dto.FillColor.Split(
',');
1245 an.fillColor = AForge.Color.FromArgb(
1246 int.Parse(fc[0]),
int.Parse(fc[1]),
1247 int.Parse(fc[2]),
int.Parse(fc[3]));
1251 if (!
string.IsNullOrEmpty(dto.Points))
1252 an.AddPoints(an.stringToPoints(dto.Points));
1254 an.BoundingBox =
new AForge.RectangleD(dto.X, dto.Y, dto.W, dto.H);
1259 Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, zarrDir);
1266 public static void SaveLabelOverlays(BioImage image, IReadOnlyList<ROI> overlays,
string outputDir)
1268 SaveLabelOverlaysAsync(image, overlays, outputDir).GetAwaiter().GetResult();
1274 public static async System.Threading.Tasks.Task SaveLabelOverlaysAsync(
1276 IReadOnlyList<ROI> overlays,
1278 CancellationToken ct =
default)
1280 if (image ==
null || overlays ==
null || overlays.Count == 0)
1283 if (
string.IsNullOrWhiteSpace(outputDir))
1286 if (!Directory.Exists(outputDir) && !outputDir.EndsWith(
".zarr", StringComparison.OrdinalIgnoreCase))
1289 var labelRois = overlays
1290 .Where(IsSupportedLabelRoi)
1293 if (labelRois.Count == 0)
1296 int baseWidth = image.Resolutions.Count > 0 ? image.Resolutions[0].SizeX : image.SizeX;
1297 int baseHeight = image.Resolutions.Count > 0 ? image.Resolutions[0].SizeY : image.SizeY;
1298 if (baseWidth <= 0 || baseHeight <= 0)
1301 int sizeT = Math.Max(1, image.SizeT);
1302 int sizeC = Math.Max(1, image.SizeC);
1303 int sizeZ = Math.Max(1, image.SizeZ);
1304 var labelColors =
new Dictionary<int, AForge.Color>();
1305 var roiEntries =
new List<(ROI Roi, int LabelId)>();
1307 foreach (var roi
in labelRois)
1309 int labelId = ParseLabelId(roi.Text, roiEntries.Count + 1);
1313 roiEntries.Add((roi, labelId));
1315 if (!labelColors.ContainsKey(labelId))
1317 var col = roi.fillColor;
1319 col = roi.strokeColor;
1321 col = GetFallbackLabelColor(labelId);
1322 labelColors[labelId] = col;
1326 if (labelColors.Count == 0)
1329 var exportLevels = BuildExportLevels(image);
1330 if (baseWidth <= 0 || baseHeight <= 0)
1333 using var store =
new ZarrNET.Core.Zarr.Store.LocalFileSystemStore(outputDir);
1334 var rootGroup = await ZarrNET.Core.Zarr.ZarrGroup.OpenRootAsync(store, ct).ConfigureAwait(
false);
1336 await WriteJsonAsync(
1342 node_type =
"group",
1348 labels =
new[] {
"0" }
1352 ct).ConfigureAwait(
false);
1354 var colors = labelColors
1355 .OrderBy(kv => kv.Key)
1356 .Select(kv =>
new Dictionary<string, object?>
1358 [
"label-value"] = kv.Key,
1359 [
"rgba"] =
new[] { kv.Value.R, kv.Value.G, kv.Value.B, kv.Value.A }
1363 var labelAxes =
new object[]
1365 new { name =
"t", type =
"time" },
1366 new { name =
"c", type =
"channel" },
1367 new { name =
"z", type =
"space", unit =
"micrometer" },
1368 new { name =
"y", type =
"space", unit =
"micrometer" },
1369 new { name =
"x", type =
"space", unit =
"micrometer" }
1372 var labelImageGroupDoc =
new Dictionary<string, object?>
1374 [
"version"] =
"0.5",
1375 [
"multiscales"] =
new[]
1382 datasets = BuildV3Datasets(exportLevels)
1385 [
"image-label"] =
new Dictionary<string, object?>
1387 [
"version"] =
"0.5",
1388 [
"source"] =
new Dictionary<string, object?>
1390 [
"image"] =
"../../"
1392 [
"colors"] = colors,
1393 [
"properties"] = Array.Empty<
object>()
1397 await WriteJsonAsync(store,
"labels/0/zarr.json",
new
1400 node_type =
"group",
1403 ome = labelImageGroupDoc
1405 }, ct).ConfigureAwait(
false);
1407 var labelGroup = await rootGroup.OpenGroupAsync(
"labels", ct).ConfigureAwait(
false);
1408 var labelImageGroup = await labelGroup.OpenGroupAsync(
"0", ct).ConfigureAwait(
false);
1410 for (
int levelIndex = 0; levelIndex < exportLevels.Count; levelIndex++)
1412 var exportLevel = exportLevels[levelIndex];
1413 int levelWidth = exportLevel.SizeX;
1414 int levelHeight = exportLevel.SizeY;
1415 double scaleX = baseWidth > 0 ? levelWidth / (double)baseWidth : 1.0;
1416 double scaleY = baseHeight > 0 ? levelHeight / (double)baseHeight : 1.0;
1418 int planePixels = levelWidth * levelHeight;
1419 int planeStride = planePixels;
1420 int zStride = planeStride;
1421 int cStride = zStride * sizeZ;
1422 int tStride = cStride * sizeC;
1424 var labelVolume =
new short[planePixels * sizeZ * sizeC * sizeT];
1425 foreach (var entry
in roiEntries)
1427 var roi = entry.Roi;
1428 int labelId = entry.LabelId;
1430 int z = Math.Clamp(roi.coord.Z, 0, sizeZ - 1);
1431 int c = Math.Clamp(roi.coord.C, 0, sizeC - 1);
1432 int t = Math.Clamp(roi.coord.T, 0, sizeT - 1);
1433 int offset = (t * tStride) + (c * cStride) + (z * zStride);
1444 image.Volume.Location.X,
1445 image.Volume.Location.Y,
1446 exportLevel.PhysicalSizeX,
1447 exportLevel.PhysicalSizeY);
1450 var levelArrayDoc =
new
1453 node_type =
"array",
1454 shape =
new long[] { sizeT, sizeC, sizeZ, levelHeight, levelWidth },
1455 data_type =
"int16",
1466 Math.Min(512, levelHeight),
1467 Math.Min(512, levelWidth)
1471 chunk_key_encoding =
new
1474 configuration =
new { separator =
"/" }
1477 dimension_names =
new[] {
"t",
"c",
"z",
"y",
"x" },
1478 codecs =
new object[]
1487 shuffle =
"byteshuffle",
1495 await WriteJsonAsync(store, $
"labels/0/{levelIndex}/zarr.json", levelArrayDoc, ct).ConfigureAwait(
false);
1497 var labelArray = await labelImageGroup.OpenArrayAsync(levelIndex.ToString(), ct).ConfigureAwait(
false);
1498 var labelBytes =
new byte[labelVolume.Length *
sizeof(short)];
1499 Buffer.BlockCopy(labelVolume, 0, labelBytes, 0, labelBytes.Length);
1501 await labelArray.WriteRegionAsync(
1502 new long[] { 0, 0, 0, 0, 0 },
1503 new long[] { sizeT, sizeC, sizeZ, levelHeight, levelWidth },
1505 ct).ConfigureAwait(
false);
1508 WriteText(Path.Combine(outputDir,
"labels",
".zgroup"), JsonSerializer.Serialize(
new { zarr_format = 2 },
new JsonSerializerOptions { WriteIndented = true }));
1509 WriteText(Path.Combine(outputDir,
"labels",
".zattrs"), JsonSerializer.Serialize(
new { labels = new[] {
"0" } },
new JsonSerializerOptions { WriteIndented =
true }));
1511 WriteText(Path.Combine(outputDir,
"labels",
"0",
".zgroup"), JsonSerializer.Serialize(
new { zarr_format = 2 },
new JsonSerializerOptions { WriteIndented = true }));
1513 var labelV2Array =
new
1516 shape =
new long[] { sizeT, sizeC, sizeZ, baseHeight, baseWidth },
1517 chunks =
new[] { 1, 1, 1, Math.Min(512, baseHeight), Math.Min(512, baseWidth) },
1529 filters = (
object?)
null,
1530 dimension_separator =
"/"
1533 WriteText(Path.Combine(outputDir,
"labels",
"0",
"0",
".zarray"), JsonSerializer.Serialize(labelV2Array,
new JsonSerializerOptions { WriteIndented = true }));
1534 var labelAttrs =
new Dictionary<string, object?>
1536 [
"multiscales"] =
new[]
1544 new { name =
"t", type =
"time" },
1545 new { name =
"c", type =
"channel" },
1546 new { name =
"z", type =
"space", unit =
"micrometer" },
1547 new { name =
"y", type =
"space", unit =
"micrometer" },
1548 new { name =
"x", type =
"space", unit =
"micrometer" }
1555 coordinateTransformations =
new object[]
1564 GetRootPhysicalSizeZ(image),
1565 GetRootPhysicalSizeY(image),
1566 GetRootPhysicalSizeX(image)
1574 [
"image-label"] =
new Dictionary<string, object?>
1576 [
"version"] =
"0.4",
1577 [
"source"] =
new Dictionary<string, object?>
1581 [
"colors"] = colors,
1582 [
"properties"] = Array.Empty<
object>()
1587 Path.Combine(outputDir,
"labels",
"0",
".zattrs"),
1588 JsonSerializer.Serialize(labelAttrs,
new JsonSerializerOptions { WriteIndented = true }));
1590 Recorder.Record(
BioLib.Recorder.GetCurrentMethodInfo(),
false, image, outputDir, labelRois.Count);
1597 public static async System.Threading.Tasks.Task<List<ROI>> LoadLabelOverlaysAsync(BioImage image,
string? sourceOverride =
null)
1599 var overlays =
new List<ROI>();
1604 string source = sourceOverride ?? image.file;
1605 if (
string.IsNullOrWhiteSpace(source))
1606 source = image.Filename;
1608 if (
string.IsNullOrWhiteSpace(source) ||
1609 (!source.Contains(
".zarr", StringComparison.OrdinalIgnoreCase) && !Directory.Exists(source)))
1614 var reader = await OmeZarrReader.OpenAsync(source).ConfigureAwait(
false);
1615 var imageNode = await reader.AsMultiscaleImageAsync().ConfigureAwait(
false);
1617 if (!await imageNode.HasLabelsAsync().ConfigureAwait(
false))
1620 var labelGroup = await imageNode.OpenLabelsAsync().ConfigureAwait(
false);
1621 foreach (var labelName
in labelGroup.LabelNames)
1625 var labelNode = await labelGroup.OpenLabelAsync(labelName).ConfigureAwait(
false);
1626 var multiscale = labelNode.Multiscales.FirstOrDefault();
1627 if (multiscale ==
null || multiscale.Datasets.Length == 0)
1630 var level = await labelNode.OpenResolutionLevelAsync(datasetIndex: 0).ConfigureAwait(
false);
1631 var axes = level.EffectiveAxes;
1632 var rawShape = level.Shape;
1633 var shape = rawShape;
1634 bool hasLeadingSingletonT = shape.Length == axes.Length + 1 && shape[0] == 1;
1635 if (hasLeadingSingletonT)
1642 shape = shape.Skip(1).ToArray();
1645 int sizeX = GetAxisSizeStatic(axes, shape,
"x");
1646 int sizeY = GetAxisSizeStatic(axes, shape,
"y");
1647 int sizeZ = Math.Max(1, GetAxisSizeStatic(axes, shape,
"z", 1));
1648 int sizeC = Math.Max(1, GetAxisSizeStatic(axes, shape,
"c", 1));
1649 int sizeT = Math.Max(1, GetAxisSizeStatic(axes, shape,
"t", 1));
1651 if (sizeX <= 0 || sizeY <= 0)
1654 double physX = GetRootPhysicalSizeX(image);
1655 double physY = GetRootPhysicalSizeY(image);
1656 double stageX = image.Volume.Location.X;
1657 double stageY = image.Volume.Location.Y;
1658 if (TryGetMultiscaleLabelTransform(multiscale, axes, out
double labelPhysX, out
double labelPhysY, out
double labelStageX, out
double labelStageY))
1662 stageX = labelStageX;
1663 stageY = labelStageY;
1669 for (
int t = 0; t < sizeT; t++)
1671 for (
int z = 0; z < sizeZ; z++)
1673 for (
int c = 0; c < sizeC; c++)
1677 if (sizeZ == 1 && sizeC == 1 && sizeT == 1 && image.SizeC > 1)
1678 viewerC = image.SizeC - 1;
1680 var regionData = await level.ReadTileAsync(
1687 t: t).ConfigureAwait(
false);
1688 if (regionData ==
null || regionData.Data ==
null || regionData.Data.Length == 0)
1691 GetRegionDimensions(regionData, sizeX, sizeY, out
int planeW, out
int planeH);
1692 if (planeW <= 0 || planeH <= 0)
1695 int totalPixels = planeW * planeH;
1696 if (totalPixels <= 0)
1699 int bytesPerPixel = Math.Max(1, regionData.Data.Length / totalPixels);
1700 var knownLabelValues = GetKnownLabelValues(labelNode.ImageLabelMetadata);
1701 var labelMasks = SplitLabelPlane(regionData.Data, planeW, planeH, bytesPerPixel, knownLabelValues);
1702 if (labelMasks.Count == 0)
1705 var zct =
new AForge.ZCT(z, viewerC, t);
1706 foreach (var kv
in labelMasks)
1708 int labelId = kv.Key;
1709 byte[] maskData = kv.Value;
1710 var color = PickLabelColor(labelNode.ImageLabelMetadata, labelId);
1712 var mask =
new ROI.Mask(
1720 preserveDimensions:
false);
1723 type = ROI.Type.Mask,
1724 roiID = $
"zarr-label:{labelName}_{labelId}",
1725 roiName = labelName,
1726 Text = labelId.ToString(),
1728 shapeIndex = shapeIdx++,
1729 strokeColor = color,
1734 roi.UpdateBoundingBox();
1741 catch (Exception ex)
1743 Console.WriteLine($
"Zarr label '{labelName}' plane t={image.Coordinate.T} c={image.Coordinate.C} z={image.Coordinate.Z} failed: {ex.Message}");
1746 catch (Exception ex)
1748 Console.WriteLine($
"Load Zarr labels failed for '{labelName}': {ex.Message}");
1752 catch (Exception ex)
1754 Console.WriteLine($
"Load Zarr labels failed: {ex.Message}");
1760 private static async System.Threading.Tasks.Task WriteJsonAsync(
1761 ZarrNET.Core.Zarr.Store.IZarrStore store,
1764 CancellationToken ct =
default)
1766 var options =
new JsonSerializerOptions { WriteIndented =
true };
1767 var json = JsonSerializer.SerializeToUtf8Bytes(document, options);
1768 await store.WriteAsync(key, json, ct).ConfigureAwait(
false);
1771 private static int ParseLabelId(
string? text,
int fallback)
1773 if (!
string.IsNullOrWhiteSpace(text) &&
1774 int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out
int parsed))
1782 private static bool IsSupportedLabelRoi(ROI roi)
1787 return roi.type == ROI.Type.Mask ||
1788 roi.type == ROI.Type.Point ||
1789 roi.type == ROI.Type.Line ||
1790 roi.type == ROI.Type.Rectangle ||
1791 roi.type == ROI.Type.Ellipse ||
1792 roi.type == ROI.Type.Polygon ||
1793 roi.type == ROI.Type.Polyline ||
1794 roi.type == ROI.Type.Freeform ||
1795 roi.type == ROI.Type.Label;
1798 private static void RasterizeRoi(
1809 double targetPhysicalSizeX,
1810 double targetPhysicalSizeY)
1812 if (roi ==
null || labelPlane ==
null || labelPlane.Length == 0)
1815 if (roi.type == ROI.Type.Mask && roi.roiMask !=
null)
1826 targetPhysicalSizeX,
1827 targetPhysicalSizeY);
1831 int thickness = Math.Max(1, (
int)Math.Round(Math.Max(1.0, roi.strokeWidth)));
1835 case ROI.Type.Point:
1836 case ROI.Type.Label:
1839 int px = (int)Math.Round(p.X * scaleX);
1840 int py = (int)Math.Round(p.Y * scaleY);
1841 PlotThickPoint(labelPlane, imageWidth, imageHeight, planeOffset, px, py, labelId, Math.Max(1, thickness));
1847 var pts = roi.GetPoints();
1848 if (pts ==
null || pts.Length < 2)
1851 DrawLine(labelPlane, imageWidth, imageHeight, planeOffset,
1852 (
int)Math.Round(pts[0].X * scaleX), (
int)Math.Round(pts[0].Y * scaleY),
1853 (
int)Math.Round(pts[1].X * scaleX), (
int)Math.Round(pts[1].Y * scaleY),
1854 labelId, thickness);
1858 case ROI.Type.Rectangle:
1859 FillRectangle(labelPlane, imageWidth, imageHeight, planeOffset, roi.BoundingBox, labelId, scaleX, scaleY);
1862 case ROI.Type.Ellipse:
1863 FillEllipse(labelPlane, imageWidth, imageHeight, planeOffset, roi.BoundingBox, labelId, scaleX, scaleY);
1866 case ROI.Type.Polygon:
1867 RasterizePolygonLike(labelPlane, imageWidth, imageHeight, planeOffset, roi, labelId, scaleX, scaleY, thickness);
1870 case ROI.Type.Polyline:
1871 RasterizePolyline(labelPlane, imageWidth, imageHeight, planeOffset, roi, labelId, scaleX, scaleY, thickness);
1874 case ROI.Type.Freeform:
1875 RasterizePolygonLike(labelPlane, imageWidth, imageHeight, planeOffset, roi, labelId, scaleX, scaleY, thickness);
1880 private static void RasterizePolyline(
short[] labelPlane,
int imageWidth,
int imageHeight,
int planeOffset, ROI roi,
int labelId,
double scaleX,
double scaleY,
int thickness)
1882 var pts = roi.GetPoints();
1883 if (pts ==
null || pts.Length < 2)
1886 for (
int i = 0; i < pts.Length - 1; i++)
1888 DrawLine(labelPlane, imageWidth, imageHeight, planeOffset,
1889 (
int)Math.Round(pts[i].X * scaleX), (
int)Math.Round(pts[i].Y * scaleY),
1890 (
int)Math.Round(pts[i + 1].X * scaleX), (
int)Math.Round(pts[i + 1].Y * scaleY),
1891 labelId, thickness);
1895 private static void RasterizePolygonLike(
short[] labelPlane,
int imageWidth,
int imageHeight,
int planeOffset, ROI roi,
int labelId,
double scaleX,
double scaleY,
int thickness)
1897 var pts = roi.GetPoints();
1898 if (pts ==
null || pts.Length == 0)
1903 RasterizePolyline(labelPlane, imageWidth, imageHeight, planeOffset, roi, labelId, scaleX, scaleY, thickness);
1907 if (roi.type == ROI.Type.Freeform || roi.closed)
1909 var scaled = pts.Select(p =>
new PointD(p.X * scaleX, p.Y * scaleY)).ToArray();
1910 FillPolygon(labelPlane, imageWidth, imageHeight, planeOffset, scaled, labelId);
1914 RasterizePolyline(labelPlane, imageWidth, imageHeight, planeOffset, roi, labelId, scaleX, scaleY, thickness);
1918 private static void FillRectangle(
short[] labelPlane,
int imageWidth,
int imageHeight,
int planeOffset, AForge.RectangleD rect,
int labelId,
double scaleX,
double scaleY)
1920 double x0 = rect.X * scaleX;
1921 double y0 = rect.Y * scaleY;
1922 double x1 = (rect.X + rect.W) * scaleX;
1923 double y1 = (rect.Y + rect.H) * scaleY;
1925 int minX = Math.Max(0, (
int)Math.Floor(Math.Min(x0, x1)));
1926 int maxX = Math.Min(imageWidth - 1, (
int)Math.Ceiling(Math.Max(x0, x1)));
1927 int minY = Math.Max(0, (
int)Math.Floor(Math.Min(y0, y1)));
1928 int maxY = Math.Min(imageHeight - 1, (
int)Math.Ceiling(Math.Max(y0, y1)));
1930 for (
int y = minY; y <= maxY; y++)
1932 int rowOffset = planeOffset + (y * imageWidth);
1933 for (
int x = minX; x <= maxX; x++)
1935 if (rowOffset + x >= 0 && rowOffset + x < labelPlane.Length)
1936 labelPlane[rowOffset + x] = (short)Math.Clamp(labelId,
short.MinValue,
short.MaxValue);
1941 private static void FillEllipse(
short[] labelPlane,
int imageWidth,
int imageHeight,
int planeOffset, AForge.RectangleD rect,
int labelId,
double scaleX,
double scaleY)
1943 double x0 = rect.X * scaleX;
1944 double y0 = rect.Y * scaleY;
1945 double x1 = (rect.X + rect.W) * scaleX;
1946 double y1 = (rect.Y + rect.H) * scaleY;
1948 double left = Math.Min(x0, x1);
1949 double top = Math.Min(y0, y1);
1950 double right = Math.Max(x0, x1);
1951 double bottom = Math.Max(y0, y1);
1952 double rx = (right - left) / 2.0;
1953 double ry = (bottom - top) / 2.0;
1954 if (rx <= 0 || ry <= 0)
1956 FillRectangle(labelPlane, imageWidth, imageHeight, planeOffset, rect, labelId, scaleX, scaleY);
1960 double cx = left + rx;
1961 double cy = top + ry;
1962 int minX = Math.Max(0, (
int)Math.Floor(left));
1963 int maxX = Math.Min(imageWidth - 1, (
int)Math.Ceiling(right));
1964 int minY = Math.Max(0, (
int)Math.Floor(top));
1965 int maxY = Math.Min(imageHeight - 1, (
int)Math.Ceiling(bottom));
1967 for (
int y = minY; y <= maxY; y++)
1969 int rowOffset = planeOffset + (y * imageWidth);
1970 double ny = ((y + 0.5) - cy) / ry;
1971 for (
int x = minX; x <= maxX; x++)
1973 double nx = ((x + 0.5) - cx) / rx;
1974 if ((nx * nx) + (ny * ny) <= 1.0 && rowOffset + x >= 0 && rowOffset + x < labelPlane.Length)
1975 labelPlane[rowOffset + x] = (short)Math.Clamp(labelId,
short.MinValue,
short.MaxValue);
1980 private static void FillPolygon(
short[] labelPlane,
int imageWidth,
int imageHeight,
int planeOffset, PointD[] points,
int labelId)
1982 if (points ==
null || points.Length < 3)
1985 double minX = points.Min(p => p.X);
1986 double maxX = points.Max(p => p.X);
1987 double minY = points.Min(p => p.Y);
1988 double maxY = points.Max(p => p.Y);
1990 int iMinX = Math.Max(0, (
int)Math.Floor(minX));
1991 int iMaxX = Math.Min(imageWidth - 1, (
int)Math.Ceiling(maxX));
1992 int iMinY = Math.Max(0, (
int)Math.Floor(minY));
1993 int iMaxY = Math.Min(imageHeight - 1, (
int)Math.Ceiling(maxY));
1995 for (
int y = iMinY; y <= iMaxY; y++)
1997 int rowOffset = planeOffset + (y * imageWidth);
1998 for (
int x = iMinX; x <= iMaxX; x++)
2000 if (!PointInPolygon(x + 0.5, y + 0.5, points))
2003 if (rowOffset + x >= 0 && rowOffset + x < labelPlane.Length)
2004 labelPlane[rowOffset + x] = (short)Math.Clamp(labelId,
short.MinValue,
short.MaxValue);
2009 private static bool PointInPolygon(
double x,
double y, PointD[] points)
2011 bool inside =
false;
2012 int j = points.Length - 1;
2014 for (
int i = 0; i < points.Length; i++)
2016 double xi = points[i].X;
2017 double yi = points[i].Y;
2018 double xj = points[j].X;
2019 double yj = points[j].Y;
2021 bool intersect = ((yi > y) != (yj > y)) &&
2022 (x < (xj - xi) * (y - yi) / Math.Max(1e-12, yj - yi) + xi);
2032 private static void DrawLine(
short[] labelPlane,
int imageWidth,
int imageHeight,
int planeOffset,
int x0,
int y0,
int x1,
int y1,
int labelId,
int thickness)
2034 int dx = Math.Abs(x1 - x0);
2035 int sx = x0 < x1 ? 1 : -1;
2036 int dy = -Math.Abs(y1 - y0);
2037 int sy = y0 < y1 ? 1 : -1;
2042 PlotThickPoint(labelPlane, imageWidth, imageHeight, planeOffset, x0, y0, labelId, thickness);
2043 if (x0 == x1 && y0 == y1)
2060 private static void PlotThickPoint(
short[] labelPlane,
int imageWidth,
int imageHeight,
int planeOffset,
int x,
int y,
int labelId,
int thickness)
2062 int radius = Math.Max(0, (thickness - 1) / 2);
2063 for (
int yy = y - radius; yy <= y + radius; yy++)
2065 if (yy < 0 || yy >= imageHeight)
2068 int rowOffset = planeOffset + (yy * imageWidth);
2069 for (
int xx = x - radius; xx <= x + radius; xx++)
2071 if (xx < 0 || xx >= imageWidth)
2074 int index = rowOffset + xx;
2075 if (index >= 0 && index < labelPlane.Length)
2076 labelPlane[index] = (short)Math.Clamp(labelId,
short.MinValue,
short.MaxValue);
2081 private static void RasterizeMask(
2090 double targetPhysicalSizeX,
2091 double targetPhysicalSizeY)
2093 targetPhysicalSizeX = targetPhysicalSizeX > 0 ? targetPhysicalSizeX : 1.0;
2094 targetPhysicalSizeY = targetPhysicalSizeY > 0 ? targetPhysicalSizeY : 1.0;
2095 double maskPhysicalSizeX = mask.PhysicalSizeX > 0 ? mask.PhysicalSizeX : targetPhysicalSizeX;
2096 double maskPhysicalSizeY = mask.PhysicalSizeY > 0 ? mask.PhysicalSizeY : targetPhysicalSizeY;
2100 int endY = mask.Height;
2101 int endX = mask.Width;
2103 for (
int y = startY; y < endY; y++)
2105 double worldY = (mask.Y + y) * maskPhysicalSizeY;
2106 int dstY = (int)Math.Round((worldY - imageStageY) / targetPhysicalSizeY);
2107 if (dstY < 0 || dstY >= imageHeight)
2110 int rowOffset = dstY * imageWidth;
2112 for (
int x = startX; x < endX; x++)
2114 if (mask.GetValue(x, y) <= 0)
2117 double worldX = (mask.X + x) * maskPhysicalSizeX;
2118 int dstX = (int)Math.Round((worldX - imageStageX) / targetPhysicalSizeX);
2119 if (dstX < 0 || dstX >= imageWidth)
2122 int index = planeOffset + rowOffset + dstX;
2123 if (index >= 0 && index < labelPlane.Length)
2124 labelPlane[index] = (short)Math.Clamp(labelId,
short.MinValue,
short.MaxValue);
2129 private static int GetAxisLength(AxisMetadata[] axes,
long[] shape,
string axisName)
2131 for (
int i = 0; i < axes.Length && i < shape.Length; i++)
2133 if (
string.Equals(axes[i].Name, axisName, StringComparison.OrdinalIgnoreCase))
2134 return (
int)shape[i];
2139 private static void GetRegionDimensions(RegionResult region,
int fallbackWidth,
int fallbackHeight, out
int width, out
int height)
2141 width = Math.Max(1, fallbackWidth);
2142 height = Math.Max(1, fallbackHeight);
2144 if (region?.Shape ==
null || region.Shape.Length == 0)
2147 if (region.Axes !=
null && region.Axes.Length == region.Shape.Length)
2149 for (
int i = 0; i < region.Axes.Length; i++)
2151 string axisName = region.Axes[i].Name?.ToLowerInvariant() ??
string.Empty;
2152 if (axisName ==
"x")
2153 width = (int)Math.Max(1, region.Shape[i]);
2154 else if (axisName ==
"y")
2155 height = (int)Math.Max(1, region.Shape[i]);
2160 if (region.Shape.Length >= 2)
2162 width = (int)Math.Max(1, region.Shape[^1]);
2163 height = (int)Math.Max(1, region.Shape[^2]);
2167 private static HashSet<int>? GetKnownLabelValues(ImageLabelMetadata metadata)
2169 if (metadata?.Colors ==
null)
2172 HashSet<int> values =
new();
2173 foreach (var entry
in metadata.Colors)
2175 if (entry.LabelValue > 0)
2176 values.Add(entry.LabelValue);
2179 return values.Count > 0 ? values :
null;
2182 private static Dictionary<int, byte[]> SplitLabelPlane(
byte[] raw,
int w,
int h,
int bytesPerPixel, ISet<int>? knownLabelValues =
null)
2185 var result =
new Dictionary<int, byte[]>();
2187 for (
int i = 0; i < total; i++)
2189 int labelId = ReadLabelId(raw, i, bytesPerPixel);
2192 if (knownLabelValues !=
null && !knownLabelValues.Contains(labelId))
2195 if (!result.TryGetValue(labelId, out
byte[]? mask))
2197 mask =
new byte[total];
2198 result[labelId] = mask;
2207 private static int ReadLabelId(
byte[] raw,
int pixelIndex,
int bytesPerPixel)
2209 int byteOffset = pixelIndex * bytesPerPixel;
2210 if (byteOffset < 0 || byteOffset >= raw.Length)
2213 return bytesPerPixel
switch
2215 1 => raw[byteOffset],
2216 2 => BitConverter.ToUInt16(raw, byteOffset),
2217 4 => BitConverter.ToInt32(raw, byteOffset),
2218 _ => raw[byteOffset],
2222 private static long[] BuildStart(AxisMetadata[] axes,
long[] shape,
int t,
int c,
int z)
2224 long[] start =
new long[axes.Length];
2225 for (
int i = 0; i < axes.Length; i++)
2227 start[i] = axes[i].Name.ToLowerInvariant()
switch
2238 private static long[] BuildEnd(AxisMetadata[] axes,
long[] shape,
int t,
int c,
int z,
int sizeX,
int sizeY)
2240 long[] end =
new long[axes.Length];
2241 for (
int i = 0; i < axes.Length; i++)
2243 end[i] = axes[i].Name.ToLowerInvariant()
switch
2256 private static int GetAxisSizeStatic(AxisMetadata[] axes,
long[] shape,
string name,
int fallback = 0)
2258 for (
int i = 0; i < axes.Length; i++)
2260 if (axes[i].Name.Equals(name, StringComparison.OrdinalIgnoreCase))
2261 return (
int)shape[i];
2266 private static byte[] ToBinaryMask(
byte[] data,
string dataType,
int pixelCount)
2268 int bytesPerPixel = dataType
switch
2280 _ => pixelCount > 0 ? Math.Max(1, data.Length / pixelCount) : 1
2283 if (pixelCount <= 0 || data.Length == 0)
2284 return Array.Empty<
byte>();
2286 var mask =
new byte[pixelCount];
2287 for (
int i = 0; i < pixelCount; i++)
2289 int offset = i * bytesPerPixel;
2290 if (offset >= data.Length)
2293 bool nonZero =
false;
2294 for (
int b = 0; b < bytesPerPixel && (offset + b) < data.Length; b++)
2296 if (data[offset + b] != 0)
2310 private static object[] BuildV2Datasets(BioImage image, List<ExportLevelInfo> exportLevels)
2312 var datasets =
new List<object>();
2313 for (
int i = 0; i < exportLevels.Count; i++)
2315 var exportLevel = exportLevels[i];
2318 path = i.ToString(),
2319 coordinateTransformations =
new object[]
2328 exportLevel.PhysicalSizeZ > 0 ? exportLevel.PhysicalSizeZ : 1.0,
2329 exportLevel.PhysicalSizeY > 0 ? exportLevel.PhysicalSizeY : 1.0,
2330 exportLevel.PhysicalSizeX > 0 ? exportLevel.PhysicalSizeX : 1.0
2337 return datasets.ToArray();
2340 private static void WriteRootMetadata(
string outputDir, BioImage image, List<ExportLevelInfo> exportLevels)
2342 var omeroNode = ZarrMetadataFixup.CreateRgbDisplayMetadata(image);
2343 var multiscales =
new[]
2348 name = Path.GetFileNameWithoutExtension(image.Filename),
2351 new { name =
"t", type =
"time" },
2352 new { name =
"c", type =
"channel" },
2353 new { name =
"z", type =
"space", unit =
"micrometer" },
2354 new { name =
"y", type =
"space", unit =
"micrometer" },
2355 new { name =
"x", type =
"space", unit =
"micrometer" }
2357 datasets = BuildV3Datasets(exportLevels)
2364 node_type =
"group",
2377 Path.Combine(outputDir,
"zarr.json"),
2378 JsonSerializer.Serialize(rootDoc,
new JsonSerializerOptions { WriteIndented = true }));
2381 private static object[] BuildV3Datasets(List<ExportLevelInfo> exportLevels)
2383 var datasets =
new List<object>();
2384 for (
int i = 0; i < exportLevels.Count; i++)
2386 var exportLevel = exportLevels[i];
2389 path = i.ToString(),
2390 coordinateTransformations =
new object[]
2399 exportLevel.PhysicalSizeZ > 0 ? exportLevel.PhysicalSizeZ : 1.0,
2400 exportLevel.PhysicalSizeY > 0 ? exportLevel.PhysicalSizeY : 1.0,
2401 exportLevel.PhysicalSizeX > 0 ? exportLevel.PhysicalSizeX : 1.0
2408 return datasets.ToArray();
2411 private static List<ExportLevelInfo> BuildExportLevels(BioImage b)
2413 var exportLevels =
new List<ExportLevelInfo>();
2415 return exportLevels;
2417 if (b.Resolutions.Count == 0)
2419 exportLevels.Add(
new ExportLevelInfo(
2423 new ResolutionLevelDescriptor(b.SizeX, b.SizeY, 1.0),
2427 GetRootPhysicalSizeX(b),
2428 GetRootPhysicalSizeY(b),
2429 GetRootPhysicalSizeZ(b)));
2430 return exportLevels;
2433 int lastSizeX =
int.MaxValue;
2434 int lastSizeY =
int.MaxValue;
2435 bool haveLast =
false;
2437 for (
int sourceIndex = 0; sourceIndex < b.Resolutions.Count; sourceIndex++)
2439 var res = b.Resolutions[sourceIndex];
2440 if (res.SizeX <= 0 || res.SizeY <= 0)
2443 if (haveLast && (res.SizeX > lastSizeX || res.SizeY > lastSizeY))
2446 if (haveLast && res.SizeX == lastSizeX && res.SizeY == lastSizeY)
2449 double downsample = b.GetLevelDownsample(sourceIndex);
2450 if (!
double.IsFinite(downsample) || downsample <= 0)
2453 double basePhysicalSizeX = exportLevels.Count > 0
2454 ? exportLevels[0].PhysicalSizeX
2455 : GetRootPhysicalSizeX(b);
2456 double basePhysicalSizeY = exportLevels.Count > 0
2457 ? exportLevels[0].PhysicalSizeY
2458 : GetRootPhysicalSizeY(b);
2459 double physicalSizeX = NormalizePhysicalSize(basePhysicalSizeX * downsample);
2460 double physicalSizeY = NormalizePhysicalSize(basePhysicalSizeY * downsample);
2462 exportLevels.Add(
new ExportLevelInfo(
2466 new ResolutionLevelDescriptor(res.SizeX, res.SizeY, downsample),
2472 NormalizePhysicalSize(res.PhysicalSizeZ)));
2474 lastSizeX = res.SizeX;
2475 lastSizeY = res.SizeY;
2479 if (exportLevels.Count == 0 && b.SizeX > 0 && b.SizeY > 0)
2481 exportLevels.Add(
new ExportLevelInfo(
2485 new ResolutionLevelDescriptor(b.SizeX, b.SizeY, 1.0),
2489 GetRootPhysicalSizeX(b),
2490 GetRootPhysicalSizeY(b),
2491 GetRootPhysicalSizeZ(b)));
2494 return exportLevels;
2497 private static bool IsAssociatedResolution(BioImage image,
int sourceIndex)
2502 return (image.MacroResolution.HasValue && image.MacroResolution.Value == sourceIndex) ||
2503 (image.LabelResolution.HasValue && image.LabelResolution.Value == sourceIndex);
2506 private static string MapV2Dtype(AForge.PixelFormat pixelFormat)
2508 return pixelFormat
switch
2510 AForge.PixelFormat.Format8bppIndexed =>
"|u1",
2511 AForge.PixelFormat.Format24bppRgb =>
"|u1",
2512 AForge.PixelFormat.Format32bppArgb =>
"|u1",
2513 AForge.PixelFormat.Format32bppRgb =>
"|u1",
2514 AForge.PixelFormat.Format16bppGrayScale =>
"<u2",
2515 AForge.PixelFormat.Format48bppRgb =>
"<u2",
2520 private static object BuildV2BloscCompressor(AForge.PixelFormat pixelFormat)
2532 private static void WriteText(
string path,
string content)
2534 var directory = Path.GetDirectoryName(path);
2535 if (!
string.IsNullOrWhiteSpace(directory))
2536 Directory.CreateDirectory(directory);
2538 File.WriteAllText(path, content);
2541 private static bool IsSupportedLabelDataType(
string dataType)
2543 return string.Equals(dataType,
"bool", StringComparison.OrdinalIgnoreCase) ||
2544 string.Equals(dataType,
"int8", StringComparison.OrdinalIgnoreCase) ||
2545 string.Equals(dataType,
"uint8", StringComparison.OrdinalIgnoreCase) ||
2546 string.Equals(dataType,
"int16", StringComparison.OrdinalIgnoreCase) ||
2547 string.Equals(dataType,
"uint16", StringComparison.OrdinalIgnoreCase) ||
2548 string.Equals(dataType,
"int32", StringComparison.OrdinalIgnoreCase) ||
2549 string.Equals(dataType,
"uint32", StringComparison.OrdinalIgnoreCase) ||
2550 string.Equals(dataType,
"int64", StringComparison.OrdinalIgnoreCase) ||
2551 string.Equals(dataType,
"uint64", StringComparison.OrdinalIgnoreCase) ||
2552 string.Equals(dataType,
"float32", StringComparison.OrdinalIgnoreCase) ||
2553 string.Equals(dataType,
"float64", StringComparison.OrdinalIgnoreCase);
2556 private static Color PickLabelColor(ImageLabelMetadata metadata,
int labelValue)
2558 if (metadata?.Colors !=
null)
2560 foreach (var entry
in metadata.Colors)
2562 if (entry.LabelValue != labelValue)
2565 var rgba = entry.Rgba;
2566 if (rgba !=
null && rgba.Length >= 4 &&
2567 (rgba[0] != 0 || rgba[1] != 0 || rgba[2] != 0 || rgba[3] != 0))
2569 return Color.FromArgb(rgba[3], rgba[0], rgba[1], rgba[2]);
2574 return GetFallbackLabelColor(labelValue);
2577 private static bool TryGetMultiscaleLabelTransform(
object? multiscale, AxisMetadata[] axes, out
double physX, out
double physY, out
double stageX, out
double stageY)
2584 if (multiscale ==
null || axes ==
null || axes.Length == 0)
2587 object? dataset = GetEnumerableFirst(GetPropertyValue(multiscale,
"Datasets"));
2588 if (dataset ==
null)
2591 object? transforms = GetPropertyValue(dataset,
"CoordinateTransformations");
2592 if (transforms ==
null)
2595 int xIndex = Array.FindIndex(axes, a =>
string.Equals(a.Name,
"x", StringComparison.OrdinalIgnoreCase));
2596 int yIndex = Array.FindIndex(axes, a =>
string.Equals(a.Name,
"y", StringComparison.OrdinalIgnoreCase));
2597 if (xIndex < 0 || yIndex < 0)
2600 double[] origin = ApplyCoordinateTransforms(transforms, axes.Length, xIndex, yIndex, 0.0, 0.0);
2601 double[] xPoint = ApplyCoordinateTransforms(transforms, axes.Length, xIndex, yIndex, 1.0, 0.0);
2602 double[] yPoint = ApplyCoordinateTransforms(transforms, axes.Length, xIndex, yIndex, 0.0, 1.0);
2604 if (origin.Length <= Math.Max(xIndex, yIndex) ||
2605 xPoint.Length <= xIndex ||
2606 yPoint.Length <= yIndex)
2609 stageX = origin[xIndex];
2610 stageY = origin[yIndex];
2611 physX = xPoint[xIndex] - origin[xIndex];
2612 physY = yPoint[yIndex] - origin[yIndex];
2614 if (!
double.IsFinite(physX) || physX <= 0)
2616 if (!
double.IsFinite(physY) || physY <= 0)
2622 private static object? GetEnumerableFirst(
object? value)
2624 if (value is not System.Collections.IEnumerable enumerable)
2627 foreach (var item
in enumerable)
2632 private static object? GetPropertyValue(
object? obj, params
string[] names)
2634 if (obj ==
null || names ==
null || names.Length == 0)
2637 var type = obj.GetType();
2638 foreach (var name
in names)
2640 var prop = type.GetProperty(name);
2642 return prop.GetValue(obj);
2648 private static double[] ApplyCoordinateTransforms(
object transforms,
int rank,
int xIndex,
int yIndex,
double x,
double y)
2650 var coordinates =
new double[Math.Max(rank, Math.Max(xIndex, yIndex) + 1)];
2651 if (xIndex >= 0 && xIndex < coordinates.Length)
2652 coordinates[xIndex] = x;
2653 if (yIndex >= 0 && yIndex < coordinates.Length)
2654 coordinates[yIndex] = y;
2656 if (transforms is not System.Collections.IEnumerable enumerable)
2659 foreach (var transform
in enumerable)
2661 if (transform ==
null)
2664 string? type = GetPropertyValue(transform,
"Type")?.ToString() ?? GetPropertyValue(transform,
"type")?.ToString();
2665 if (
string.IsNullOrWhiteSpace(type))
2668 if (type.Equals(
"scale", StringComparison.OrdinalIgnoreCase))
2670 double[]? scale = GetDoubleArray(GetPropertyValue(transform,
"Scale",
"scale"));
2671 if (scale ==
null || scale.Length == 0)
2674 for (
int i = 0; i < coordinates.Length && i < scale.Length; i++)
2675 coordinates[i] *= scale[i];
2677 else if (type.Equals(
"translation", StringComparison.OrdinalIgnoreCase))
2679 double[]? translation = GetDoubleArray(GetPropertyValue(transform,
"Translation",
"translation"));
2680 if (translation ==
null || translation.Length == 0)
2683 for (
int i = 0; i < coordinates.Length && i < translation.Length; i++)
2684 coordinates[i] += translation[i];
2691 private static double[]? GetDoubleArray(
object? value)
2696 if (value is
double[] d)
2699 if (value is
float[] f)
2700 return f.Select(v => (
double)v).ToArray();
2702 if (value is
int[] i)
2703 return i.Select(v => (
double)v).ToArray();
2705 if (value is
long[] l)
2706 return l.Select(v => (
double)v).ToArray();
2708 if (value is System.Collections.IEnumerable enumerable)
2710 var list =
new List<double>();
2711 foreach (var item
in enumerable)
2716 list.Add(Convert.ToDouble(item, CultureInfo.InvariantCulture));
2718 return list.ToArray();
2724 private static Color GetFallbackLabelColor(
int labelValue)
2726 lock (s_labelColorLock)
2728 if (s_generatedLabelColors.TryGetValue(labelValue, out var cached))
2731 var color = ColorFromHsv((s_labelColorRandom.NextDouble() + (labelValue * 0.6180339887498949)) % 1.0);
2732 s_generatedLabelColors[labelValue] = color;
2737 private static Color ColorFromHsv(
double hue)
2739 double saturation = 0.65 + (s_labelColorRandom.NextDouble() * 0.2);
2740 double value = 0.80 + (s_labelColorRandom.NextDouble() * 0.15);
2741 hue = (hue % 1.0 + 1.0) % 1.0;
2742 double h = hue * 6.0;
2743 int sector = (int)Math.Floor(h);
2744 double fraction = h - sector;
2745 double p = value * (1.0 - saturation);
2746 double q = value * (1.0 - saturation * fraction);
2747 double t = value * (1.0 - saturation * (1.0 - fraction));
2752 case 0: r = value; g = t; b = p;
break;
2753 case 1: r = q; g = value; b = p;
break;
2754 case 2: r = p; g = value; b = t;
break;
2755 case 3: r = p; g = q; b = value;
break;
2756 case 4: r = t; g = p; b = value;
break;
2757 default: r = value; g = p; b = q;
break;
2760 return Color.FromArgb(
2762 (
byte)Math.Clamp((
int)Math.Round(r * 255.0), 0, 255),
2763 (
byte)Math.Clamp((
int)Math.Round(g * 255.0), 0, 255),
2764 (
byte)Math.Clamp((
int)Math.Round(b * 255.0), 0, 255));
2779 private static byte[] DeinterleaveChannel(
2786 var output =
new byte[pixelCount * bytesPerSample];
2787 var stride = totalChannels * bytesPerSample;
2788 var channelStart = channelIndex * bytesPerSample;
2790 for (
int px = 0; px < pixelCount; px++)
2792 var srcOffset = px * stride + channelStart;
2793 var dstOffset = px * bytesPerSample;
2795 for (
int byteIdx = 0; byteIdx < bytesPerSample; byteIdx++)
2796 output[dstOffset + byteIdx] = interleaved[srcOffset + byteIdx];