/// 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);
}
}
}
}