/// Copyright (c) 2021 Iiro Iivanainen, Harri Linna, Jere Pakkanen, Riikka Vilavaara /// /// Permission is hereby granted, free of charge, to any person obtaining /// a copy of this software and associated documentation files (the /// "Software"), to deal in the Software without restriction, including /// without limitation the rights to use, copy, modify, merge, publish, /// distribute, sublicense, and/or sell copies of the Software, and to /// permit persons to whom the Software is furnished to do so, subject to /// the following conditions: /// /// The above copyright notice and this permission notice shall be included /// in all copies or substantial portions of the Software. /// /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, /// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF /// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. /// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY /// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, /// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE /// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. /// using System; using System.Collections.Generic; using System.IO; using System.Linq; using ImageMagick; namespace CoreLibrary { /// /// Exception class for image reading /// public class ImageReadException : Exception { public ImageReadException(string message) : base(message) { } } /// /// Exception class for stack writing /// public class StackWriteException : Exception { public StackWriteException(string message) : base(message) { } } /// /// Exception class for stack deleting /// public class StackDeleteException : Exception { public StackDeleteException(string message) : base(message) { } } /// /// Class for reading and writing 3D-map files. /// public class MapReader { #region image sequnce /// /// Makes a guess what image sequence template name could be from the /// file name. /// /// Filepath of some picture of the image sequence /// stack. /// Guess for the image sequence filename template. /// Thrown when failed to guess template name /// Thrown when failed to do IO operations to the image /// sequence file public static string MakeImgSeqTemplateGuess(string filepath) { if (!File.Exists(filepath)) return ""; FileAttributes attr = File.GetAttributes(filepath); if ((attr & FileAttributes.Directory) == FileAttributes.Directory) return ""; int filetypeI = filepath.LastIndexOf("."); int filepathI = filepath.LastIndexOf("\\"); string filename = filepath.Substring(filepathI + 1, filetypeI - filepathI - 1); char[] digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; string template = filename.TrimEnd(digits); return template; } /// /// Changes filepath to integer according to the ending of filename. /// Works as a natural sort for LINQ OrderBy aslong as the filename /// stays the same. /// /// String of filepath. /// The integer in the end of the file, or 0 if there isnt any. /// Thrown when failed to sort filenames private static int NaturalSort(string filepath) { int filetypeI = filepath.LastIndexOf("."); int filepathI = filepath.LastIndexOf("\\"); string path = filepath.Substring(filepathI + 1, filetypeI - filepathI - 1); if(Int32.TryParse(string.Concat(path.ToArray().Reverse().TakeWhile(char.IsNumber).Reverse()), out int result)) return result; return 1; } /// /// Makes a filelist for image sequence. /// /// Path to the image sequence file or directory /// containing it. /// Template to filter image files. /// List of paths containing image sequence. /// Thrown when failed to make image list private static List MakeImgSeqFileList(string filepath, string template) { try { int filepathI = filepath.LastIndexOf("\\"); string path = filepath.Substring(0, filepathI); string[] files = Directory.GetFiles(path); List filteredFiles = new(); //Filter non-image files. string[] imgExtensions = { "png", "bmp", "tif", "tiff" }; foreach (string file in files) { string filename = file[(file.LastIndexOf("\\") + 1)..]; if (imgExtensions.Any(x => file.EndsWith(x)) && filename.StartsWith(template)) filteredFiles.Add(file); } return filteredFiles.OrderBy(file => NaturalSort(file)).ToList(); } catch(ArgumentException e) { throw new ImageReadException("Failed to sort the files: " + e.Message); } } /// /// Reads a 2D image from image sequence. /// /// Location of the stack files. /// The depth of the 2D slice. /// A MagickImage object from the read file. /// Thrown when failed to read image public static MagickImage ReadImgSeq(string filepath, int depth, string template) { try { if (depth < 0) depth = 0; if (!filepath.Contains("\\")) throw new ImageReadException("Invalid filepath"); int filepathI = filepath.LastIndexOf("\\"); string path = filepath.Substring(0, filepathI); if (!Directory.Exists(path)) throw new ImageReadException("Directory does not exist"); List filteredFiles = MakeImgSeqFileList(filepath, template); if (depth >= filteredFiles.Count) throw new ImageReadException("Image out of bounds"); string loadImg = filteredFiles[depth]; MagickImage image = new(); if (File.Exists(loadImg)) { image.Read(loadImg); image.Density = new Density(96); return image; } throw new ImageReadException("Image does not exist"); } catch (MagickException e) { throw new ImageReadException("Failed to read sequence file: " + e.Message); } catch (ArgumentException e) { throw new ImageReadException("Failed to read sequence file: " + e.Message); } } /// /// Gets the stack size of image sequence. /// /// Path file to the sequence. /// Template for filtering. /// How many slices there are. public static int SequenceSlices(string filepath, string template) { try { if (!filepath.Contains("\\")) return 0; int filepathI = filepath.LastIndexOf("\\"); string path = filepath.Substring(0, filepathI); if (!Directory.Exists(path)) return 0; string[] files = Directory.GetFiles(path); List filteredFiles = new(); //Filter non-image files. string[] imgExtensions = { "png", "bmp", "tif", "tiff" }; foreach (string file in files) { string filename = file[(file.LastIndexOf("\\") + 1)..]; if (imgExtensions.Any(x => file.EndsWith(x)) && filename.StartsWith(template)) filteredFiles.Add(file); } return filteredFiles.Count; } catch (IOException) { return 0; } catch (ArgumentException) { return 0; } catch (UnauthorizedAccessException) { return 0; } } /// /// Makes a new stack from the original stack that is Y-axel oriented /// and writes it to harddrive. /// /// Path to the original stack. /// Template to filter files from the folder. /// Where the program writes the stack. /// Template name for the written image files. /// Thrown when failed to write stack public static void MakeSeqYStack(string filepath, string template, string destinationPath, string yStackName) { try { int filepathI = filepath.LastIndexOf("\\"); string path = filepath.Substring(0, filepathI); List filteredFiles = MakeImgSeqFileList(filepath, template); //Initialize empty destination filepath and y stack name if (destinationPath == "") { destinationPath = path + "\\yStack\\"; if (!Directory.Exists(destinationPath)) Directory.CreateDirectory(destinationPath); } string imgExtension = filteredFiles[0][filteredFiles[0].LastIndexOf(".")..]; if (yStackName == "") { yStackName = filteredFiles[0].Substring(filteredFiles[0].LastIndexOf("\\") + 1, filteredFiles[0].LastIndexOf(".") - filteredFiles[0].LastIndexOf("\\") - 1) + "y"; char[] digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; yStackName = yStackName.TrimEnd(digits); } for (int i = 0; i < filteredFiles.Count; i++) { using MagickImage slice = new(); slice.Read(filteredFiles[i]); for (int j = 0; j < slice.Height; j++) { if (i == 0 && File.Exists(destinationPath + "\\" + yStackName + j + imgExtension)) File.Delete(destinationPath + "\\" + yStackName + j + imgExtension); using MagickImageCollection stack = new(); MagickReadSettings ySettings = new(); ySettings.Width = slice.Width; ySettings.Height = 1; MagickImage ySlice = new("xc:black", ySettings); ySlice.Format = slice.Format; ySlice.Grayscale(); ySlice.CopyPixels(slice, new MagickGeometry(0, j, slice.Width, 1)); if (File.Exists(destinationPath + "\\" + yStackName + j + imgExtension)) stack.Add(destinationPath + "\\" + yStackName + j + imgExtension); stack.Add(ySlice); stack.AppendVertically().Write(destinationPath + "\\" + yStackName + j + imgExtension); } } } catch (IOException e) { throw new StackWriteException("Failed to write image sequence files, problems with IO operations: " + e.Message); } catch (MagickException e) { throw new StackWriteException("Failed to write image sequence files. Problems with Magick.NET: " + e.Message); } catch (ArgumentException e) { throw new StackWriteException("Failed to write image sequence files: " + e.Message); } } /// /// Makes a new stack from the original stack that is X-axel oriented and /// writes it to harddrive. /// /// Path to the original stack. /// Template to filter files from the folder. /// Where the program writes the stack. /// Template name for the written image files. /// Thrown when failed to write stack public static void MakeSeqXStack(string filepath, string template, string destinationPath, string xStackName) { try { int filepathI = filepath.LastIndexOf("\\"); string path = filepath.Substring(0, filepathI); List filteredFiles = MakeImgSeqFileList(filepath, template); //Initialize empty destination filepath and x stack name. if (destinationPath == "") { destinationPath = path + "\\xStack\\"; if (!Directory.Exists(destinationPath)) Directory.CreateDirectory(destinationPath); } string imgExtension = filteredFiles[0][filteredFiles[0].LastIndexOf(".")..]; if (xStackName == "") { xStackName = filteredFiles[0].Substring(filteredFiles[0].LastIndexOf("\\") + 1, filteredFiles[0].LastIndexOf(".") - filteredFiles[0].LastIndexOf("\\") - 1) + "x"; char[] digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; xStackName = xStackName.TrimEnd(digits); } for (int i = 0; i < filteredFiles.Count; i++) { using MagickImage slice = new(); slice.Read(filteredFiles[i]); for (int j = 0; j < slice.Width; j++) { if (i == 0 && File.Exists(destinationPath + "\\" + xStackName + j + imgExtension)) File.Delete(destinationPath + "\\" + xStackName + j + imgExtension); using MagickImageCollection stack = new(); MagickReadSettings xSettings = new(); xSettings.Width = 1; xSettings.Height = slice.Height; MagickImage xSlice = new("xc:black", xSettings); xSlice.Format = slice.Format; xSlice.Grayscale(); xSlice.CopyPixels(slice, new MagickGeometry(j, 0, 1, slice.Height)); if (File.Exists(destinationPath + "\\" + xStackName + j + imgExtension)) stack.Add(destinationPath + "\\" + xStackName + j + imgExtension); stack.Add(xSlice); stack.AppendHorizontally().Write(destinationPath + "\\" + xStackName + j + imgExtension); } } } catch (IOException e) { throw new StackWriteException("Failed to write image sequence files, problems with IO operations: " + e.Message); } catch (MagickException e) { throw new StackWriteException("Failed to write image sequence files. Problems with Magick.NET: " + e.Message); } catch (ArgumentException e) { throw new StackWriteException("Failed to write image sequence files: " + e.Message); } } #endregion #region 3D-tif /// /// Reads a 2D image from 3D tif file. /// /// Filepath of the image. /// The depth of the 2D slice. /// A MagickImage object from the read file. /// Thrown when failed to read image public static MagickImage Read3Dtif(string filepath, int depth) { if (!File.Exists(filepath)) throw new ImageReadException("Image does not exist"); try { MagickImageCollection images = new(); MagickImageCollection meta = new(); meta.Ping(filepath); if (depth < meta.Count && depth >= 0) { MagickReadSettings settings = new(); settings.FrameIndex = depth; settings.FrameCount = 1; images.Read(filepath, settings); MagickImage image = (MagickImage)images[0]; image.Density = new Density(96); return image; } else throw new ImageReadException("Image out of bounds"); } catch(MagickException e) { throw new ImageReadException("Error reading image: " + e.Message); } } /// /// Gets the stack size of 3D tif stack. /// /// Location of the file. /// Size of the 3D tif stack. public static int ThreeDTifSlices(string filepath) { try { if (!File.Exists(filepath)) return 0; MagickImageCollection images = new(); images.Ping(filepath); return images.Count; } catch (MagickException) { return 0; } } #endregion #region raw /// /// Turns one color channel raw data to image magick object. /// /// Location of the raw data file. /// What depth we take the slice. /// Width of the picture. /// Height of the picture<./param> /// How many bytes represents one pixel. /// Is the raw data in little endian form. /// MagickImage object in png format containing the picture /// from the raw data. /// Thrown when failed to read image public static MagickImage ReadRaw(string filepath, int depth, int width, int height, int byteDepth, bool littleEndian) { try { if (!File.Exists(filepath)) throw new ImageReadException("File does not exist"); if (depth > RawSlices(filepath, width, height, byteDepth)) throw new ImageReadException("Tried to read image out of bounds"); if (width < 1 || height < 1) throw new ImageReadException("Tried to read image out of bounds"); if (depth < 0) depth = 0; MagickImage image = new(); var pixelDepth = byteDepth switch { 2 => StorageType.Short, 4 => StorageType.Float, 8 => StorageType.Double, _ => StorageType.Char, }; FileStream fs = new(filepath, FileMode.Open, FileAccess.Read); int sliceSize = byteDepth * width * height; long offSet = (long)sliceSize * (long)depth; byte[] bytes = new byte[sliceSize]; fs.Seek(offSet, SeekOrigin.Begin); fs.Read(bytes, 0, sliceSize); fs.Close(); if (!littleEndian) { for (int i = 0; i < bytes.Length; i += byteDepth) { byte[] temp = new byte[byteDepth]; for (int j = 0; j < temp.Length; j++) { temp[temp.Length - j - 1] = bytes[i + j]; } for (int j = 0; j < temp.Length; j++) { bytes[i + j] = temp[j]; } } } PixelReadSettings pixelSettings = new(width, height, pixelDepth, "R"); image.ReadPixels(bytes, pixelSettings); image.Format = MagickFormat.Tif; image.Density = new Density(96); return image; } catch (IOException e) { throw new ImageReadException("Failed to read raw file, problems with IO operations: " + e.Message); } catch (MagickException e) { throw new ImageReadException("Failed to read raw file, Magick.NET exception: " + e.Message); } catch (ArgumentException e) { throw new ImageReadException("Failed to read raw file: " + e.Message); } } /// /// Gets the stack size of raw data image stack. /// /// Location of the file. /// Width of one image in the stack. /// Heigth of one image in the stack. /// How many bytes represents one pixel /// Size of the raw stack. public static int RawSlices(string filepath, int width, int height, int byteDepth) { try { if (!File.Exists(filepath)) return 0; long slices = new System.IO.FileInfo(filepath).Length / (width * height * byteDepth); return (int)slices; } catch (IOException) { return 0; } } /// /// Makes a new stack from the original raw stack that is Y-axel /// oriented and writes it to harddrive. /// /// Path to original stack. /// Path to new stack, if "" will /// be put in a new folder in the same filepath as the original stack. /// Name of the new stack file, if "" will /// be the same name as original + y. /// Heigth of the raw file. /// Width of the raw file. /// Depth of the stack. /// How many bytes represents 1 pixel. /// Thrown when failed to write stack public static void MakeRawYStack(string filepath, string destinationPath, string yStackName, int heigth, int width, int z, int byteDepth) { try { int filepathI = filepath.LastIndexOf("\\"); string path = filepath.Substring(0, filepathI); //Initialize empty destination filepath and x stack name. if (destinationPath == "") { destinationPath = path + "\\yStack\\"; if (!Directory.Exists(destinationPath)) Directory.CreateDirectory(destinationPath); } if (yStackName == "") { yStackName = filepath.Substring(filepath.LastIndexOf("\\") + 1, filepath.LastIndexOf(".") - filepath.LastIndexOf("\\") - 1) + "y" + "raw"; } int row = width * byteDepth; long sliceSizeDest = (long)width * (long)z * (long)byteDepth; long sliceSizeOrig = (long)heigth * (long)row; if (File.Exists(destinationPath + "\\" + yStackName)) { File.Delete(destinationPath + "\\" + yStackName); } using (FileStream fs = new(destinationPath + "\\" + yStackName, FileMode.CreateNew)) { try { fs.Seek(sliceSizeOrig * (long)z - 1, SeekOrigin.Begin); fs.WriteByte(0); } catch (IOException) { throw; } finally { fs.Close(); } } using FileStream rs = File.OpenRead(filepath); using FileStream ws = new(destinationPath + "\\" + yStackName, FileMode.Open); try { ws.Position = 0; rs.Position = 0; byte[] bytes = new byte[row]; for (int i = 0; i < z; i++) { for (int j = 0; j < heigth; j++) { ws.Position = sliceSizeDest * (long)j + (long)i * (long)row; rs.Read(bytes, 0, row); ws.Write(bytes, 0, row); } } } catch (IOException) { throw; } catch (ArgumentException) { throw; } finally { ws.Close(); rs.Close(); } } catch (IOException e) { throw new StackWriteException("Failed to write raw stack to file, problems with IO operations: " + e.Message); } catch (ArgumentException e) { throw new StackWriteException("Failed to write raw stack to file, bad parameters: " + e.Message); } } /// /// Makes a new stack from the original raw stack that is Y-axel oriented /// and writes it to harddrive. /// /// Path to original stack. /// Path to new stack, if "" will be put /// in a new folder in the same filepath as the original stack. /// Name of the new stack file, if "" will be /// the same name as original + y. /// Heigth of the raw file. /// Width of the raw file. /// Depth of the stack. /// How many bytes represents 1 pixel. /// Thrown when failed to write stack public static void MakeRawXStack(string filepath, string destinationPath, string xStackName, int heigth, int width, int z, int byteDepth) { try { int filepathI = filepath.LastIndexOf("\\"); string path = filepath.Substring(0, filepathI); //Initialize empty destination filepath and x stack name. if (destinationPath == "") { destinationPath = path + "\\xStack\\"; if (!Directory.Exists(destinationPath)) Directory.CreateDirectory(destinationPath); } if (xStackName == "") { xStackName = filepath.Substring(filepath.LastIndexOf("\\") + 1, filepath.LastIndexOf(".") - filepath.LastIndexOf("\\") - 1) + "x" + ".raw"; } int rowDest = z * byteDepth; long sliceSizeDest = (long)heigth * (long)z * (long)byteDepth; long sliceSizeOrig = (long)heigth * (long)width * (long)byteDepth; if (File.Exists(destinationPath + "\\" + xStackName)) { File.Delete(destinationPath + "\\" + xStackName); } using (FileStream fs = new(destinationPath + "\\" + xStackName, FileMode.CreateNew)) { try { fs.Seek(sliceSizeOrig * (long)z - 1, SeekOrigin.Begin); fs.WriteByte(0); } catch(IOException) { throw; } finally { fs.Close(); } } using FileStream rs = File.OpenRead(filepath); using FileStream ws = new(destinationPath + "\\" + xStackName, FileMode.Open); try { ws.Position = 0; rs.Position = 0; byte[] bytes = new byte[byteDepth]; for (int i = 0; i < z; i++) { for (int j = 0; j < heigth; j++) { for (int k = 0; k < width; k++) { ws.Position = ((long)width - (long)k - 1) * sliceSizeDest + (long)j * (long)rowDest + i * (long)byteDepth; rs.Read(bytes, 0, byteDepth); ws.Write(bytes, 0, byteDepth); } } } } catch(IOException) { throw; } catch(ArgumentException) { throw; } finally { ws.Close(); rs.Close(); } } catch (IOException e) { throw new StackWriteException("Failed to write raw stack to file, problems wit IO operations: " + e.Message); } catch(ArgumentException e) { throw new StackWriteException("Failed to write raw stack to file, bad parameters: " + e.Message); } } #endregion /// /// Delete written stacks from harddrive. /// /// Filepath to the deleted file. /// Template for stack written in multiple files. /// Is the stack in multiple files. /// Thrown when failed to delete stack public static void DeleteStack(string filepath, string template, bool multifile) { try { if (multifile) { List filteredFiles = MakeImgSeqFileList(filepath, template); foreach (string file in filteredFiles) { File.Delete(file); } } else if (File.Exists(filepath)) File.Delete(filepath); } catch (IOException e) { throw new StackDeleteException("Failed to delete files. Problems with IO operations: " + e.Message); } catch (ArgumentException e) { throw new StackDeleteException("Failed to delete files: " + e.Message); } } } }