Repository: microsoft/InnerEye-DICOM-RT (language: python)

Issue:
Inferenced segmentation.dcm cannot be imported to Eclipse
### Discussed in https://github.com/microsoft/InnerEye-DeepLearning/discussions/687

<div type='discussions-op-text'>

<sup>Originally posted by **furtheraway** March  5, 2022</sup>
Hi all,

I am walking through the model taining and inference with the example lung segamentation dataset https://github.com/microsoft/InnerEye-DeepLearning/blob/main/docs/sample_tasks.md

The model training has succeed on Azure ML.

Then I zip up the DICOM files of the second test subject into a zip file:
C:\Users\xxxx\Downloads\IE_lung_dataset\manifest-1557326747206\LCTSC\LCTSC-Test-S1-102\11-04-2003-NA-RTRCCTTHORAX8FHigh Adult-20444\0.000000-CT114545RespCT  3.0  B30f  50 Ex-81163

`zip -r patS1102.zip 0.000000-CT114545RespCT\ \ 3.0\ \ B30f\ \ 50\ Ex-81163/`

Then submit it with submit_for_inference.py:

```
python InnerEye/Scripts/submit_for_inference.py --image_file /home/xxxx/datasets/inference_dicom/patS1102.zip --model_id LungExample:1 --download_folder /home/xxxx/source/2022/download/ --use_dicom true

```

The inference run finished successfully and I got the segmentation.dcm file. But when I tried to import this segmentation.dcm file to Eclipse, I run into the following error:

![image](https://user-images.githubusercontent.com/7104230/156857012-e7adb451-7660-45c4-89ab-92f93f6cfe17.png)

I can import the original DICOM files for this subject without any issue, 

@ant0nsc 
</div>

[AB#5453](https://innereye.visualstudio.com/60ce1777-00d6-4015-82bc-488a0c00202f/_workitems/edit/5453)


Discussion / hints:
@peterhessey FYI
Additional details from previous e-mail chain:

"_It looks like the problem is near the end of the file._

_The dumps show the data twice, first as hex, then as ASCII.  Each pair of lines is prefixed with the byte offset._

_After the end of the file, the original file has extra 00 bytes, as shown in the snippets below. These
were cut and pasted from the hex dump files and had the byte offset corrected._

_If you want to test it, you can use DICOM+ to read it in and see if it shows any errors._

```console
Original file:
1232651  65  74  65  72  5e  fe  ff  0d  e0  00  00  00  00  fe  ff  dd  e0  00  00  00  00  **00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00**
          e   t   e   r   ^   ~ del  cr   ` nul nul nul nul   ~ del   ]   ` nul nul nul nul **nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul**

Fixed file:
1232677  65  74  65  72  5e  fe  ff  0d  e0  00  00  00  00  fe  ff  dd  e0  00  00  00  00
          e   t   e   r   ^   ~ del  cr   ` nul nul nul nul   ~ del   ]   ` nul nul nul nul
```
"

It was recommended that the data between the pairs of asterisks (**) should be removed (asterisks added manually).

Adding @furtheraway for visibility, who had originally posted the question

Fix patch:
diff --git a/RTConvert/Microsoft.RTConvert.Converters.Tests/RTEncodingTests.cs b/RTConvert/Microsoft.RTConvert.Converters.Tests/RTEncodingTests.cs
index 97de9b8..37e36ff 100644
--- a/RTConvert/Microsoft.RTConvert.Converters.Tests/RTEncodingTests.cs
+++ b/RTConvert/Microsoft.RTConvert.Converters.Tests/RTEncodingTests.cs
@@ -52,15 +52,13 @@ public void RtStructOutputEncoder_SuccessWithValidInputs()
 
             var outputEncoder = new DicomRTStructOutputEncoder();
 
