Change Segmentation timestamp from UTC to local time (#15)

* Change timestamp from UTC to local time

* 🚨 Fix flake8 warnings

* 📝 Add comments and documentation
This commit is contained in:
Peter Hessey 2022-10-12 15:58:58 +01:00 коммит произвёл GitHub
Родитель 6453c03a05
Коммит e7f56ecec8
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 209 добавлений и 201 удалений

Просмотреть файл

@ -1,32 +1,34 @@
# Introduction
# Introduction
InnerEye-DICOM-RT contains tools to convert medical datasets in NIFTI format to DICOM-RT. Datasets converted using
InnerEye-DICOM-RT contains tools to convert medical datasets in NIFTI format to DICOM-RT. Datasets converted using
this tool can be consumed directly by [InnerEye-DeepLearning](https://github.com/microsoft/InnerEye-DeepLearning).
Most of the work is done by a .NET Core 2.1 project in RTConvert, written in C#. There is a very lightweight wrapper
around this so that it can be consumed from Python. The wrapper relies on the PyPI package https://pypi.org/project/dotnetcore2/ which wraps up .NET Core 2.1.
# Installing
## Installing
## Git for Windows
### Git for Windows
Get the installer from [Git for Windows](https://git-scm.com/download/win)
The installer will prompt you to "Select Components". Make sure that you tick
The installer will prompt you to "Select Components". Make sure that you tick
* Git LFS (Large File Support)
* Git Credential Manager for Windows
After the installation, open a command prompt or the Git Bash:
- Run `git lfs install` to set up the hooks in git
- Run `git config --global core.autocrlf true` to ensure that line endings are working as expected
* Run `git lfs install` to set up the hooks in git
* Run `git config --global core.autocrlf true` to ensure that line endings are working as expected
Clone the InnerEye-DICOM-RT repository on your machine: Run `git lfs clone --recursive https://github.com/microsoft/InnerEye-DICOM-RT`
## Visual Studio / .NET Core
### Visual Studio / .NET Core
The C# components can be built with the .NET Core SDK. We use version 2.1 for compatibility with the PyPI package `dotnetcore2`.
Installation instructions are here: https://docs.microsoft.com/en-us/dotnet/core/install/.
Visual Studio is not required to build, but if you wish to use it then for .Net Core 2.1 you need at least:
Installation instructions are here: https://docs.microsoft.com/en-us/dotnet/core/install/.
Visual Studio is not required to build, but if you wish to use it then for .Net Core 2.1 you need at least:
[Visual Studio 2017 version 15.7](https://visualstudio.microsoft.com/vs/?utm_medium=microsoft&utm_source=docs.microsoft.com&utm_campaign=inline+link).
### RTConvert
@ -55,12 +57,12 @@ Finally, for consumption by the Python wrapper, this solution must be published:
dotnet publish RTConvert --configuration Release -p:Platform=x64
```
This should create a folder with all the requirements for RTConvert at:
This should create a folder with all the requirements for RTConvert at:
`RTConvert/Microsoft.RTConvert.Console/bin/x64/Release/netcoreapp2.1/publish/*`
### Echo
Echo is a very simple application that takes 1 or 2 arguments. The first is echoed to `stdout`, and if a
Echo is a very simple application that takes 1 or 2 arguments. The first is echoed to `stdout`, and if a
second argument is supplied then it is echoed to `stderr`. This is only required for units tests to establish
that a .NET Core application can be called.
@ -95,6 +97,7 @@ The Python package is created with:
```bash
python setup.py sdist bdist_wheel
```
which builds a source distribution and wheel to the `dist` folder.
To run the Python tests:
@ -132,6 +135,7 @@ To call RTConvert:
```
where:
* `in_file` is the path to the input Nifti file. This file is a 3D volume in [Nifti format](https://nifti.nimh.nih.gov/).
* `reference_series` is the path to the input folder containing the reference DICOM series;
* `out_file` is the path to the output DICOM-RT file;
@ -153,6 +157,12 @@ where:
* `interpreter` Interpreter for the DICOM-RT (check DICOM-RT documentation)
* `roi_interpreted_types` is a list of ROIInterpretedType. Possible values (None, CTV, ORGAN, EXTERNAL).
### Segmentation Timestamps
In the process of converting from NIFTI masks to DICOM, InnerEye-DICOM-RT adds a timestamp tag to the series. This timestamp will be set to the ***local time of the compute resource that `rtconvert()` is run on***. This is as some DICOM readers do not handle timezones / UTC offsets from these tags and will result in erroneous times being displayed. Therefore, if your compute resource is running in a different timezone to the timezone you are viewing the DICOM files in, the timestamps may not be what you are expecting.
## License
[MIT License](LICENSE)
**You are responsible for the performance, the necessary testing, and if needed any regulatory clearance for
@ -162,7 +172,7 @@ where:
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
the rights to use your contribution. For details, visit the [Micrsoft CLA page](https://cla.opensource.microsoft.com).
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
@ -174,8 +184,8 @@ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additio
## Trademarks
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
trademarks or logos is subject to and must follow
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
trademarks or logos is subject to and must follow
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
Any use of third-party trademarks or logos are subject to those third-party's policies.

Просмотреть файл

@ -55,7 +55,9 @@ namespace Microsoft.RTConvert.MedIO.RT
ds.Add(DicomTag.SeriesNumber, string.Empty);
// Type 3 tags - optional but useful
var now = DateTime.UtcNow;
// Using DateTime.Now as some DICOM readers do not handle UTC offests. Local time is best guess.
var now = DateTime.Now;
var date = now.ToString("yyyyMMdd", CultureInfo.InvariantCulture);
var time = now.ToString("HHmmss", CultureInfo.InvariantCulture);
ds.Add(DicomTag.SeriesDate, date);
@ -63,4 +65,4 @@ namespace Microsoft.RTConvert.MedIO.RT
}
}
}
}

Просмотреть файл

@ -1,5 +1,5 @@
dotnetcore2==2.1.23
flake8==3.8.4
flake8==5.0.4
mypy==0.800
pydicom==2.1.2
pytest-cov==2.11.1

Просмотреть файл

@ -1,183 +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)]