dicomwf 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dicomwf-0.3.0/LICENSE.md +7 -0
- dicomwf-0.3.0/PKG-INFO +156 -0
- dicomwf-0.3.0/README.md +124 -0
- dicomwf-0.3.0/pyproject.toml +55 -0
- dicomwf-0.3.0/setup.cfg +4 -0
- dicomwf-0.3.0/src/dicomwf/__init__.py +20 -0
- dicomwf-0.3.0/src/dicomwf/data.py +116 -0
- dicomwf-0.3.0/src/dicomwf/routinescalp.py +233 -0
- dicomwf-0.3.0/src/dicomwf.egg-info/PKG-INFO +156 -0
- dicomwf-0.3.0/src/dicomwf.egg-info/SOURCES.txt +13 -0
- dicomwf-0.3.0/src/dicomwf.egg-info/dependency_links.txt +1 -0
- dicomwf-0.3.0/src/dicomwf.egg-info/requires.txt +8 -0
- dicomwf-0.3.0/src/dicomwf.egg-info/top_level.txt +1 -0
- dicomwf-0.3.0/tests/test_routinescalp.py +78 -0
- dicomwf-0.3.0/tests/test_routinescalp_unit.py +154 -0
dicomwf-0.3.0/LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Inria
|
|
2
|
+
|
|
3
|
+
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:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
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.
|
dicomwf-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dicomwf
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A Python library for creating DICOM EEG waveform files
|
|
5
|
+
Author: Clément Chesnin
|
|
6
|
+
Author-email: Evgenia Kartsaki <evgenia.kartsaki@inria.fr>, Thomas Prampart <thomas.prampart@inria.fr>, Michael Kain <michael.kain@inria.fr>, Eric Poiseau <eric.poiseau@inria.fr>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://gitlab.inria.fr/openvibe/dicomwf
|
|
9
|
+
Project-URL: Repository, https://gitlab.inria.fr/openvibe/dicomwf
|
|
10
|
+
Keywords: dicom,eeg,waveform,medical,imaging
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: ==3.11.*
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE.md
|
|
24
|
+
Requires-Dist: numpy>=1.20.0
|
|
25
|
+
Requires-Dist: pydicom>=2.3.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: mne>=1.11.0; extra == "dev"
|
|
29
|
+
Requires-Dist: build>=1.4.0; extra == "dev"
|
|
30
|
+
Requires-Dist: twine>=6.0.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# dicomwf
|
|
34
|
+
|
|
35
|
+
A Python library for integrating waveforms in DICOM files.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
Supported waveforms IODs:
|
|
40
|
+
- RoutineScalpEEG
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
### User Installation
|
|
45
|
+
|
|
46
|
+
Install directly from PyPI:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install dicomwf
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Developer Installation
|
|
53
|
+
|
|
54
|
+
1. Clone the repository:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://gitlab.inria.fr/openvibe/dicomwf.git
|
|
58
|
+
cd dicomwf
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
2. Create and activate a virtual environment:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m venv .venv
|
|
65
|
+
|
|
66
|
+
# On Windows
|
|
67
|
+
.venv\Scripts\activate
|
|
68
|
+
|
|
69
|
+
# On Unix/macOS
|
|
70
|
+
source .venv/bin/activate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
3. Install in editable mode with development dependencies:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install -e ".[dev]"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This installs the package in editable mode along with development tools.
|
|
80
|
+
|
|
81
|
+
## Quick Start
|
|
82
|
+
|
|
83
|
+
We will use the EEG data in the edf format from the [EEGBCI dataset](https://doi.org/10.1109/TBME.2004.827072).
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import numpy as np
|
|
87
|
+
from dicomwf import *
|
|
88
|
+
from urllib.request import urlretrieve
|
|
89
|
+
import mne
|
|
90
|
+
|
|
91
|
+
# Load EEG data
|
|
92
|
+
url = "https://physionet.org/files/eegmmidb/1.0.0/S001/S001R01.edf?download"
|
|
93
|
+
raw_fname = "S001R01.edf"
|
|
94
|
+
urlretrieve(url, raw_fname)
|
|
95
|
+
|
|
96
|
+
raw = mne.io.read_raw_edf(raw_fname, preload=True)
|
|
97
|
+
|
|
98
|
+
# Create DICOM EEG
|
|
99
|
+
dicom_eeg_data = RoutineScalpEEGData(
|
|
100
|
+
patient=Patient(name="Doe^John", birth_date="20250101", sex="M"),
|
|
101
|
+
study=GeneralStudy(id="1",date="19940423", time="101601.934000", accession_number="1"),
|
|
102
|
+
equipment=GeneralEquipment(
|
|
103
|
+
manufacturer="PyDICOM",
|
|
104
|
+
manufacturer_model_name="PyDICOM",
|
|
105
|
+
device_serial_number="1",
|
|
106
|
+
software_versions="1.0",
|
|
107
|
+
),
|
|
108
|
+
waveform_identification=WaveformIdentification(),
|
|
109
|
+
waveform=Waveform(
|
|
110
|
+
channel_count=raw.info["nchan"],
|
|
111
|
+
sampling_frequency=raw.info["sfreq"],
|
|
112
|
+
channel_names=raw.ch_names,
|
|
113
|
+
data=np.transpose(raw.get_data()),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
dicom_eeg = RoutineScalpEEG(uid_root="1.2.3.4.5.", rse_data=dicom_eeg_data)
|
|
118
|
+
dicom_eeg.save_as("output.dcm")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Dependencies
|
|
122
|
+
|
|
123
|
+
**Required:**
|
|
124
|
+
- numpy >= 1.20.0
|
|
125
|
+
- pydicom >= 2.3.0
|
|
126
|
+
|
|
127
|
+
**Optional (for development):**
|
|
128
|
+
- pytest >= 7.0.0
|
|
129
|
+
- mne>=1.11.0
|
|
130
|
+
|
|
131
|
+
## Running Tests
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pytest
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
|
|
140
|
+
|
|
141
|
+
## Authors
|
|
142
|
+
|
|
143
|
+
- Evgenia Kartsaki
|
|
144
|
+
- Thomas Prampart
|
|
145
|
+
- Michael Kain
|
|
146
|
+
- Eric Poiseau
|
|
147
|
+
- Clement Chesnin
|
|
148
|
+
|
|
149
|
+
## Data Sources
|
|
150
|
+
This project uses the following data for unit testing:
|
|
151
|
+
* **[EEG Motor Movement/Imagery Dataset]**: Sourced from [https://physionet.org/content/eegmmidb/1.0.0/S001/].
|
|
152
|
+
* **License**: This data is licensed under the [Open Data Commons Attribution License (ODC-By) v1.0](https://physionet.org/content/eegmmidb/view-license/1.0.0/).
|
|
153
|
+
|
|
154
|
+
## Contributing
|
|
155
|
+
|
|
156
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
dicomwf-0.3.0/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# dicomwf
|
|
2
|
+
|
|
3
|
+
A Python library for integrating waveforms in DICOM files.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
Supported waveforms IODs:
|
|
8
|
+
- RoutineScalpEEG
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
### User Installation
|
|
13
|
+
|
|
14
|
+
Install directly from PyPI:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install dicomwf
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Developer Installation
|
|
21
|
+
|
|
22
|
+
1. Clone the repository:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://gitlab.inria.fr/openvibe/dicomwf.git
|
|
26
|
+
cd dicomwf
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2. Create and activate a virtual environment:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python -m venv .venv
|
|
33
|
+
|
|
34
|
+
# On Windows
|
|
35
|
+
.venv\Scripts\activate
|
|
36
|
+
|
|
37
|
+
# On Unix/macOS
|
|
38
|
+
source .venv/bin/activate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
3. Install in editable mode with development dependencies:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install -e ".[dev]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This installs the package in editable mode along with development tools.
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
We will use the EEG data in the edf format from the [EEGBCI dataset](https://doi.org/10.1109/TBME.2004.827072).
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import numpy as np
|
|
55
|
+
from dicomwf import *
|
|
56
|
+
from urllib.request import urlretrieve
|
|
57
|
+
import mne
|
|
58
|
+
|
|
59
|
+
# Load EEG data
|
|
60
|
+
url = "https://physionet.org/files/eegmmidb/1.0.0/S001/S001R01.edf?download"
|
|
61
|
+
raw_fname = "S001R01.edf"
|
|
62
|
+
urlretrieve(url, raw_fname)
|
|
63
|
+
|
|
64
|
+
raw = mne.io.read_raw_edf(raw_fname, preload=True)
|
|
65
|
+
|
|
66
|
+
# Create DICOM EEG
|
|
67
|
+
dicom_eeg_data = RoutineScalpEEGData(
|
|
68
|
+
patient=Patient(name="Doe^John", birth_date="20250101", sex="M"),
|
|
69
|
+
study=GeneralStudy(id="1",date="19940423", time="101601.934000", accession_number="1"),
|
|
70
|
+
equipment=GeneralEquipment(
|
|
71
|
+
manufacturer="PyDICOM",
|
|
72
|
+
manufacturer_model_name="PyDICOM",
|
|
73
|
+
device_serial_number="1",
|
|
74
|
+
software_versions="1.0",
|
|
75
|
+
),
|
|
76
|
+
waveform_identification=WaveformIdentification(),
|
|
77
|
+
waveform=Waveform(
|
|
78
|
+
channel_count=raw.info["nchan"],
|
|
79
|
+
sampling_frequency=raw.info["sfreq"],
|
|
80
|
+
channel_names=raw.ch_names,
|
|
81
|
+
data=np.transpose(raw.get_data()),
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
dicom_eeg = RoutineScalpEEG(uid_root="1.2.3.4.5.", rse_data=dicom_eeg_data)
|
|
86
|
+
dicom_eeg.save_as("output.dcm")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Dependencies
|
|
90
|
+
|
|
91
|
+
**Required:**
|
|
92
|
+
- numpy >= 1.20.0
|
|
93
|
+
- pydicom >= 2.3.0
|
|
94
|
+
|
|
95
|
+
**Optional (for development):**
|
|
96
|
+
- pytest >= 7.0.0
|
|
97
|
+
- mne>=1.11.0
|
|
98
|
+
|
|
99
|
+
## Running Tests
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pytest
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
|
|
108
|
+
|
|
109
|
+
## Authors
|
|
110
|
+
|
|
111
|
+
- Evgenia Kartsaki
|
|
112
|
+
- Thomas Prampart
|
|
113
|
+
- Michael Kain
|
|
114
|
+
- Eric Poiseau
|
|
115
|
+
- Clement Chesnin
|
|
116
|
+
|
|
117
|
+
## Data Sources
|
|
118
|
+
This project uses the following data for unit testing:
|
|
119
|
+
* **[EEG Motor Movement/Imagery Dataset]**: Sourced from [https://physionet.org/content/eegmmidb/1.0.0/S001/].
|
|
120
|
+
* **License**: This data is licensed under the [Open Data Commons Attribution License (ODC-By) v1.0](https://physionet.org/content/eegmmidb/view-license/1.0.0/).
|
|
121
|
+
|
|
122
|
+
## Contributing
|
|
123
|
+
|
|
124
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dicomwf"
|
|
7
|
+
version = "0.3.0" # will be replaced by CI
|
|
8
|
+
description = "A Python library for creating DICOM EEG waveform files"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = "==3.11.*"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE.md"]
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Evgenia Kartsaki", email = "evgenia.kartsaki@inria.fr"},
|
|
15
|
+
{name = "Thomas Prampart", email = "thomas.prampart@inria.fr"},
|
|
16
|
+
{name = "Michael Kain", email = "michael.kain@inria.fr"},
|
|
17
|
+
{name = "Eric Poiseau", email = "eric.poiseau@inria.fr"},
|
|
18
|
+
{name = "Clément Chesnin"},
|
|
19
|
+
]
|
|
20
|
+
keywords = ["dicom", "eeg", "waveform", "medical", "imaging"]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Intended Audience :: Healthcare Industry",
|
|
24
|
+
"Intended Audience :: Science/Research",
|
|
25
|
+
"Development Status :: 5 - Production/Stable",
|
|
26
|
+
"Programming Language :: Python",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Topic :: Scientific/Engineering :: Medical Science Apps.",
|
|
30
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
31
|
+
"Topic :: Software Development :: Libraries"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
dependencies = [
|
|
35
|
+
"numpy>=1.20.0",
|
|
36
|
+
"pydicom>=2.3.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=7.0.0",
|
|
42
|
+
"mne>=1.11.0",
|
|
43
|
+
"build>=1.4.0",
|
|
44
|
+
"twine>=6.0.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://gitlab.inria.fr/openvibe/dicomwf"
|
|
49
|
+
Repository = "https://gitlab.inria.fr/openvibe/dicomwf"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
where = ["src"]
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.package-data]
|
|
55
|
+
dicomwf = ["py.typed"]
|
dicomwf-0.3.0/setup.cfg
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .routinescalp import RoutineScalpEEG
|
|
2
|
+
from .data import (
|
|
3
|
+
RoutineScalpEEGData,
|
|
4
|
+
GeneralEquipment,
|
|
5
|
+
Patient,
|
|
6
|
+
GeneralStudy,
|
|
7
|
+
Waveform,
|
|
8
|
+
WaveformIdentification,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"RoutineScalpEEG",
|
|
13
|
+
"RoutineScalpEEGData",
|
|
14
|
+
"GeneralEquipment",
|
|
15
|
+
"Patient",
|
|
16
|
+
"GeneralStudy",
|
|
17
|
+
"Waveform",
|
|
18
|
+
"WaveformIdentification",
|
|
19
|
+
]
|
|
20
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Data classes for DICOM Routine Scalp EEG waveform storage.
|
|
2
|
+
|
|
3
|
+
Defines the structured data containers used to populate a DICOM
|
|
4
|
+
Routine Scalp Electroencephalogram (EEG) Waveform object.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import datetime
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Patient:
|
|
15
|
+
"""DICOM Patient module.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
name: Patient's full name, using '^' as a separator (e.g. 'Doe^John') (DICOM keyword PatientName).
|
|
19
|
+
birth_date: Birth date (DICOM keyword PatientBirthDate) in DA format (YYYYMMDD).
|
|
20
|
+
sex: Patient sex — ``M``, ``F``, or ``O`` (DICOM keyword PatientSex).
|
|
21
|
+
id: Patient primary identifier. Defaults to ``name`` if not provided (DICOM keyword PatientID).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
birth_date: str
|
|
26
|
+
sex: str
|
|
27
|
+
id: str | None = None
|
|
28
|
+
|
|
29
|
+
def __post_init__(self):
|
|
30
|
+
if self.id is None:
|
|
31
|
+
self.id = self.name
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class GeneralStudy:
|
|
36
|
+
"""DICOM General Study module.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
id: Study identifier (DICOM keyword StudyID).
|
|
40
|
+
date: Date the study started (DICOM keyword StudyDate) in DICOM DA format (YYYYMMDD).
|
|
41
|
+
time: Time the study started (DICOM keyword StudyTime) in DICOM TM format (HHMMSS.FFFFFF).
|
|
42
|
+
accession_number: A departmental Information System generated number that identifies the Imaging Service Request (DICOM keyword AccessionNumber).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
id: str
|
|
46
|
+
date: str
|
|
47
|
+
time: str
|
|
48
|
+
accession_number: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class GeneralEquipment:
|
|
53
|
+
"""DICOM Enhanced General Equipment module.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
manufacturer: Equipment manufacturer name (DICOM keyword Manufacturer).
|
|
57
|
+
manufacturer_model_name: Manufacturer's model name of the equipment (DICOM keyword ManufacturerModelName).
|
|
58
|
+
device_serial_number: Manufacturer's serial number of the equipment (DICOM keyword DeviceSerialNumber).
|
|
59
|
+
software_versions: Manufacturer's designation of software version of the equipment (DICOM keyword SoftwareVersions).
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
manufacturer: str
|
|
63
|
+
manufacturer_model_name: str
|
|
64
|
+
device_serial_number: str
|
|
65
|
+
software_versions: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class WaveformIdentification:
|
|
70
|
+
"""DICOM Waveform Identification module.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
content_date_time: Date/time the content was created.
|
|
74
|
+
acquisition_date_time: Date/time that the acquisition of data that resulted in this waveform started (DICOM keyword AcquisitionDateTime).
|
|
75
|
+
instance_number: A number that identifies this waveform (DICOM keyword InstanceNumber). Defaults to "1".
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
content_date_time: datetime.datetime = datetime.datetime.now()
|
|
79
|
+
acquisition_date_time: datetime.datetime = datetime.datetime.now()
|
|
80
|
+
instance_number: str = "1"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class Waveform:
|
|
85
|
+
"""EEG waveform data and metadata. Not a direct mapping to a DICOM module, but used to populate the Waveform Sequence and Channel Definition Sequence.
|
|
86
|
+
|
|
87
|
+
Attributes:
|
|
88
|
+
channel_count: Number of EEG channels.
|
|
89
|
+
sampling_frequency: Sampling frequency in Hz.
|
|
90
|
+
channel_names: Ordered list of channel labels.
|
|
91
|
+
data: NumPy array of shape ``(samples, channels)`` holding the signal values.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
channel_count: int
|
|
95
|
+
sampling_frequency: float
|
|
96
|
+
channel_names: list[str]
|
|
97
|
+
data: np.ndarray
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class RoutineScalpEEGData:
|
|
102
|
+
"""Aggregates all data needed to build a Routine Scalp EEG DICOM object.
|
|
103
|
+
|
|
104
|
+
Attributes:
|
|
105
|
+
patient: Patient module.
|
|
106
|
+
study: Study module.
|
|
107
|
+
equipment: Equipment module.
|
|
108
|
+
waveform_identification: Waveform Identification module.
|
|
109
|
+
waveform: The EEG signal data and channel metadata.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
patient: Patient
|
|
113
|
+
study: GeneralStudy
|
|
114
|
+
equipment: GeneralEquipment
|
|
115
|
+
waveform_identification: WaveformIdentification
|
|
116
|
+
waveform: Waveform
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Builder for DICOM Routine Scalp EEG Waveform Storage objects.
|
|
2
|
+
|
|
3
|
+
Constructs a complete DICOM dataset conforming to the Routine Scalp
|
|
4
|
+
Electroencephalogram Waveform IOD (SOP Class 1.2.840.10008.5.1.4.1.1.9.1.2).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pydicom
|
|
9
|
+
from pydicom.dataset import Dataset, FileMetaDataset
|
|
10
|
+
from pydicom.uid import UID, ExplicitVRLittleEndian, ImplicitVRLittleEndian
|
|
11
|
+
from typing import Tuple
|
|
12
|
+
|
|
13
|
+
from dicomwf.data import (
|
|
14
|
+
Patient,
|
|
15
|
+
GeneralStudy,
|
|
16
|
+
GeneralEquipment,
|
|
17
|
+
WaveformIdentification,
|
|
18
|
+
Waveform,
|
|
19
|
+
RoutineScalpEEGData,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RoutineScalpEEG:
|
|
24
|
+
"""Builds and saves a DICOM Routine Scalp EEG Waveform dataset.
|
|
25
|
+
|
|
26
|
+
The constructor populates every required DICOM module from a
|
|
27
|
+
``RoutineScalpEEGData`` instance. Call ``save_as`` to write the
|
|
28
|
+
result to disk.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
ds: A DICOM dataset as a mutable mapping of DICOM Data Elements.
|
|
32
|
+
sop_class_uid: SOP Class UID for Routine Scalp EEG Waveform Storage.
|
|
33
|
+
uid_root: Organisation UID root used to derive all generated UIDs.
|
|
34
|
+
sop_instance_uid: Unique identifier for this SOP instance.
|
|
35
|
+
implementation_class_uid: Implementation Class UID written to file meta.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
uid_root: str,
|
|
41
|
+
rse_data: RoutineScalpEEGData,
|
|
42
|
+
):
|
|
43
|
+
"""Initialise the dataset and populate all DICOM modules.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
uid_root: Organisation-specific UID prefix used when generating
|
|
47
|
+
DICOM UIDs (Study, Series, SOP Instance).
|
|
48
|
+
rse_data: Structured container holding patient, study, equipment,
|
|
49
|
+
waveform identification, and waveform data.
|
|
50
|
+
"""
|
|
51
|
+
self.ds = Dataset()
|
|
52
|
+
self.sop_class_uid = pydicom.uid.RoutineScalpElectroencephalogramWaveformStorage
|
|
53
|
+
self.uid_root = uid_root
|
|
54
|
+
self.sop_instance_uid = pydicom.uid.generate_uid(prefix=self.uid_root)
|
|
55
|
+
self.implementation_class_uid = UID("1.2.3.4")
|
|
56
|
+
|
|
57
|
+
self._build_metadata()
|
|
58
|
+
self._build_patient(rse_data.patient)
|
|
59
|
+
self._build_study(rse_data.study)
|
|
60
|
+
self._build_series()
|
|
61
|
+
self._build_enhanced_general_equipment(rse_data.equipment)
|
|
62
|
+
self._build_waveform_identification(rse_data.waveform_identification)
|
|
63
|
+
self._build_waveform(rse_data.waveform)
|
|
64
|
+
|
|
65
|
+
def _build_metadata(self):
|
|
66
|
+
"""Populate the DICOM File Meta Information header."""
|
|
67
|
+
file_meta = FileMetaDataset()
|
|
68
|
+
file_meta.MediaStorageSOPClassUID = self.sop_class_uid
|
|
69
|
+
file_meta.MediaStorageSOPInstanceUID = self.sop_instance_uid
|
|
70
|
+
file_meta.ImplementationClassUID = self.implementation_class_uid
|
|
71
|
+
file_meta.TransferSyntaxUID = ExplicitVRLittleEndian
|
|
72
|
+
self.ds.file_meta = file_meta
|
|
73
|
+
|
|
74
|
+
def _build_patient(self, patient: Patient):
|
|
75
|
+
"""Populate the Patient module attributes."""
|
|
76
|
+
self.ds.PatientName = patient.name
|
|
77
|
+
self.ds.PatientID = patient.id
|
|
78
|
+
self.ds.PatientBirthDate = patient.birth_date
|
|
79
|
+
self.ds.PatientSex = patient.sex
|
|
80
|
+
|
|
81
|
+
def _build_study(self, study: GeneralStudy):
|
|
82
|
+
"""Populate the General Study module attributes."""
|
|
83
|
+
self.ds.StudyDate = study.date
|
|
84
|
+
self.ds.StudyTime = study.time
|
|
85
|
+
self.ds.AccessionNumber = study.accession_number
|
|
86
|
+
self.ds.ReferringPhysicianName = ""
|
|
87
|
+
self.ds.StudyInstanceUID = pydicom.uid.generate_uid(prefix=self.uid_root)
|
|
88
|
+
self.ds.StudyID = study.id
|
|
89
|
+
|
|
90
|
+
def _build_series(self):
|
|
91
|
+
"""Populate the General Series module (modality fixed to EEG)."""
|
|
92
|
+
self.ds.Modality = "EEG"
|
|
93
|
+
self.ds.SeriesInstanceUID = pydicom.uid.generate_uid(prefix=self.uid_root)
|
|
94
|
+
self.ds.SeriesNumber = "1"
|
|
95
|
+
|
|
96
|
+
def _build_enhanced_general_equipment(self, equipment: GeneralEquipment):
|
|
97
|
+
"""Populate the Enhanced General Equipment module attributes."""
|
|
98
|
+
self.ds.Manufacturer = equipment.manufacturer
|
|
99
|
+
self.ds.ManufacturerModelName = equipment.manufacturer_model_name
|
|
100
|
+
self.ds.DeviceSerialNumber = equipment.device_serial_number
|
|
101
|
+
self.ds.SoftwareVersions = equipment.software_versions
|
|
102
|
+
|
|
103
|
+
def _build_waveform_identification(self, identification: WaveformIdentification):
|
|
104
|
+
"""Populate the Waveform Identification module (content and acquisition timestamps)."""
|
|
105
|
+
self.ds.ContentDate = identification.content_date_time.strftime("%Y%m%d")
|
|
106
|
+
self.ds.ContentTime = identification.content_date_time.strftime("%H%M%S.%f")
|
|
107
|
+
self.ds.AcquisitionDateTime = identification.acquisition_date_time.strftime(
|
|
108
|
+
"%Y%m%d%H%M%S.%f"
|
|
109
|
+
)
|
|
110
|
+
self.ds.InstanceNumber = identification.instance_number
|
|
111
|
+
|
|
112
|
+
def _build_channel_definition_sequence(
|
|
113
|
+
self,
|
|
114
|
+
channel_name: str,
|
|
115
|
+
factor: float,
|
|
116
|
+
offset: float,
|
|
117
|
+
):
|
|
118
|
+
"""Build a single Channel Definition Sequence item.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
channel_name: Human-readable label for the channel (stored as
|
|
122
|
+
``CodeMeaning`` in the Channel Source Sequence).
|
|
123
|
+
factor: Scaling factor (``ChannelSensitivity``) used to convert
|
|
124
|
+
stored integer values back to real-world units.
|
|
125
|
+
offset: Baseline offset (``ChannelBaseline``) added after scaling.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
A ``pydicom.Dataset`` representing one channel definition.
|
|
129
|
+
"""
|
|
130
|
+
channel_definition = Dataset()
|
|
131
|
+
channel_definition.ChannelSampleSkew = "0"
|
|
132
|
+
channel_definition.ChannelSensitivity = f"{factor:.10g}"
|
|
133
|
+
channel_definition.ChannelBaseline = f"{offset:.10g}"
|
|
134
|
+
channel_definition.WaveformBitsStored = 64
|
|
135
|
+
channel_definition.ChannelSourceSequence = [Dataset()]
|
|
136
|
+
source = channel_definition.ChannelSourceSequence[0]
|
|
137
|
+
source.CodeValue = "1.0"
|
|
138
|
+
source.CodingSchemeDesignator = "PYDICOM"
|
|
139
|
+
source.CodingSchemeVersion = "1.0"
|
|
140
|
+
source.CodeMeaning = channel_name
|
|
141
|
+
|
|
142
|
+
return channel_definition
|
|
143
|
+
|
|
144
|
+
def quantize_to_int64(self, data: np.ndarray) -> Tuple[np.ndarray, np.float64, np.float64]:
|
|
145
|
+
"""Linearly quantize floating-point waveform data to signed 64-bit integers.
|
|
146
|
+
|
|
147
|
+
The input range ``[min, max]`` is mapped to ``[0, 2**31 - 1]`` so
|
|
148
|
+
the original values can be reconstructed as
|
|
149
|
+
``value = stored * factor + offset``.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
data: Array of floating-point samples to quantize.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
A tuple of ``(quantized_data, factor, offset)`` where
|
|
156
|
+
*quantized_data* is an ``int64`` C-contiguous array, *factor*
|
|
157
|
+
is the scaling multiplier, and *offset* is the baseline value.
|
|
158
|
+
"""
|
|
159
|
+
min, max = data.min(), data.max()
|
|
160
|
+
|
|
161
|
+
offset = min
|
|
162
|
+
|
|
163
|
+
target_range = 2**31 - 1
|
|
164
|
+
factor = (max - min) / target_range if min != max else 1.0
|
|
165
|
+
|
|
166
|
+
quantized_data = np.round((data - offset) / factor).astype(np.int64).copy(order="C")
|
|
167
|
+
return quantized_data, factor, offset
|
|
168
|
+
|
|
169
|
+
def _build_waveform_sequence(
|
|
170
|
+
self,
|
|
171
|
+
channel_count: int,
|
|
172
|
+
sampling_frequency: float,
|
|
173
|
+
channel_names: list[str],
|
|
174
|
+
data: np.ndarray,
|
|
175
|
+
):
|
|
176
|
+
"""Build a single Waveform Sequence item containing all channels.
|
|
177
|
+
|
|
178
|
+
The floating-point signal data is quantized to ``int64`` via
|
|
179
|
+
``quantize_to_int64`` and a ``ChannelDefinitionSequence`` is
|
|
180
|
+
created with the corresponding sensitivity/baseline values.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
channel_count: Number of channels in the waveform.
|
|
184
|
+
sampling_frequency: Sampling frequency in Hz.
|
|
185
|
+
channel_names: Ordered list of channel labels.
|
|
186
|
+
data: Array of shape ``(samples, channels)`` with signal values.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
A ``pydicom.Dataset`` representing one Waveform Sequence item.
|
|
190
|
+
"""
|
|
191
|
+
new = Dataset()
|
|
192
|
+
new.WaveformOriginality = "ORIGINAL"
|
|
193
|
+
new.NumberOfWaveformChannels = channel_count
|
|
194
|
+
new.NumberOfWaveformSamples = data.shape[0]
|
|
195
|
+
new.SamplingFrequency = sampling_frequency
|
|
196
|
+
|
|
197
|
+
quantized_data, factor, offset = self.quantize_to_int64(data)
|
|
198
|
+
|
|
199
|
+
new.ChannelDefinitionSequence = []
|
|
200
|
+
for i in range(channel_count):
|
|
201
|
+
new.ChannelDefinitionSequence.append(
|
|
202
|
+
self._build_channel_definition_sequence(channel_names[i], factor, offset)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
new.WaveformBitsAllocated = 64
|
|
206
|
+
new.WaveformSampleInterpretation = "SV"
|
|
207
|
+
new.WaveformData = quantized_data
|
|
208
|
+
|
|
209
|
+
return new
|
|
210
|
+
|
|
211
|
+
def _build_waveform(
|
|
212
|
+
self,
|
|
213
|
+
waveform: Waveform,
|
|
214
|
+
):
|
|
215
|
+
"""Populate the SOP Common module and the Waveform Sequence."""
|
|
216
|
+
self.ds.SOPInstanceUID = self.sop_instance_uid
|
|
217
|
+
self.ds.SOPClassUID = self.sop_class_uid
|
|
218
|
+
self.ds.WaveformSequence = [
|
|
219
|
+
self._build_waveform_sequence(
|
|
220
|
+
waveform.channel_count,
|
|
221
|
+
waveform.sampling_frequency,
|
|
222
|
+
waveform.channel_names,
|
|
223
|
+
waveform.data,
|
|
224
|
+
)
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
def save_as(self, filename: str):
|
|
228
|
+
"""Write the DICOM dataset to a file.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
filename: Destination file path.
|
|
232
|
+
"""
|
|
233
|
+
self.ds.save_as(filename, enforce_file_format=True)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dicomwf
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A Python library for creating DICOM EEG waveform files
|
|
5
|
+
Author: Clément Chesnin
|
|
6
|
+
Author-email: Evgenia Kartsaki <evgenia.kartsaki@inria.fr>, Thomas Prampart <thomas.prampart@inria.fr>, Michael Kain <michael.kain@inria.fr>, Eric Poiseau <eric.poiseau@inria.fr>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://gitlab.inria.fr/openvibe/dicomwf
|
|
9
|
+
Project-URL: Repository, https://gitlab.inria.fr/openvibe/dicomwf
|
|
10
|
+
Keywords: dicom,eeg,waveform,medical,imaging
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: ==3.11.*
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE.md
|
|
24
|
+
Requires-Dist: numpy>=1.20.0
|
|
25
|
+
Requires-Dist: pydicom>=2.3.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: mne>=1.11.0; extra == "dev"
|
|
29
|
+
Requires-Dist: build>=1.4.0; extra == "dev"
|
|
30
|
+
Requires-Dist: twine>=6.0.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# dicomwf
|
|
34
|
+
|
|
35
|
+
A Python library for integrating waveforms in DICOM files.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
Supported waveforms IODs:
|
|
40
|
+
- RoutineScalpEEG
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
### User Installation
|
|
45
|
+
|
|
46
|
+
Install directly from PyPI:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install dicomwf
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Developer Installation
|
|
53
|
+
|
|
54
|
+
1. Clone the repository:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://gitlab.inria.fr/openvibe/dicomwf.git
|
|
58
|
+
cd dicomwf
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
2. Create and activate a virtual environment:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m venv .venv
|
|
65
|
+
|
|
66
|
+
# On Windows
|
|
67
|
+
.venv\Scripts\activate
|
|
68
|
+
|
|
69
|
+
# On Unix/macOS
|
|
70
|
+
source .venv/bin/activate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
3. Install in editable mode with development dependencies:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install -e ".[dev]"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This installs the package in editable mode along with development tools.
|
|
80
|
+
|
|
81
|
+
## Quick Start
|
|
82
|
+
|
|
83
|
+
We will use the EEG data in the edf format from the [EEGBCI dataset](https://doi.org/10.1109/TBME.2004.827072).
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import numpy as np
|
|
87
|
+
from dicomwf import *
|
|
88
|
+
from urllib.request import urlretrieve
|
|
89
|
+
import mne
|
|
90
|
+
|
|
91
|
+
# Load EEG data
|
|
92
|
+
url = "https://physionet.org/files/eegmmidb/1.0.0/S001/S001R01.edf?download"
|
|
93
|
+
raw_fname = "S001R01.edf"
|
|
94
|
+
urlretrieve(url, raw_fname)
|
|
95
|
+
|
|
96
|
+
raw = mne.io.read_raw_edf(raw_fname, preload=True)
|
|
97
|
+
|
|
98
|
+
# Create DICOM EEG
|
|
99
|
+
dicom_eeg_data = RoutineScalpEEGData(
|
|
100
|
+
patient=Patient(name="Doe^John", birth_date="20250101", sex="M"),
|
|
101
|
+
study=GeneralStudy(id="1",date="19940423", time="101601.934000", accession_number="1"),
|
|
102
|
+
equipment=GeneralEquipment(
|
|
103
|
+
manufacturer="PyDICOM",
|
|
104
|
+
manufacturer_model_name="PyDICOM",
|
|
105
|
+
device_serial_number="1",
|
|
106
|
+
software_versions="1.0",
|
|
107
|
+
),
|
|
108
|
+
waveform_identification=WaveformIdentification(),
|
|
109
|
+
waveform=Waveform(
|
|
110
|
+
channel_count=raw.info["nchan"],
|
|
111
|
+
sampling_frequency=raw.info["sfreq"],
|
|
112
|
+
channel_names=raw.ch_names,
|
|
113
|
+
data=np.transpose(raw.get_data()),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
dicom_eeg = RoutineScalpEEG(uid_root="1.2.3.4.5.", rse_data=dicom_eeg_data)
|
|
118
|
+
dicom_eeg.save_as("output.dcm")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Dependencies
|
|
122
|
+
|
|
123
|
+
**Required:**
|
|
124
|
+
- numpy >= 1.20.0
|
|
125
|
+
- pydicom >= 2.3.0
|
|
126
|
+
|
|
127
|
+
**Optional (for development):**
|
|
128
|
+
- pytest >= 7.0.0
|
|
129
|
+
- mne>=1.11.0
|
|
130
|
+
|
|
131
|
+
## Running Tests
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pytest
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
|
|
140
|
+
|
|
141
|
+
## Authors
|
|
142
|
+
|
|
143
|
+
- Evgenia Kartsaki
|
|
144
|
+
- Thomas Prampart
|
|
145
|
+
- Michael Kain
|
|
146
|
+
- Eric Poiseau
|
|
147
|
+
- Clement Chesnin
|
|
148
|
+
|
|
149
|
+
## Data Sources
|
|
150
|
+
This project uses the following data for unit testing:
|
|
151
|
+
* **[EEG Motor Movement/Imagery Dataset]**: Sourced from [https://physionet.org/content/eegmmidb/1.0.0/S001/].
|
|
152
|
+
* **License**: This data is licensed under the [Open Data Commons Attribution License (ODC-By) v1.0](https://physionet.org/content/eegmmidb/view-license/1.0.0/).
|
|
153
|
+
|
|
154
|
+
## Contributing
|
|
155
|
+
|
|
156
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE.md
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/dicomwf/__init__.py
|
|
5
|
+
src/dicomwf/data.py
|
|
6
|
+
src/dicomwf/routinescalp.py
|
|
7
|
+
src/dicomwf.egg-info/PKG-INFO
|
|
8
|
+
src/dicomwf.egg-info/SOURCES.txt
|
|
9
|
+
src/dicomwf.egg-info/dependency_links.txt
|
|
10
|
+
src/dicomwf.egg-info/requires.txt
|
|
11
|
+
src/dicomwf.egg-info/top_level.txt
|
|
12
|
+
tests/test_routinescalp.py
|
|
13
|
+
tests/test_routinescalp_unit.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dicomwf
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import mne
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pydicom
|
|
6
|
+
|
|
7
|
+
from dicomwf import *
|
|
8
|
+
from urllib.request import urlretrieve
|
|
9
|
+
|
|
10
|
+
def test_routinescalp():
|
|
11
|
+
|
|
12
|
+
# Load EEG data
|
|
13
|
+
url = "https://physionet.org/files/eegmmidb/1.0.0/S001/S001R01.edf?download"
|
|
14
|
+
raw_fname = "S001R01.edf"
|
|
15
|
+
urlretrieve(url, raw_fname)
|
|
16
|
+
|
|
17
|
+
raw = mne.io.read_raw_edf(raw_fname, preload=True)
|
|
18
|
+
|
|
19
|
+
# Create a DICOM EEG
|
|
20
|
+
dicom_eeg_data = RoutineScalpEEGData(
|
|
21
|
+
patient=Patient(name="John^Doe", birth_date="20250101", sex="M"),
|
|
22
|
+
study=GeneralStudy(
|
|
23
|
+
date="19940423", time="101601.934000", accession_number="1", id="847542"
|
|
24
|
+
),
|
|
25
|
+
equipment=GeneralEquipment(
|
|
26
|
+
manufacturer="PyDICOM",
|
|
27
|
+
manufacturer_model_name="PyDICOM",
|
|
28
|
+
device_serial_number="1",
|
|
29
|
+
software_versions="1.0",
|
|
30
|
+
),
|
|
31
|
+
waveform_identification=WaveformIdentification(),
|
|
32
|
+
waveform=Waveform(
|
|
33
|
+
channel_count=raw.info["nchan"],
|
|
34
|
+
sampling_frequency=raw.info["sfreq"],
|
|
35
|
+
channel_names=raw.ch_names,
|
|
36
|
+
data=np.transpose(raw.get_data()),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
dicom_eeg = RoutineScalpEEG(uid_root="1.2.3.4.5.", rse_data=dicom_eeg_data)
|
|
41
|
+
dicom_eeg.save_as("toto.dcm")
|
|
42
|
+
|
|
43
|
+
pydicom_eeg = pydicom.dcmread("toto.dcm")
|
|
44
|
+
assert pydicom_eeg.Modality == "EEG"
|
|
45
|
+
assert pydicom_eeg.PatientName == "John^Doe"
|
|
46
|
+
assert pydicom_eeg.PatientBirthDate == "20250101"
|
|
47
|
+
assert pydicom_eeg.PatientSex == "M"
|
|
48
|
+
assert pydicom_eeg.PatientID == "John^Doe"
|
|
49
|
+
assert pydicom_eeg.AccessionNumber == "1"
|
|
50
|
+
assert pydicom_eeg.StudyDate == "19940423"
|
|
51
|
+
assert pydicom_eeg.StudyTime == "101601.934000"
|
|
52
|
+
assert pydicom_eeg.StudyID == "847542"
|
|
53
|
+
assert pydicom_eeg.Manufacturer == "PyDICOM"
|
|
54
|
+
assert pydicom_eeg.ManufacturerModelName == "PyDICOM"
|
|
55
|
+
assert pydicom_eeg.DeviceSerialNumber == "1"
|
|
56
|
+
assert pydicom_eeg.SoftwareVersions == "1.0"
|
|
57
|
+
|
|
58
|
+
assert len(pydicom_eeg.WaveformSequence) == 1
|
|
59
|
+
waveform = pydicom_eeg.WaveformSequence[0]
|
|
60
|
+
assert waveform.NumberOfWaveformChannels == raw.info["nchan"]
|
|
61
|
+
assert waveform.SamplingFrequency == raw.info["sfreq"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
assert len(waveform.ChannelDefinitionSequence) == raw.info["nchan"]
|
|
65
|
+
factor = waveform.ChannelDefinitionSequence[0].ChannelSensitivity
|
|
66
|
+
offset = waveform.ChannelDefinitionSequence[0].ChannelBaseline
|
|
67
|
+
|
|
68
|
+
for i in range(raw.info["nchan"]):
|
|
69
|
+
assert waveform.ChannelDefinitionSequence[i].ChannelSourceSequence[0].CodeMeaning == raw.ch_names[i]
|
|
70
|
+
|
|
71
|
+
print(len(waveform.WaveformData))
|
|
72
|
+
|
|
73
|
+
data = np.frombuffer(waveform.WaveformData, dtype=np.int64)
|
|
74
|
+
assert len(data) == raw.info["nchan"] * raw.n_times
|
|
75
|
+
|
|
76
|
+
data = np.reshape(data, (raw.n_times, raw.info["nchan"]))
|
|
77
|
+
data = np.transpose(data * factor + offset)
|
|
78
|
+
assert np.allclose(data, raw.get_data(), atol=1e-6)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pydicom
|
|
7
|
+
from pydicom.uid import ExplicitVRLittleEndian
|
|
8
|
+
|
|
9
|
+
from dicomwf.routinescalp import RoutineScalpEEG
|
|
10
|
+
from dicomwf.data import Patient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestRoutineScalpEEG:
|
|
14
|
+
|
|
15
|
+
def test_init(self, sample_rse_data):
|
|
16
|
+
# Test uid_root is stored
|
|
17
|
+
eeg = RoutineScalpEEG(uid_root="9.8.7.6.5.", rse_data=sample_rse_data)
|
|
18
|
+
assert eeg.uid_root == "9.8.7.6.5."
|
|
19
|
+
|
|
20
|
+
# Test sop_instance_uid uses root
|
|
21
|
+
assert eeg.sop_instance_uid.startswith("9.8.7.6.5.")
|
|
22
|
+
|
|
23
|
+
# Test dataset is created
|
|
24
|
+
assert eeg.ds is not None
|
|
25
|
+
|
|
26
|
+
def test_build_metadata(self, routine_scalp_eeg):
|
|
27
|
+
assert hasattr(routine_scalp_eeg.ds, "file_meta")
|
|
28
|
+
assert routine_scalp_eeg.ds.file_meta is not None
|
|
29
|
+
assert routine_scalp_eeg.ds.file_meta.MediaStorageSOPClassUID == pydicom.uid.RoutineScalpElectroencephalogramWaveformStorage
|
|
30
|
+
assert routine_scalp_eeg.ds.file_meta.MediaStorageSOPInstanceUID == routine_scalp_eeg.sop_instance_uid
|
|
31
|
+
assert routine_scalp_eeg.sop_instance_uid.startswith("1.2.3.4.5.")
|
|
32
|
+
assert routine_scalp_eeg.ds.file_meta.ImplementationClassUID == "1.2.3.4"
|
|
33
|
+
assert routine_scalp_eeg.ds.file_meta.TransferSyntaxUID == ExplicitVRLittleEndian
|
|
34
|
+
|
|
35
|
+
def test_build_patient(self, routine_scalp_eeg):
|
|
36
|
+
assert routine_scalp_eeg.ds.PatientName == "Test^Patient"
|
|
37
|
+
assert routine_scalp_eeg.ds.PatientID == "PAT001"
|
|
38
|
+
assert routine_scalp_eeg.ds.PatientBirthDate == "19900101"
|
|
39
|
+
assert routine_scalp_eeg.ds.PatientSex == "F"
|
|
40
|
+
|
|
41
|
+
# Test patient id defaults to name
|
|
42
|
+
patient = Patient(name="Default^Name", birth_date="20000101", sex="M")
|
|
43
|
+
assert patient.id == "Default^Name"
|
|
44
|
+
|
|
45
|
+
def test_build_study(self, routine_scalp_eeg):
|
|
46
|
+
assert routine_scalp_eeg.ds.StudyDate == "20240115"
|
|
47
|
+
assert routine_scalp_eeg.ds.StudyTime == "143022.000000"
|
|
48
|
+
assert routine_scalp_eeg.ds.AccessionNumber == "ACC123"
|
|
49
|
+
assert routine_scalp_eeg.ds.ReferringPhysicianName == ""
|
|
50
|
+
assert routine_scalp_eeg.ds.StudyInstanceUID.startswith("1.2.3.4.5.")
|
|
51
|
+
assert routine_scalp_eeg.ds.StudyID == "847542"
|
|
52
|
+
|
|
53
|
+
def test_build_series(self, routine_scalp_eeg):
|
|
54
|
+
assert routine_scalp_eeg.ds.Modality == "EEG"
|
|
55
|
+
assert routine_scalp_eeg.ds.SeriesInstanceUID.startswith("1.2.3.4.5.")
|
|
56
|
+
assert routine_scalp_eeg.ds.SeriesNumber == "1"
|
|
57
|
+
|
|
58
|
+
def test_build_enhanced_general_equipment(self, routine_scalp_eeg):
|
|
59
|
+
assert routine_scalp_eeg.ds.Manufacturer == "TestManufacturer"
|
|
60
|
+
assert routine_scalp_eeg.ds.ManufacturerModelName == "TestModel"
|
|
61
|
+
assert routine_scalp_eeg.ds.DeviceSerialNumber == "SN12345"
|
|
62
|
+
assert routine_scalp_eeg.ds.SoftwareVersions == "2.0.1"
|
|
63
|
+
|
|
64
|
+
def test_build_waveform_identification(self, routine_scalp_eeg):
|
|
65
|
+
assert routine_scalp_eeg.ds.ContentDate == "20240115"
|
|
66
|
+
assert routine_scalp_eeg.ds.ContentTime == "143022.123456"
|
|
67
|
+
assert routine_scalp_eeg.ds.AcquisitionDateTime == "20240115142500.000000"
|
|
68
|
+
assert routine_scalp_eeg.ds.InstanceNumber == "5"
|
|
69
|
+
|
|
70
|
+
def test_build_channel_definition_sequence(self, routine_scalp_eeg):
|
|
71
|
+
channel_def = routine_scalp_eeg._build_channel_definition_sequence("TestChannel", 1.5, 0.5)
|
|
72
|
+
|
|
73
|
+
assert channel_def.ChannelSampleSkew == "0"
|
|
74
|
+
assert float(channel_def.ChannelSensitivity) == 1.5
|
|
75
|
+
assert float(channel_def.ChannelBaseline) == 0.5
|
|
76
|
+
# Verify DS values fit within 16 character limit
|
|
77
|
+
assert len(str(channel_def.ChannelSensitivity)) <= 16
|
|
78
|
+
assert len(str(channel_def.ChannelBaseline)) <= 16
|
|
79
|
+
assert channel_def.WaveformBitsStored == 64
|
|
80
|
+
|
|
81
|
+
# Test channel source sequence
|
|
82
|
+
assert len(channel_def.ChannelSourceSequence) == 1
|
|
83
|
+
source = channel_def.ChannelSourceSequence[0]
|
|
84
|
+
assert source.CodeValue == "1.0"
|
|
85
|
+
assert source.CodingSchemeDesignator == "PYDICOM"
|
|
86
|
+
assert source.CodingSchemeVersion == "1.0"
|
|
87
|
+
assert source.CodeMeaning == "TestChannel"
|
|
88
|
+
|
|
89
|
+
def test_quantize_to_int64(self, routine_scalp_eeg):
|
|
90
|
+
# Basic quantization
|
|
91
|
+
data = np.array([[0.0, 1.0], [0.5, 0.75]])
|
|
92
|
+
quantized, factor, offset = routine_scalp_eeg.quantize_to_int64(data)
|
|
93
|
+
assert quantized.dtype == np.int64
|
|
94
|
+
assert offset == 0.0
|
|
95
|
+
assert factor == 1.0 / (2**31 - 1)
|
|
96
|
+
|
|
97
|
+
# Shape preservation
|
|
98
|
+
data = np.random.randn(100, 5)
|
|
99
|
+
quantized, factor, offset = routine_scalp_eeg.quantize_to_int64(data)
|
|
100
|
+
assert quantized.shape == data.shape
|
|
101
|
+
|
|
102
|
+
# Constant data (factor should be 1.0)
|
|
103
|
+
data = np.ones((10, 3)) * 5.0
|
|
104
|
+
quantized, factor, offset = routine_scalp_eeg.quantize_to_int64(data)
|
|
105
|
+
assert factor == 1.0
|
|
106
|
+
assert offset == 5.0
|
|
107
|
+
assert np.all(quantized == 0)
|
|
108
|
+
|
|
109
|
+
# Range mapping
|
|
110
|
+
data = np.array([[-100.0], [100.0]])
|
|
111
|
+
quantized, factor, offset = routine_scalp_eeg.quantize_to_int64(data)
|
|
112
|
+
assert offset == -100.0
|
|
113
|
+
assert factor == pytest.approx(0.0000000931323, abs=1e-13)
|
|
114
|
+
assert quantized[0, 0] == 0
|
|
115
|
+
assert quantized[1, 0] == 2**31 - 1
|
|
116
|
+
|
|
117
|
+
# C-order output
|
|
118
|
+
data = np.array([[1.0, 2.0], [3.0, 4.0]], order='F')
|
|
119
|
+
quantized, _, _ = routine_scalp_eeg.quantize_to_int64(data)
|
|
120
|
+
assert quantized.flags['C_CONTIGUOUS']
|
|
121
|
+
|
|
122
|
+
def test_build_waveform_sequence(self, routine_scalp_eeg):
|
|
123
|
+
data = np.random.randn(150, 3)
|
|
124
|
+
seq = routine_scalp_eeg._build_waveform_sequence(3, 512.0, ["Ch1", "Ch2", "Ch3"], data)
|
|
125
|
+
|
|
126
|
+
assert seq.WaveformOriginality == "ORIGINAL"
|
|
127
|
+
assert seq.NumberOfWaveformChannels == 3
|
|
128
|
+
assert seq.NumberOfWaveformSamples == 150
|
|
129
|
+
assert seq.SamplingFrequency == 512.0
|
|
130
|
+
assert len(seq.ChannelDefinitionSequence) == 3
|
|
131
|
+
assert seq.WaveformBitsAllocated == 64
|
|
132
|
+
assert seq.WaveformSampleInterpretation == "SV"
|
|
133
|
+
|
|
134
|
+
def test_build_waveform(self, routine_scalp_eeg):
|
|
135
|
+
assert routine_scalp_eeg.ds.SOPInstanceUID == routine_scalp_eeg.sop_instance_uid
|
|
136
|
+
assert routine_scalp_eeg.ds.SOPClassUID == pydicom.uid.RoutineScalpElectroencephalogramWaveformStorage
|
|
137
|
+
assert hasattr(routine_scalp_eeg.ds, "WaveformSequence")
|
|
138
|
+
assert len(routine_scalp_eeg.ds.WaveformSequence) == 1
|
|
139
|
+
|
|
140
|
+
def test_save_as(self, routine_scalp_eeg):
|
|
141
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
142
|
+
filepath = os.path.join(tmpdir, "test_output.dcm")
|
|
143
|
+
routine_scalp_eeg.save_as(filepath)
|
|
144
|
+
|
|
145
|
+
assert os.path.exists(filepath)
|
|
146
|
+
|
|
147
|
+
# Read back and verify
|
|
148
|
+
ds = pydicom.dcmread(filepath)
|
|
149
|
+
assert ds.Modality == "EEG"
|
|
150
|
+
assert ds.PatientName == "Test^Patient"
|
|
151
|
+
|
|
152
|
+
# Verify it's a valid DICOM file
|
|
153
|
+
assert hasattr(ds, "file_meta")
|
|
154
|
+
assert ds.file_meta.TransferSyntaxUID == ExplicitVRLittleEndian
|