static void Main(string[] args) { try { if (args.Length < 2) { Console.WriteLine("Usage: DicomSeriesReader <input_directory> <output_file>"); return; } Console.WriteLine("Reading Dicom directory: " + args[0]); ImageSeriesReader reader = new ImageSeriesReader(); VectorString dicom_names = ImageSeriesReader.GetGDCMSeriesFileNames(args[0]); reader.SetFileNames(dicom_names); Image image = reader.Execute(); VectorUInt32 size = image.GetSize(); Console.WriteLine("Image size: " + size[0] + " " + size[1] + " " + size[2]); Console.WriteLine("Writing image: " + args[1]); ImageFileWriter writer = new ImageFileWriter(); writer.SetFileName(args[1]); writer.Execute(image); if (Environment.GetEnvironmentVariable("SITK_NOSHOW") == null) { SimpleITK.Show(image, "Dicom Series"); } } catch (Exception ex) { Console.WriteLine("Usage: DicomSeriesReader <input_directory> <output_file>"); Console.WriteLine(ex); } }
/*! Load the entire series (i.e. the entire volume). * Unlike loadImageData(), this function does not create a colors array or texture. To * access volume data, simply get the image (DICOM.image) and read pixel values from it. */ private void loadVolumeData() { // Get all file names for the series: VectorString fileNames = seriesInfo.filenames; // Create a reader which will read the whole series: ImageSeriesReader reader = new ImageSeriesReader(); reader.SetFileNames(fileNames); // Load the entire image into a series: Image image = reader.Execute(); // Make the loaded image accessable from elsewhere: this.image = image; }
/// <inheritdoc/> public ImageReaderBase CreateImageReader(string path) { if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentNullException(nameof(path)); } var fs = fileSystemStrategy.Create(path); if (fs == null) { throw new UnexpectedNullException("Filesystem could not be created based on the provided path."); } if (fs.IsDirectory(path)) { var entryPath = DiscoverEntryPath(fs.Directory.GetFiles(path)); if (string.IsNullOrWhiteSpace(entryPath)) { // By default try to read DICOM image series // https://simpleitk.readthedocs.io/en/master/Examples/DicomSeriesReader/Documentation.html ImageSeriesReader seriesReader = new ImageSeriesReader(); seriesReader.SetFileNames(ImageSeriesReader.GetGDCMSeriesFileNames(path)); return(seriesReader); } else { // Try to read the image based on the entry path ImageFileReader reader = new ImageFileReader(); reader.SetFileName(entryPath); return(reader); } } else { ImageFileReader reader = new ImageFileReader(); reader.SetFileName(path); return(reader); } }
/*! Searches the directoryToLoad for DICOMs. Called in thread!*/ public void parseDirectory(object sender, DoWorkEventArgs e) { // Parse the directory and return the seriesUIDs of all the DICOM series: try { availableSeries.Clear(); Debug.Log("[DICOM] Searching directory: " + directoryToLoad); VectorString series = ImageSeriesReader.GetGDCMSeriesIDs(directoryToLoad); if (series.Count > 0) { foreach (string s in series) { DICOMSeries info = new DICOMSeries(directoryToLoad, s); availableSeries.Add(info); } } // Debug log: string str = "[DICOM] Found " + series.Count + " series."; Debug.Log(str); } catch (System.Exception err) { Debug.LogError("Error while trying to parse DICOM directory: " + err.Message); } }
/*! Load the entire series (i.e. the entire volume). * Unlike loadImageData(), this function does not create a colors array or texture. To * access volume data, simply get the image (DICOM.image) and read pixel values from it. */ private void loadVolumeData() { // Get all file names for the series: VectorString fileNames = seriesInfo.filenames; // Create a reader which will read the whole series: ImageSeriesReader reader = new ImageSeriesReader(); reader.SetFileNames(fileNames); // Load the entire image into a series: Image image = reader.Execute(); origTexWidth = (int)image.GetWidth(); origTexHeight = (int)image.GetHeight(); origTexDepth = (int)image.GetDepth(); texWidth = Mathf.NextPowerOfTwo((int)image.GetWidth()); texHeight = Mathf.NextPowerOfTwo((int)image.GetHeight()); texDepth = Mathf.NextPowerOfTwo((int)image.GetDepth()); texPaddingWidth = texWidth - origTexWidth; texPaddingHeight = texHeight - origTexHeight; texPaddingDepth = texDepth - origTexDepth; Debug.Log("Original texture dimensions: " + origTexWidth + " " + origTexHeight + " " + origTexDepth); Debug.Log("Texture dimensions: " + texWidth + " " + texHeight + " " + texDepth); colors = new Color32[texWidth * texHeight * texDepth]; int intercept = 0; int slope = 1; try { intercept = Int32.Parse(image.GetMetaData("0028|1052")); slope = Int32.Parse(image.GetMetaData("0028|1053")); } catch { } Debug.Log("Slope: " + slope + " Intercept: " + intercept); if (image.GetDimension() != 3) { throw(new System.Exception("Cannot load volume: Image needs to be 3D. Dimensions of image: " + image.GetDimension())); } histogram = new Histogram(); UInt32 min = UInt32.MaxValue; UInt32 max = UInt32.MinValue; Debug.Log("Pixel format: " + image.GetPixelID()); // Copy the image into a colors array: if (image.GetPixelID() == PixelIDValueEnum.sitkUInt16) { IntPtr bufferPtr = image.GetBufferAsUInt16(); unsafe { UInt16 *ptr = (UInt16 *)bufferPtr.ToPointer(); int consecutiveIndex = 0; for (UInt32 z = 0; z < texDepth; z++) { for (UInt32 y = 0; y < texHeight; y++) { for (UInt32 x = 0; x < texWidth; x++) { if (x < origTexWidth && y < origTexHeight && z < origTexDepth) { long jumpingIndex = x + y * texWidth + z * texWidth * texHeight; UInt32 pixelValue = (UInt32)((UInt16)ptr [consecutiveIndex]); colors [jumpingIndex] = F2C(pixelValue); if (pixelValue > max) { max = pixelValue; } if (pixelValue < min) { min = pixelValue; } histogram.addValue(pixelValue); consecutiveIndex++; } } } } } } else if (image.GetPixelID() == PixelIDValueEnum.sitkInt16) { IntPtr bufferPtr = image.GetBufferAsInt16(); unsafe { Int16 *ptr = (Int16 *)bufferPtr.ToPointer(); int consecutiveIndex = 0; for (UInt32 z = 0; z < texDepth; z++) { for (UInt32 y = 0; y < texHeight; y++) { for (UInt32 x = 0; x < texWidth; x++) { if (x < origTexWidth && y < origTexHeight && z < origTexDepth) { long jumpingIndex = x + y * texWidth + z * texWidth * texHeight; UInt32 pixelValue = (UInt32)((Int16)ptr[consecutiveIndex] + Int16.MaxValue); colors [jumpingIndex] = F2C(pixelValue); if (pixelValue > max) { max = pixelValue; } if (pixelValue < min) { min = pixelValue; } histogram.addValue(pixelValue); consecutiveIndex++; } } } } } } else if (image.GetPixelID() == PixelIDValueEnum.sitkInt32) { IntPtr bufferPtr = image.GetBufferAsInt32(); unsafe { Int32 *ptr = (Int32 *)bufferPtr.ToPointer(); int consecutiveIndex = 0; for (UInt32 z = 0; z < texDepth; z++) { for (UInt32 y = 0; y < texHeight; y++) { for (UInt32 x = 0; x < texWidth; x++) { if (x < origTexWidth && y < origTexHeight && z < origTexDepth) { long jumpingIndex = x + y * texWidth + z * texWidth * texHeight; // TODO: To move from Int32 to UInt32 range, we should add Int32.MaxValue?! // However, when we do this, UInt32 pixelValue = (UInt32)((Int32)ptr[consecutiveIndex]) + (UInt32)Int16.MaxValue; colors [jumpingIndex] = F2C(pixelValue); if (pixelValue > max) { max = pixelValue; } if (pixelValue < min) { min = pixelValue; } histogram.addValue(pixelValue); consecutiveIndex++; } } } } } } else { throw(new System.Exception("Unsupported pixel format: " + image.GetPixelID())); } /*IntPtr bufferPtr; * UInt32 numberOfPixels = image.GetWidth () * image.GetHeight () * image.GetDepth(); * if (image.GetPixelID () == PixelIDValueEnum.sitkUInt16) { * bufferPtr = image.GetBufferAsUInt16 (); * * UInt16[] colorsTmp = new UInt16[ numberOfPixels ]; * Int16[] tmp = new Int16[ numberOfPixels ]; * Marshal.Copy( bufferPtr, tmp, 0, (int)numberOfPixels ); * System.Buffer.BlockCopy (tmp, 0, colorsTmp, 0, (int)numberOfPixels); * * int index = 0; * //for (UInt32 z = 0; z < texDepth; z++) { * for (UInt32 z = 0; z < texDepth; z++) { * for (UInt32 y = 0; y < texHeight; y++) { * for (UInt32 x = 0; x < texWidth; x++) { * //long consecutiveIndex = (texWidth-1-x) + y*texWidth + z*texWidth*texHeight; * long consecutiveIndex = x + y * texWidth + z*texWidth*texHeight; * if (x < origTexWidth && y < origTexHeight && z < origTexDepth) { * UInt16 pixelValue = (UInt16)((colorsTmp [index] - intercept) / slope); * colors [consecutiveIndex] = F2C (pixelValue); * * if (pixelValue > max) * max = pixelValue; * if (pixelValue < min) * min = pixelValue; * * histogram.addValue (pixelValue); * * index++; * } * } * } * } * } else if ( image.GetPixelID() == PixelIDValueEnum.sitkInt16 ) { * bufferPtr = image.GetBufferAsInt16 (); * * Int16[] colorsTmp = new Int16[ numberOfPixels ]; * Marshal.Copy( bufferPtr, colorsTmp, 0, (int)numberOfPixels ); * * int index = 0; * //for (UInt32 z = 0; z < texDepth; z++) { * for (UInt32 z = 0; z < texDepth; z++) { * for (UInt32 y = 0; y < texHeight; y++) { * for (UInt32 x = 0; x < texWidth; x++) { * //long consecutiveIndex = (texWidth-1-x) + y*texWidth + z*texWidth*texHeight; * long consecutiveIndex = x + y * texWidth + z*texWidth*texHeight; * if (x < origTexWidth && y < origTexHeight && z < origTexDepth ) * { * //Int16 pixelValueInt16 = (Int16)((colorsTmp [index] - intercept) / slope); * //UInt32 pixelValue = (UInt32)((int)pixelValueInt16 + 32768); * * UInt16 pixelValue = (UInt16)((colorsTmp [index] - intercept) / slope); * colors [ consecutiveIndex] = F2C(pixelValue); * * if (pixelValue > max) * max = pixelValue; * if (pixelValue < min) * min = pixelValue; * * histogram.addValue (pixelValue); * * index++; * } * } * } * } * } else if ( image.GetPixelID() == PixelIDValueEnum.sitkInt32 ) { * bufferPtr = image.GetBufferAsInt32 (); * * Int32[] colorsTmp = new Int32[ numberOfPixels ]; * Marshal.Copy( bufferPtr, colorsTmp, 0, (int)numberOfPixels ); * * int index = 0; * //for (UInt32 z = 0; z < texDepth; z++) { * for (UInt32 z = 0; z < texDepth; z++) { * for (UInt32 y = 0; y < texHeight; y++) { * for (UInt32 x = 0; x < texWidth; x++) { * //long consecutiveIndex = (texWidth-1-x) + y*texWidth + z*texWidth*texHeight; * long consecutiveIndex = x + y * texWidth + z*texWidth*texHeight; * if (x < origTexWidth && y < origTexHeight && z < origTexDepth) { * Int32 pixelValueInt32 = (Int32)((colorsTmp [index] - intercept) / slope); * * UInt32 pixelValue = (UInt32)((Int64)pixelValueInt32 + Int32.MaxValue); * colors [ consecutiveIndex ] = F2C(pixelValue); * * if (pixelValue > max) * max = pixelValue; * if (pixelValue < min) * min = pixelValue; * * histogram.addValue (pixelValue); * * index++; * } * } * } * } * } else { * throw(new System.Exception ("Unsupported pixel format: " + image.GetPixelID())); * }*/ // Manually set the min and max values, because we just caculated them for the whole volume and // can thus be sure that we have the correct values: seriesInfo.setMinMaxPixelValues(min, max); histogram.setMinMaxPixelValues(min, max); // Make the loaded image accessable from elsewhere: this.image = image; Debug.Log("Loaded."); }
/*! Constructor, fills most of the attributes of the DICOMSeries class. * \note This does some heavy file/directory parsing to determine the files which are part of * this series and their order. This is why the DICOMSeries should be constructed in a * background thread and then passed to the main thread. */ public DICOMSeries(string directory, string seriesUID) { // Get the file names for the series: filenames = ImageSeriesReader.GetGDCMSeriesFileNames(directory, seriesUID); if (filenames.Count <= 0) { throw(new System.Exception("No files found for series " + seriesUID + ".")); } this.seriesUID = seriesUID; // Load the first slice in volume to get meta information: firstSlice = SimpleITK.ReadImage(filenames[0]); VectorDouble o1 = firstSlice.GetOrigin(); if (o1.Count < 3) { throw(new System.Exception("Invalid origins found in first image.")); } origin = new Vector3((float)o1 [0], (float)o1 [1], (float)o1 [2]); numberOfSlices = filenames.Count; // Offset between two adjacent slices. If only one slice is present, // this defaults to zero. Vector3 sliceOffset = Vector3.zero; // If we have more than one slice, also load the last slice to be able to determine the slice spacing: if (filenames.Count > 1) { lastSlice = SimpleITK.ReadImage(filenames[filenames.Count - 1]); // Get the origins of the two images: VectorDouble o2 = lastSlice.GetOrigin(); if (o2.Count < 3) { throw(new System.Exception("Invalid origins found in last image.")); } Vector3 lastOrigin = new Vector3((float)o2 [0], (float)o2 [1], (float)o2 [2]); // Calculate offset between two adjacent slices (assuming all neighbours are the same distance apart): // Note: I expect sliceOffset.x and sliceOffset.y to be zero most of the time. // Using a Vector just for completeness. sliceOffset = (lastOrigin - origin) / (filenames.Count - 1); } // Load the direction cosines: // ITK stores the direction cosines in a matrix with row-major-ordering. The weird indexing is because // we need the first and second column (0,3,6 for X and 1,4,7 for Y) VectorDouble direction = firstSlice.GetDirection(); if (direction.Count < 6) { throw(new System.Exception("Invalid direction cosines found in images.")); } directionCosineX = new Vector3((float)direction [0], (float)direction [3], (float)direction [6]); directionCosineY = new Vector3((float)direction [1], (float)direction [4], (float)direction [7]); sliceNormal = Vector3.Cross(directionCosineX, directionCosineY); // Calculate the which direction the normal is facing to determine the orienation (Transverse, // Coronal or Saggital). float absX = Mathf.Abs(sliceNormal.x); float absY = Mathf.Abs(sliceNormal.y); float absZ = Mathf.Abs(sliceNormal.z); if (absX > absY && absX > absZ) { sliceOrientation = SliceOrientation.Saggital; } else if (absY > absX && absY > absZ) { sliceOrientation = SliceOrientation.Coronal; } else if (absZ > absX && absZ > absY) { sliceOrientation = SliceOrientation.Transverse; } else { sliceOrientation = SliceOrientation.Unknown; } // Load the direction cosines: // NOTE: It seems that the the first value is the spacing between rows (i.e. y direction), // the second value is the spacing between columns (i.e. x direction). // I was not able to verify this so far, since all test dicoms we had have the same spacing in // x and y direction... VectorDouble spacing = firstSlice.GetSpacing(); if (spacing.Count < 2) { throw(new System.Exception("Invalid pixel spacing found in images.")); } pixelSpacing = new Vector2((float)spacing [1], (float)spacing [0]); // Set up the transformation matrix: Matrix4x4 transformMatrix = new Matrix4x4(); // Column 1: transformMatrix [0, 0] = directionCosineX.x * pixelSpacing.x; transformMatrix [1, 0] = directionCosineX.y * pixelSpacing.x; transformMatrix [2, 0] = directionCosineX.z * pixelSpacing.x; transformMatrix [3, 0] = 0f; // Column 2: transformMatrix [0, 1] = directionCosineY.x * pixelSpacing.y; transformMatrix [1, 1] = directionCosineY.y * pixelSpacing.y; transformMatrix [2, 1] = directionCosineY.z * pixelSpacing.y; transformMatrix [3, 1] = 0f; // Column 3: transformMatrix [0, 2] = sliceOffset.x; transformMatrix [1, 2] = sliceOffset.y; transformMatrix [2, 2] = sliceOffset.z; transformMatrix [3, 2] = 0f; // Column 4: transformMatrix [0, 3] = origin.x; transformMatrix [1, 3] = origin.y; transformMatrix [2, 3] = origin.z; transformMatrix [3, 3] = 1f; // Convert to a the left-hand-side coordinate system which Unity uses: Matrix4x4 rightHandToLeftHand = new Matrix4x4(); rightHandToLeftHand [0, 0] = 1f; rightHandToLeftHand [1, 1] = 1f; rightHandToLeftHand [2, 2] = -1f; rightHandToLeftHand [3, 3] = 1f; pixelToPatient = rightHandToLeftHand * transformMatrix; // Inverse transformation: patientToPixel = pixelToPatient.inverse; // Read the minimum and maximum values which are stored in this image: minPixelValue = UInt16.MinValue; maxPixelValue = UInt16.MaxValue; foundMinMaxPixelValues = false; try { minPixelValue = Int32.Parse(firstSlice.GetMetaData("0028|0106")); maxPixelValue = Int32.Parse(firstSlice.GetMetaData("0028|0107")); foundMinMaxPixelValues = true; } catch { } }
/*! Constructor, fills most of the attributes of the DICOMSeries class. * \note This does some heavy file/directory parsing to determine the files which are part of * this series and their order. This is why the DICOMSeries should be constructed in a * background thread and then passed to the main thread. */ public DICOMSeries(string directory, string seriesUID) { Debug.Log("Loading Meta Data for Series: " + seriesUID); // Get the file names for the series: filenames = ImageSeriesReader.GetGDCMSeriesFileNames(directory, seriesUID); if (filenames.Count <= 0) { throw(new System.Exception("No files found for series " + seriesUID + ".")); } this.seriesUID = seriesUID; // Load the first slice in volume to get meta information: firstSlice = SimpleITK.ReadImage(filenames[0]); lastSlice = SimpleITK.ReadImage(filenames[Math.Max(filenames.Count - 1, 0)]); numberOfSlices = filenames.Count; // Load the direction cosines: // ITK stores the direction cosines in a matrix with row-major-ordering. The weird indexing is because // we need the first and second column (0,3,6 for X and 1,4,7 for Y) VectorDouble direction = firstSlice.GetDirection(); if (direction.Count < 6) { throw(new System.Exception("Invalid direction cosines found in images.")); } directionCosineX = new Vector3((float)direction [0], (float)direction [3], (float)direction [6]); directionCosineY = new Vector3((float)direction [1], (float)direction [4], (float)direction [7]); sliceNormal = Vector3.Cross(directionCosineX, directionCosineY); if (lastSlice != null) { // Get the origins of the two images: VectorDouble o1 = firstSlice.GetOrigin(); if (o1.Count < 3) { throw(new System.Exception("Invalid origins found in first image.")); } Vector3 origin = new Vector3((float)o1 [0], (float)o1 [1], (float)o1 [2]); VectorDouble o2 = lastSlice.GetOrigin(); if (o2.Count < 3) { throw(new System.Exception("Invalid origins found in last image.")); } Vector3 lastOrigin = new Vector3((float)o2 [0], (float)o2 [1], (float)o2 [2]); // Calculate offset between two adjacent slices (assuming all neighbours are the same distance apart): // Note: I expect sliceOffset.x and sliceOffset.y to be zero most of the time. // Using a Vector just for completeness. sliceOffset = (lastOrigin - origin) / (numberOfSlices - 1); } if (lastSlice != null && numberOfSlices > 1) { // Load the direction cosines: // ITK stores the direction cosines in a matrix with row-major-ordering. The weird indexing is because // we need the first and second column (0,3,6 for X and 1,4,7 for Y) VectorDouble directionLast = lastSlice.GetDirection(); if (directionLast.Count < 6) { throw(new System.Exception("Invalid direction cosines found in images.")); } Vector3 directionCosineXLast = new Vector3((float)directionLast [0], (float)directionLast [3], (float)directionLast [6]); Vector3 directionCosineYLast = new Vector3((float)directionLast [1], (float)directionLast [4], (float)directionLast [7]); Vector3 sliceNormalLast = Vector3.Cross(directionCosineXLast, directionCosineYLast); // If the first and last slice have the same orientation, then consider this series to be a volume. // TODO: Better check? if ((sliceNormal == sliceNormalLast)) { isConsecutiveVolume = true; } else { Debug.LogWarning("First and last slice of the series do not have the same orientation. This will not be considered a volume.\n" + "\tNormal first slice, Normal last slice: " + sliceNormal + " " + sliceNormalLast); } } else { isConsecutiveVolume = false; } if (isConsecutiveVolume) { // Calculate which direction the normal is facing to determine the orienation (Transverse, // Coronal or Saggital). float absX = Mathf.Abs(sliceNormal.x); float absY = Mathf.Abs(sliceNormal.y); float absZ = Mathf.Abs(sliceNormal.z); if (absX > absY && absX > absZ) { sliceOrientation = SliceOrientation.Saggital; } else if (absY > absX && absY > absZ) { sliceOrientation = SliceOrientation.Coronal; } else if (absZ > absX && absZ > absY) { sliceOrientation = SliceOrientation.Transverse; } else { sliceOrientation = SliceOrientation.Unknown; } } else { sliceOrientation = SliceOrientation.Unknown; // Can't know what the orientation is if the first and last slice have different normals } // Read the minimum and maximum values which are stored in this image: minPixelValue = UInt16.MinValue; maxPixelValue = UInt16.MaxValue; foundMinMaxPixelValues = false; try { minPixelValue = UInt32.Parse(firstSlice.GetMetaData("0028|0106")); maxPixelValue = UInt32.Parse(firstSlice.GetMetaData("0028|0107")); foundMinMaxPixelValues = true; } catch { } }