diff --git a/BioImager.csproj b/BioImager.csproj index ebbf48c..742a458 100644 --- a/BioImager.csproj +++ b/BioImager.csproj @@ -14,12 +14,12 @@ Microscopy;Biology; GPL-3.0-only A .NET microscopy imaging application based on Bio library. Supports various microscopes by using imported libraries & GUI automation. Supported libraries include Prior® & Zeiss® & all devices supported by Micromanager 2.0 and python-microscope. - 3.9.1 + 4.0.0 https://github.com/BiologyTools/BioImager True - Accelerated graphics fix. + OME type slides fix. Support for 16 bit slides. AnyCPU;x86;x64 diff --git a/Source/Bio/ISlideSource.cs b/Source/Bio/ISlideSource.cs index 4e5b68b..fe2d6cb 100644 --- a/Source/Bio/ISlideSource.cs +++ b/Source/Bio/ISlideSource.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using AForge; +using Image = SixLabors.ImageSharp.Image; namespace BioImager { public class LruCache @@ -62,6 +63,13 @@ public void Add(Info key, TValue value) lruList.AddLast(newNode); cacheMap[key] = newNode; } + public void Dispose() + { + foreach (LinkedListNode<(Info key, TValue value)> item in cacheMap.Values) + { + lruList.Remove(item); + } + } } public class TileCache { @@ -110,6 +118,10 @@ private async Task LoadTile(TileInformation tileId) return null; } } + public void Dispose() + { + cache.Dispose(); + } } public class TileInformation @@ -160,7 +172,6 @@ public static ISlideSource Create(BioImage source, SlideImage im, bool enableCac } #endregion public double MinUnitsPerPixel { get; protected set; } - public static byte[] LastSlice; public static Extent destExtent; public static Extent sourceExtent; public static double curUnitsPerPixel = 1; @@ -194,9 +205,12 @@ public async Task GetSlice(SliceInfo sliceInfo) { try { - NetVips.Image im = OpenSlideGTK.ImageUtil.JoinVips(tiles, srcPixelExtent, new Extent(0, 0, dstPixelWidth, dstPixelHeight)); - LastSlice = im.WriteToMemory(); - return LastSlice; + NetVips.Image im = null; + if (this.Image.BioImage.Resolutions[curLevel].PixelFormat == PixelFormat.Format16bppGrayScale) + im = ImageUtil.JoinVips16(tiles, srcPixelExtent, new Extent(0, 0, dstPixelWidth, dstPixelHeight)); + else if(this.Image.BioImage.Resolutions[curLevel].PixelFormat == PixelFormat.Format24bppRgb) + im = ImageUtil.JoinVipsRGB24(tiles, srcPixelExtent, new Extent(0, 0, dstPixelWidth, dstPixelHeight)); + return im.WriteToMemory(); } catch (Exception e) { @@ -207,16 +221,28 @@ public async Task GetSlice(SliceInfo sliceInfo) } try { - Image im = OpenSlideGTK.ImageUtil.Join(tiles, srcPixelExtent, new Extent(0, 0, dstPixelWidth, dstPixelHeight)); - LastSlice = GetRgb24Bytes(im); - im.Dispose(); + Image im = null; + if (this.Image.BioImage.Resolutions[curLevel].PixelFormat == PixelFormat.Format16bppGrayScale) + { + im = ImageUtil.Join16(tiles, srcPixelExtent, new Extent(0, 0, dstPixelWidth, dstPixelHeight)); + byte[] bts = Get16Bytes((Image)im); + im.Dispose(); + return bts; + } + else if (this.Image.BioImage.Resolutions[curLevel].PixelFormat == PixelFormat.Format24bppRgb) + { + im = ImageUtil.JoinRGB24(tiles, srcPixelExtent, new Extent(0, 0, dstPixelWidth, dstPixelHeight)); + byte[] bts = GetRgb24Bytes((Image)im); + im.Dispose(); + return bts; + } } catch (Exception er) { Console.WriteLine(er.Message); return null; } - return LastSlice; + return null; } public byte[] GetRgb24Bytes(Image image) { @@ -230,14 +256,34 @@ public byte[] GetRgb24Bytes(Image image) for (int x = 0; x < width; x++) { Rgb24 pixel = image[x, y]; - rgbBytes[byteIndex++] = pixel.R; - rgbBytes[byteIndex++] = pixel.G; rgbBytes[byteIndex++] = pixel.B; + rgbBytes[byteIndex++] = pixel.G; + rgbBytes[byteIndex++] = pixel.R; } } return rgbBytes; } + public byte[] Get16Bytes(Image image) + { + int width = image.Width; + int height = image.Height; + byte[] bytes = new byte[width * height * 2]; + + int byteIndex = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + L16 pixel = image[x, y]; + byte[] bts = BitConverter.GetBytes(pixel.PackedValue); + bytes[byteIndex++] = bts[0]; + bytes[byteIndex++] = bts[1]; + } + } + + return bytes; + } public SlideImage Image { get; set; } @@ -291,11 +337,7 @@ public async Task GetTileAsync(TileInformation tileInfo) var curTileWidth = (int)(tileInfo.Extent.MaxX > Schema.Extent.Width ? tileWidth - (tileInfo.Extent.MaxX - Schema.Extent.Width) / r : tileWidth); var curTileHeight = (int)(-tileInfo.Extent.MinY > Schema.Extent.Height ? tileHeight - (-tileInfo.Extent.MinY - Schema.Extent.Height) / r : tileHeight); var bgraData = await Image.ReadRegionAsync(tileInfo.Index.Level, (long)curLevelOffsetXPixel, (long)curLevelOffsetYPixel, curTileWidth, curTileHeight,tileInfo.Coordinate); - //We check to see if the data is valid. - if (bgraData.Length != curTileWidth * curTileHeight * 4) - return null; - byte[] bm = ConvertRgbaToRgb(bgraData); - return bm; + return bgraData; } public async Task GetTileAsync(BruTile.TileInfo tileInfo) { @@ -309,11 +351,7 @@ public async Task GetTileAsync(BruTile.TileInfo tileInfo) var curTileWidth = (int)(tileInfo.Extent.MaxX > Schema.Extent.Width ? tileWidth - (tileInfo.Extent.MaxX - Schema.Extent.Width) / r : tileWidth); var curTileHeight = (int)(-tileInfo.Extent.MinY > Schema.Extent.Height ? tileHeight - (-tileInfo.Extent.MinY - Schema.Extent.Height) / r : tileHeight); var bgraData = await Image.ReadRegionAsync(tileInfo.Index.Level, (long)curLevelOffsetXPixel, (long)curLevelOffsetYPixel, curTileWidth, curTileHeight, new ZCT()); - //We check to see if the data is valid. - if (bgraData.Length != curTileWidth * curTileHeight * 4) - return null; - byte[] bm = ConvertRgbaToRgb(bgraData); - return bm; + return bgraData; } public static byte[] ConvertRgbaToRgb(byte[] rgbaArray) { diff --git a/Source/Bio/SlideBase.cs b/Source/Bio/SlideBase.cs index d191df4..6c9820e 100644 --- a/Source/Bio/SlideBase.cs +++ b/Source/Bio/SlideBase.cs @@ -7,7 +7,6 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.PixelFormats; -using AForge; namespace BioImager { diff --git a/Source/Bio/SlideImage.cs b/Source/Bio/SlideImage.cs index 66ea767..8bcd850 100644 --- a/Source/Bio/SlideImage.cs +++ b/Source/Bio/SlideImage.cs @@ -1,7 +1,6 @@ using AForge; using OpenSlideGTK; using OpenSlideGTK.Interop; -using org.checkerframework.common.returnsreceiver.qual; using System; using System.Collections.Generic; using System.IO; @@ -224,7 +223,7 @@ public int GetBestLevelForDownsample(double downsample) /// public unsafe byte[] ReadRegion(int level, long x, long y, long width, long height) { - return BioImage.GetTile(BioImage, App.viewer.GetCoordinate(), level, (int)x, (int)y, (int)width, (int)height).RGBBytes; + return BioImage.GetTile(BioImage, App.viewer.GetCoordinate(), level, (int)x, (int)y, (int)width, (int)height).Bytes; } /// @@ -237,22 +236,13 @@ public unsafe byte[] ReadRegion(int level, long x, long y, long width, long heig /// The height of the region. Must be non-negative. /// The BGRA pixel data of this region. /// - public unsafe bool TryReadRegion(int level, long x, long y, long width, long height, ZCT coord, out byte[] data) + public unsafe bool TryReadRegion(int level, long x, long y, long width, long height, out byte[] data, ZCT zct) { - try - { - data = BioImage.GetTile(BioImage, coord, level, (int)x, (int)y, (int)width, (int)height).RGBBytes; - if (data == null) - return false; - else - return true; - } - catch (Exception e) - { - data = null; + data = BioImage.GetTile(BioImage, zct, level, (int)x, (int)y, (int)width, (int)height).Bytes; + if (data == null) return false; - } - + else + return true; } /// @@ -308,7 +298,7 @@ public async Task ReadRegionAsync(int level, long curLevelOffsetXPixel, try { byte[] bts; - TryReadRegion(level, curLevelOffsetXPixel, curLevelOffsetYPixel, curTileWidth, curTileHeight, coord,out bts); + TryReadRegion(level, curLevelOffsetXPixel, curLevelOffsetYPixel, curTileWidth, curTileHeight,out bts,coord); return bts; } catch (Exception e) diff --git a/Source/Bio/SlideTileLayer.cs b/Source/Bio/SlideTileLayer.cs index 06e8b49..6c72f5c 100644 --- a/Source/Bio/SlideTileLayer.cs +++ b/Source/Bio/SlideTileLayer.cs @@ -30,7 +30,7 @@ public SlideTileLayer( int minExtraTiles = -1, int maxExtraTiles = -1, Func> fetchTileAsFeature = null) - : base(source, minTiles, maxTiles, dataFetchStrategy, renderFetchStrategy, minExtraTiles, maxExtraTiles, fetchTileAsFeature) + : base(source, minTiles, maxTiles, dataFetchStrategy, renderFetchStrategy, minExtraTiles, maxExtraTiles, (Func>)fetchTileAsFeature) { Name = "TileLayer"; _slideSource = source; diff --git a/Source/Bio/Utilities.cs b/Source/Bio/Utilities.cs new file mode 100644 index 0000000..b19ae34 --- /dev/null +++ b/Source/Bio/Utilities.cs @@ -0,0 +1,367 @@ +using BruTile; +using System; +using System.Collections.Generic; +using System.Linq; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System.IO; +using NetVips; +using System.Drawing.Imaging; +using SharpDX.Direct2D1.Effects; +using AForge; +namespace BioImager +{ + public class ImageUtil + { + /// + /// Join by and cut by then scale to (only height an width is useful). + /// + /// tile with tile extent collection + /// canvas extent + /// jpeg output size + /// + public static Image JoinRGB24(IEnumerable> srcPixelTiles, Extent srcPixelExtent, Extent dstPixelExtent) + { + if (srcPixelTiles == null || srcPixelTiles.Count() == 0) + return null; + srcPixelExtent = srcPixelExtent.ToIntegerExtent(); + dstPixelExtent = dstPixelExtent.ToIntegerExtent(); + int canvasWidth = (int)srcPixelExtent.Width; + int canvasHeight = (int)srcPixelExtent.Height; + var dstWidth = (int)dstPixelExtent.Width; + var dstHeight = (int)dstPixelExtent.Height; + Image canvas = new Image(canvasWidth, canvasHeight); + foreach (var tile in srcPixelTiles) + { + try + { + var tileExtent = tile.Item1.ToIntegerExtent(); + var intersect = srcPixelExtent.Intersect(tileExtent); + if (intersect.Width == 0 || intersect.Height == 0) + continue; + if(tile.Item2 == null) + continue; + Image tileRawData = (Image)CreateImageFromBytes(tile.Item2, (int)tileExtent.Width, (int)tileExtent.Height,AForge.PixelFormat.Format24bppRgb); + var tileOffsetPixelX = (int)Math.Ceiling(intersect.MinX - tileExtent.MinX); + var tileOffsetPixelY = (int)Math.Ceiling(intersect.MinY - tileExtent.MinY); + var canvasOffsetPixelX = (int)Math.Ceiling(intersect.MinX - srcPixelExtent.MinX); + var canvasOffsetPixelY = (int)Math.Ceiling(intersect.MinY - srcPixelExtent.MinY); + //We copy the tile region to the canvas. + for (int y = 0; y < intersect.Height; y++) + { + for (int x = 0; x < intersect.Width; x++) + { + int indx = canvasOffsetPixelX + x; + int indy = canvasOffsetPixelY + y; + int tindx = tileOffsetPixelX + x; + int tindy = tileOffsetPixelY + y; + canvas[indx, indy] = tileRawData[tindx, tindy]; + } + } + tileRawData.Dispose(); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + + } + if (dstWidth != canvasWidth || dstHeight != canvasHeight) + { + try + { + canvas.Mutate(x => x.Resize(dstWidth, dstHeight)); + return canvas; + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + } + return canvas; + } + + /// + /// Join by and cut by then scale to (only height an width is useful). + /// + /// tile with tile extent collection + /// canvas extent + /// jpeg output size + /// + public static ImageJoin16(IEnumerable> srcPixelTiles, Extent srcPixelExtent, Extent dstPixelExtent) + { + if (srcPixelTiles == null || srcPixelTiles.Count() == 0) + return null; + srcPixelExtent = srcPixelExtent.ToIntegerExtent(); + dstPixelExtent = dstPixelExtent.ToIntegerExtent(); + int canvasWidth = (int)srcPixelExtent.Width; + int canvasHeight = (int)srcPixelExtent.Height; + var dstWidth = (int)dstPixelExtent.Width; + var dstHeight = (int)dstPixelExtent.Height; + Image canvas = new Image(canvasWidth, canvasHeight); + foreach (var tile in srcPixelTiles) + { + try + { + var tileExtent = tile.Item1.ToIntegerExtent(); + var intersect = srcPixelExtent.Intersect(tileExtent); + if (intersect.Width == 0 || intersect.Height == 0) + continue; + if (tile.Item2 == null) + continue; + Image tileRawData = (Image)CreateImageFromBytes(tile.Item2, (int)tileExtent.Width, (int)tileExtent.Height, AForge.PixelFormat.Format16bppGrayScale); + var tileOffsetPixelX = (int)Math.Ceiling(intersect.MinX - tileExtent.MinX); + var tileOffsetPixelY = (int)Math.Ceiling(intersect.MinY - tileExtent.MinY); + var canvasOffsetPixelX = (int)Math.Ceiling(intersect.MinX - srcPixelExtent.MinX); + var canvasOffsetPixelY = (int)Math.Ceiling(intersect.MinY - srcPixelExtent.MinY); + //We copy the tile region to the canvas. + for (int y = 0; y < intersect.Height; y++) + { + for (int x = 0; x < intersect.Width; x++) + { + int indx = canvasOffsetPixelX + x; + int indy = canvasOffsetPixelY + y; + int tindx = tileOffsetPixelX + x; + int tindy = tileOffsetPixelY + y; + canvas[indx, indy] = tileRawData[tindx, tindy]; + } + } + tileRawData.Dispose(); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + + } + if (dstWidth != canvasWidth || dstHeight != canvasHeight) + { + try + { + canvas.Mutate(x => x.Resize(dstWidth, dstHeight)); + return canvas; + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + } + return canvas; + } + + /// + /// Join by and cut by then scale to (only height an width is useful). + /// + /// tile with tile extent collection + /// canvas extent + /// jpeg output size + /// + public static unsafe NetVips.Image JoinVipsRGB24(IEnumerable> srcPixelTiles, Extent srcPixelExtent, Extent dstPixelExtent) + { + if (srcPixelTiles == null || !srcPixelTiles.Any()) + return null; + + srcPixelExtent = srcPixelExtent.ToIntegerExtent(); + dstPixelExtent = dstPixelExtent.ToIntegerExtent(); + int canvasWidth = (int)srcPixelExtent.Width; + int canvasHeight = (int)srcPixelExtent.Height; + + // Create a base canvas. Adjust as necessary, for example, using a transparent image if needed. + NetVips.Image canvas = NetVips.Image.Black(canvasWidth, canvasHeight, bands: 3); + + foreach (var tile in srcPixelTiles) + { + if (tile.Item2 == null) + continue; + + fixed (byte* pTileData = tile.Item2) + { + var tileExtent = tile.Item1.ToIntegerExtent(); + NetVips.Image tileImage = NetVips.Image.NewFromMemory((IntPtr)pTileData, (ulong)tile.Item2.Length, (int)tileExtent.Width, (int)tileExtent.Height, 3, Enums.BandFormat.Uchar); + + // Calculate positions and sizes for cropping and inserting + var intersect = srcPixelExtent.Intersect(tileExtent); + if (intersect.Width == 0 || intersect.Height == 0) + continue; + + int tileOffsetPixelX = (int)Math.Ceiling(intersect.MinX - tileExtent.MinX); + int tileOffsetPixelY = (int)Math.Ceiling(intersect.MinY - tileExtent.MinY); + int canvasOffsetPixelX = (int)Math.Ceiling(intersect.MinX - srcPixelExtent.MinX); + int canvasOffsetPixelY = (int)Math.Ceiling(intersect.MinY - srcPixelExtent.MinY); + + using (var croppedTile = tileImage.Crop(tileOffsetPixelX, tileOffsetPixelY, (int)intersect.Width, (int)intersect.Height)) + { + // Instead of inserting directly, we composite over the base canvas + canvas = canvas.Composite2(croppedTile, Enums.BlendMode.Over, canvasOffsetPixelX, canvasOffsetPixelY); + } + } + } + + // Resize if the destination extent differs from the source canvas size + if ((int)dstPixelExtent.Width != canvasWidth || (int)dstPixelExtent.Height != canvasHeight) + { + double scaleX = (double)dstPixelExtent.Width / canvasWidth; + double scaleY = (double)dstPixelExtent.Height / canvasHeight; + canvas = canvas.Resize(scaleX, vscale: scaleY, kernel: Enums.Kernel.Nearest); + } + + return canvas; + } + + /// + /// Join by and cut by then scale to (only height an width is useful). + /// + /// tile with tile extent collection + /// canvas extent + /// jpeg output size + /// + public static unsafe NetVips.Image JoinVips16(IEnumerable> srcPixelTiles, Extent srcPixelExtent, Extent dstPixelExtent) + { + if (srcPixelTiles == null || !srcPixelTiles.Any()) + return null; + + srcPixelExtent = srcPixelExtent.ToIntegerExtent(); + dstPixelExtent = dstPixelExtent.ToIntegerExtent(); + int canvasWidth = (int)srcPixelExtent.Width; + int canvasHeight = (int)srcPixelExtent.Height; + + // Create a base canvas. Adjust as necessary, for example, using a transparent image if needed. + AForge.Bitmap bf = new AForge.Bitmap(canvasWidth, canvasHeight, AForge.PixelFormat.Format16bppGrayScale); + NetVips.Image canvas = NetVips.Image.NewFromMemory(bf.Bytes, bf.SizeX, bf.SizeX, 1, Enums.BandFormat.Ushort); + + foreach (var tile in srcPixelTiles) + { + if (tile.Item2 == null) + continue; + + fixed (byte* pTileData = tile.Item2) + { + var tileExtent = tile.Item1.ToIntegerExtent(); + NetVips.Image tileImage = NetVips.Image.NewFromMemory((IntPtr)pTileData, (ulong)tile.Item2.Length, (int)tileExtent.Width, (int)tileExtent.Height, 1, Enums.BandFormat.Ushort); + + // Calculate positions and sizes for cropping and inserting + var intersect = srcPixelExtent.Intersect(tileExtent); + if (intersect.Width == 0 || intersect.Height == 0) + continue; + + int tileOffsetPixelX = (int)Math.Ceiling(intersect.MinX - tileExtent.MinX); + int tileOffsetPixelY = (int)Math.Ceiling(intersect.MinY - tileExtent.MinY); + int canvasOffsetPixelX = (int)Math.Ceiling(intersect.MinX - srcPixelExtent.MinX); + int canvasOffsetPixelY = (int)Math.Ceiling(intersect.MinY - srcPixelExtent.MinY); + + using (var croppedTile = tileImage.Crop(tileOffsetPixelX, tileOffsetPixelY, (int)intersect.Width, (int)intersect.Height)) + { + // Instead of inserting directly, we composite over the base canvas + canvas = canvas.Composite2(croppedTile, Enums.BlendMode.Over, canvasOffsetPixelX, canvasOffsetPixelY); + } + } + } + + // Resize if the destination extent differs from the source canvas size + if ((int)dstPixelExtent.Width != canvasWidth || (int)dstPixelExtent.Height != canvasHeight) + { + double scaleX = (double)dstPixelExtent.Width / canvasWidth; + double scaleY = (double)dstPixelExtent.Height / canvasHeight; + canvas = canvas.Resize(scaleX, vscale: scaleY, kernel: Enums.Kernel.Nearest); + } + + return canvas; + } + + public static SixLabors.ImageSharp.Image CreateImageFromBytes(byte[] rgbBytes, int width, int height, AForge.PixelFormat px) + { + if (px == AForge.PixelFormat.Format24bppRgb) + { + if (rgbBytes.Length != width * height * 3) + { + throw new ArgumentException("Byte array size does not match the dimensions of the image"); + } + + // Create a new image of the specified size + Image image = new Image(width, height); + + // Index for the byte array + int byteIndex = 0; + + // Iterate over the image pixels + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + // Create a color from the next three bytes + Rgb24 color = new Rgb24(rgbBytes[byteIndex], rgbBytes[byteIndex + 1], rgbBytes[byteIndex + 2]); + byteIndex += 3; + // Set the pixel + image[x, y] = color; + } + } + + return image; + } + else + if (px == AForge.PixelFormat.Format16bppGrayScale) + { + if (rgbBytes.Length != width * height * 2) + { + throw new ArgumentException("Byte array size does not match the dimensions of the image"); + } + + // Create a new image of the specified size + Image image = new Image(width, height); + + // Index for the byte array + int byteIndex = 0; + + // Iterate over the image pixels + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + // Create a color from the next three bytes + L16 color = new L16(BitConverter.ToUInt16(rgbBytes, byteIndex)); + byteIndex += 2; + // Set the pixel + image[x, y] = color; + } + } + + return image; + } + else + if (px == AForge.PixelFormat.Format32bppArgb) + { + if (rgbBytes.Length != width * height * 4) + { + throw new ArgumentException("Byte array size does not match the dimensions of the image"); + } + + // Create a new image of the specified size + Image image = new Image(width, height); + + // Index for the byte array + int byteIndex = 0; + + // Iterate over the image pixels + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + // Create a color from the next three bytes + Bgra32 color = new Bgra32(rgbBytes[byteIndex], rgbBytes[byteIndex + 1], rgbBytes[byteIndex + 2], rgbBytes[byteIndex + 3]); + byteIndex += 4; + // Set the pixel + image[x, y] = color; + } + } + + return image; + } + return null; + } + + } + +}