-            var outputStructureBytes = outputEncoder.EncodeStructures(
+            var dcm = outputEncoder.EncodeStructures(
                 volumesWithMetadata,
                 new Dictionary<string, MedicalVolume>() { { "", referenceVolume } },
                 "modelX:1",
                 "manufacturer",
                 "interpreter");
 
-            var dcm = DicomFile.Open(new MemoryStream(outputStructureBytes.Array));
-
             // Check the output format (should be RT struct)
             Assert.AreEqual(DicomUID.RTStructureSetStorage, dcm.FileMetaInfo.MediaStorageSOPClassUID);
 
diff --git a/RTConvert/Microsoft.RTConvert.Converters/DicomRTStructOutputEncoder.cs b/RTConvert/Microsoft.RTConvert.Converters/DicomRTStructOutputEncoder.cs
index 7f49ed7..fe01f1f 100644
--- a/RTConvert/Microsoft.RTConvert.Converters/DicomRTStructOutputEncoder.cs
+++ b/RTConvert/Microsoft.RTConvert.Converters/DicomRTStructOutputEncoder.cs
@@ -23,7 +23,7 @@ public class DicomRTStructOutputEncoder : ISegmentationOutputEncoder
 
         public SegmentationOutputEncoding OutputEncoding => SegmentationOutputEncoding.RTStruct;
 
-        public ArraySegment<byte> EncodeStructures(
+        public DicomFile EncodeStructures(
             IEnumerable<(string name, Volume3D<byte> volume, RGBColor color, bool fillHoles, ROIInterpretedType roiInterpretedType)> outputStructuresWithMetadata,
             IReadOnlyDictionary<string, MedicalVolume> inputChannels,
             string modelNameAndVersion,
@@ -40,21 +40,7 @@ public ArraySegment<byte> EncodeStructures(
                 manufacturer,
                 interpreter);
 
-            return SerializeDicomFile(structureSetFile);
-        }
-
-        private static ArraySegment<byte> SerializeDicomFile(DicomFile structureSetFile)
-        {
-            using (var memoryStream = new MemoryStream())
-            {
-                structureSetFile.Save(memoryStream);
-
-                if (!memoryStream.TryGetBuffer(out ArraySegment<byte> output))
-                {
-                    throw new InvalidOperationException("Could not extract Array-Segment instance from Memory-Stream");
-                }
-                return output;
-            }
+            return structureSetFile;
         }
 
         private static DicomFile CreateStructureSetFile(
diff --git a/RTConvert/Microsoft.RTConvert.Converters/ISegmentationOutputEncoder.cs b/RTConvert/Microsoft.RTConvert.Converters/ISegmentationOutputEncoder.cs
index 8ff3afe..9e1e604 100644
--- a/RTConvert/Microsoft.RTConvert.Converters/ISegmentationOutputEncoder.cs
+++ b/RTConvert/Microsoft.RTConvert.Converters/ISegmentationOutputEncoder.cs
@@ -3,6 +3,7 @@
 
 namespace Microsoft.RTConvert.Converters
 {
+    using Dicom;
     using Microsoft.RTConvert.MedIO.Models;
     using Microsoft.RTConvert.MedIO.RT;
     using Microsoft.RTConvert.Models;
@@ -40,7 +41,7 @@ public interface ISegmentationOutputEncoder
         SegmentationOutputEncoding OutputEncoding { get; }
 
         /// <summary>
-        /// Encodes the segmentation output in a binary format. 
+        /// Encodes the segmentation output into a DicomFile object. 
         /// </summary>
         /// <param name="outputStructures">The output structures from eh machine learning</param>
         /// <param name="inputChannels">The input channels that were used to generate the structures</param>
@@ -48,7 +49,7 @@ public interface ISegmentationOutputEncoder
         /// <param name="manufacturer">The creator of the model.</param>
         /// <param name="interpreter">The interpreter of the model.</param>
         /// <returns></returns>
-        ArraySegment<byte> EncodeStructures(
+        DicomFile EncodeStructures(
               IEnumerable<(string name, Volume3D<byte> volume, RGBColor color, bool fillHoles, ROIInterpretedType roiInterpretedType)> outputStructures,
               IReadOnlyDictionary<string, MedicalVolume> inputChannels,
               string modelAndVersion,
diff --git a/RTConvert/Microsoft.RTConvert.Converters/RTConverters.cs b/RTConvert/Microsoft.RTConvert.Converters/RTConverters.cs
index 5912adf..0b81261 100644
--- a/RTConvert/Microsoft.RTConvert.Converters/RTConverters.cs
+++ b/RTConvert/Microsoft.RTConvert.Converters/RTConverters.cs
@@ -275,14 +275,14 @@ public static void ConvertNiftiToDicom(
 
             var outputEncoder = new DicomRTStructOutputEncoder();
 
-            var outputStructureBytes = outputEncoder.EncodeStructures(
+            var outputDicomFile = outputEncoder.EncodeStructures(
                 volumesWithMetadata,
                 new Dictionary<string, MedicalVolume>() { { "", referenceVolume } },
                 modelNameAndVersion,
                 manufacturer,
                 interpreter);
 
-            File.WriteAllBytes(dcmFilename, outputStructureBytes.Array);
+            outputDicomFile.Save(dcmFilename);
         }
     }
 }


Test patch:
diff --git a/tests/test_nifti_to_dicom_rt_converter.py b/tests/test_nifti_to_dicom_rt_converter.py
index e48ed18..ad42779 100644
--- a/tests/test_nifti_to_dicom_rt_converter.py
+++ b/tests/test_nifti_to_dicom_rt_converter.py
@@ -1,179 +1,179 @@
-#  ------------------------------------------------------------------------------------------
-#  Copyright (c) Microsoft Corporation. All rights reserved.
-#  Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
-#  ------------------------------------------------------------------------------------------
-
-"""
-Tests for nifti_to_dicom_rt_converter.
-"""
-
-import logging
-from pathlib import Path
-from typing import List
-from pydicom import dcmread
-
-try:
-    from InnerEye_DICOM_RT.nifti_to_dicom_rt_converter import echo, get_version, rtconvert  # type: ignore
-except ImportError:
-    logging.info("using local src")
-    from src.InnerEye_DICOM_RT.nifti_to_dicom_rt_converter import echo, get_version, rtconvert  # type: ignore
-
-logger = logging.getLogger('test_rtconvert')
-logger.setLevel(logging.DEBUG)
-
-
-def test_get_version() -> None:
-    """
-    Test that .dotnet core can be called --info and that it is
-    running version 2.1.
-    """
-    (stdout, stderr) = get_version()
-
-    logger.debug("stdout: %s", stdout)
-    logger.debug("stderr: %s", stderr)
-
-    assert stderr == ''
-    assert 'Microsoft.NETCore.App 2.1.' in stdout
-
-
-def test_echo() -> None:
-    """
-    Test that the test Echo dll can be called and returns the test string and no error.
-    """
-    test_string = "hello world2!"
-    (stdout, stderr) = echo(test_string)
-
-    logger.debug("stdout: %s", stdout)
-    logger.debug("stderr: %s", stderr)
-
-    assert stderr == ''
-    assert stdout == test_string + '\n'
-
-
-def test_echo_err() -> None:
-    """
-    Test that the test Echo dll can be called and returns the test and error strings.
-    """
-    test_string = "hello world2!"
-    test_error = "Test error."
-    (stdout, stderr) = echo(test_string, test_error)
-
-    logger.debug("stdout: %s", stdout)
-    logger.debug("stderr: %s", stderr)
-
-    assert stderr == test_error + '\n'
-    assert stdout == test_string + '\n'
-
-
-# The directory containing this file.
-THIS_DIR: Path = Path(__file__).parent.resolve()
-# The TestData directory.
-TEST_DATA_DIR: Path = THIS_DIR / "TestData"
-# Test Nifti file.
-TestNiftiSegmentationLocation: Path = TEST_DATA_DIR / "hnsegmentation.nii.gz"
-# Test reference series.
-TestDicomVolumeLocation: Path = TEST_DATA_DIR / "HN"
-# Target test output file.
-TestOutputFile: Path = THIS_DIR / "test.dcm"
-
-# Test fill holes.
-FillHoles: List[bool] = [
-        True, True, True, True,
-        False, False, True, True,
-        True, True, False, True,
-        True, True, True, False,
-        True, False, True, True,
-        False, True
-]
-
-# Test ROIInterpretedType.
-ROIInterpretedTypes: List[str] = [
-        "ORGAN", "None", "CTV", "EXTERNAL",
-        "ORGAN", "None", "CTV", "EXTERNAL",
-        "ORGAN", "None", "CTV", "EXTERNAL",
-        "ORGAN", "None", "CTV", "EXTERNAL",
-        "ORGAN", "None", "CTV", "EXTERNAL",
-        "ORGAN", "None"
-]
-
-# Test structure colors.
-StructureColors: List[str] = [
-        "FF0001", "FF0002", "FF0003", "FF0004",
-        "FF0101", "FF0102", "FF0103", "FF0103",
-        "FF0201", "FF02FF", "FF0203", "FF0204",
-        "FF0301", "FF0302", "01FF03", "FF0304",
-        "FF0401", "00FFFF", "FF0403", "FF0404",
-        "FF0501", "FF0502"
-]
-
-# Test structure names.
-StructureNames: List[str] = [
-        "External", "parotid_l", "parotid_r", "smg_l",
-        "smg_r", "spinal_cord", "brainstem", "globe_l",
-        "Globe_r", "mandible", "spc_muscle", "mpc_muscle",
-        "Cochlea_l", "cochlea_r", "lens_l", "lens_r",
-        "optic_chiasm", "optic_nerve_l", "optic_nerve_r", "pituitary_gland",
-        "lacrimal_gland_l", "lacrimal_gland_r"
-]
-
-Manufacturer = "Contosos"
-Interpreter = "Ai"
-ModelId = "XYZ:12"
-
-def test_rtconvert() -> None:
-    """
-    Test calling RTConvert for the test data.
-    """
-    (stdout, stderr) = rtconvert(
-        in_file=TestNiftiSegmentationLocation,
-        reference_series=TestDicomVolumeLocation,
-        out_file=TestOutputFile,
-        struct_names=StructureNames,
-        struct_colors=StructureColors,
-        fill_holes=FillHoles,
-        roi_interpreted_types=ROIInterpretedTypes,
-        manufacturer=Manufacturer,
-        interpreter=Interpreter,
-        modelId=ModelId
-    )
-
-    logger.debug("stdout: %s", stdout)
-    logger.debug("stderr: %s", stderr)
-
-    assert stderr == ''
-    assert "Successfully written" in stdout
-
-    assert TestOutputFile.is_file()
-
-    with open(TestOutputFile, 'rb') as infile:
-        ds = dcmread(infile)
-
-        assert ds is not None
-
-        # Check the modality
-        assert ds.Modality == 'RTSTRUCT'
-
-        assert ds.Manufacturer == Manufacturer
-        assert ds.SoftwareVersions == ModelId
-
-        assert len(ds.StructureSetROISequence) == len(StructureNames)
-
-        for i, item in enumerate(StructureNames):
-            assert ds.StructureSetROISequence[i].ROINumber == i + 1
-            assert ds.StructureSetROISequence[i].ROIName == item
-            assert Interpreter in ds.RTROIObservationsSequence[i].ROIInterpreter
-            assert ds.RTROIObservationsSequence[i].RTROIInterpretedType == ('' if ROIInterpretedTypes[i] == 'None' else ROIInterpretedTypes[i])
-
-        assert len(ds.ROIContourSequence) == len(StructureNames)
-
-        for i, item in enumerate(StructureNames):
-            assert ds.ROIContourSequence[i].ReferencedROINumber == i + 1
-            assert ds.ROIContourSequence[i].ROIDisplayColor == _parse_rgb(StructureColors[i])
-
-def _parse_rgb(rgb: str) -> List[int]:
-    """
-    Convert the string representation of RGB color to an int list
-    :param rgb: Color string
-    :return: List of [R, G, B] components.
-    """
-    return [int(rgb[i:i+2], 16) for i in (0, 2, 4)]
+#  ------------------------------------------------------------------------------------------
+#  Copyright (c) Microsoft Corporation. All rights reserved.
+#  Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
+#  ------------------------------------------------------------------------------------------
+
+"""
+Tests for nifti_to_dicom_rt_converter.
+"""
+
+import logging
+from pathlib import Path
+from typing import List
+from pydicom import dcmread
+
+try:
+    from InnerEye_DICOM_RT.nifti_to_dicom_rt_converter import echo, get_version, rtconvert  # type: ignore
+except ImportError:
+    logging.info("using local src")
+    from src.InnerEye_DICOM_RT.nifti_to_dicom_rt_converter import echo, get_version, rtconvert  # type: ignore
+
+logger = logging.getLogger('test_rtconvert')
+logger.setLevel(logging.DEBUG)
+
+
+def test_get_version() -> None:
+    """
+    Test that .dotnet core can be called --info and that it is
+    running version 2.1.
+    """
+    (stdout, stderr) = get_version()
+
+    logger.debug("stdout: %s", stdout)
+    logger.debug("stderr: %s", stderr)
+
+    assert stderr == ''
+    assert 'Microsoft.NETCore.App 2.1.' in stdout
+
+
+def test_echo() -> None:
+    """
+    Test that the test Echo dll can be called and returns the test string and no error.
+    """
+    test_string = "hello world2!"
+    (stdout, stderr) = echo(test_string)
+
+    logger.debug("stdout: %s", stdout)
+    logger.debug("stderr: %s", stderr)
+
+    assert stderr == ''
+    assert stdout == test_string + '\n'
+
+
+def test_echo_err() -> None:
+    """
+    Test that the test Echo dll can be called and returns the test and error strings.
+    """
+    test_string = "hello world2!"
+    test_error = "Test error."
+    (stdout, stderr) = echo(test_string, test_error)
+
+    logger.debug("stdout: %s", stdout)
+    logger.debug("stderr: %s", stderr)
+
+    assert stderr == test_error + '\n'
+    assert stdout == test_string + '\n'
+
+
+# The directory containing this file.
+THIS_DIR: Path = Path(__file__).parent.resolve()
+# The TestData directory.
+TEST_DATA_DIR: Path = THIS_DIR / "TestData"
+# Test Nifti file.
+TestNiftiSegmentationLocation: Path = TEST_DATA_DIR / "hnsegmentation.nii.gz"
+# Test reference series.
+TestDicomVolumeLocation: Path = TEST_DATA_DIR / "HN"
+# Target test output file.
+TestOutputFile: Path = THIS_DIR / "test.dcm"
+
+# Test fill holes.
+FillHoles: List[bool] = [
+    True, True, True, True,
+    False, False, True, True,
+    True, True, False, True,
+    True, True, True, False,
+    True, False, True, True,
+    False, True
+]
+
+# Test ROIInterpretedType.
+ROIInterpretedTypes: List[str] = [
+    "ORGAN", "None", "CTV", "EXTERNAL",
+    "ORGAN", "None", "CTV", "EXTERNAL",
+    "ORGAN", "None", "CTV", "EXTERNAL",
+    "ORGAN", "None", "CTV", "EXTERNAL",
+    "ORGAN", "None", "CTV", "EXTERNAL",
+    "ORGAN", "None"
+]
+
+# Test structure colors.
+StructureColors: List[str] = [
+    "FF0001", "FF0002", "FF0003", "FF0004",
+    "FF0101", "FF0102", "FF0103", "FF0103",
+    "FF0201", "FF02FF", "FF0203", "FF0204",
+    "FF0301", "FF0302", "01FF03", "FF0304",
+    "FF0401", "00FFFF", "FF0403", "FF0404",
+    "FF0501", "FF0502"
+]
+
+# Test structure names.
+StructureNames: List[str] = [
+    "External", "parotid_l", "parotid_r", "smg_l",
+    "smg_r", "spinal_cord", "brainstem", "globe_l",
+    "Globe_r", "mandible", "spc_muscle", "mpc_muscle",
+    "Cochlea_l", "cochlea_r", "lens_l", "lens_r",
+    "optic_chiasm", "optic_nerve_l", "optic_nerve_r", "pituitary_gland",
+    "lacrimal_gland_l", "lacrimal_gland_r"
+]
+
+Manufacturer = "Contosos"
+Interpreter = "Ai"
+ModelId = "XYZ:12"
+
+def test_rtconvert() -> None:
+    """
+    Test calling RTConvert for the test data.
+    """
+    (stdout, stderr) = rtconvert(
+        in_file=TestNiftiSegmentationLocation,
+        reference_series=TestDicomVolumeLocation,
+        out_file=TestOutputFile,
+        struct_names=StructureNames,
+        struct_colors=StructureColors,
+        fill_holes=FillHoles,
+        roi_interpreted_types=ROIInterpretedTypes,
+        manufacturer=Manufacturer,
+        interpreter=Interpreter,
+        modelId=ModelId
+    )
+
+    logger.debug("stdout: %s", stdout)
+    logger.debug("stderr: %s", stderr)
+
+    assert stderr == ''
+    assert "Successfully written" in stdout
+
+    assert TestOutputFile.is_file()
+
+    with open(TestOutputFile, 'rb') as infile:
+        ds = dcmread(infile)
+
+        assert ds is not None
+
+        # Check the modality
+        assert ds.Modality == 'RTSTRUCT'
+
+        assert ds.Manufacturer == Manufacturer
+        assert ds.SoftwareVersions == ModelId
+
+        assert len(ds.StructureSetROISequence) == len(StructureNames)
+
+        for i, item in enumerate(StructureNames):
+            assert ds.StructureSetROISequence[i].ROINumber == i + 1
+            assert ds.StructureSetROISequence[i].ROIName == item
+            assert Interpreter in ds.RTROIObservationsSequence[i].ROIInterpreter
+            assert ds.RTROIObservationsSequence[i].RTROIInterpretedType == ('' if ROIInterpretedTypes[i] == 'None' else ROIInterpretedTypes[i])
+
+        assert len(ds.ROIContourSequence) == len(StructureNames)
+
+        for i, item in enumerate(StructureNames):
+            assert ds.ROIContourSequence[i].ReferencedROINumber == i + 1
+            assert ds.ROIContourSequence[i].ROIDisplayColor == _parse_rgb(StructureColors[i])
+
+def _parse_rgb(rgb: str) -> List[int]:
+    """
+    Convert the string representation of RGB color to an int list
+    :param rgb: Color string
+    :return: List of [R, G, B] components.
+    """
+    return [int(rgb[i:i+2], 16) for i in (0, 2, 4)]