dicompare 0.1.38__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.
Files changed (67) hide show
  1. dicompare-0.1.38/LICENSE +21 -0
  2. dicompare-0.1.38/PKG-INFO +227 -0
  3. dicompare-0.1.38/README.md +191 -0
  4. dicompare-0.1.38/dicompare/__init__.py +18 -0
  5. dicompare-0.1.38/dicompare/cli/__init__.py +0 -0
  6. dicompare-0.1.38/dicompare/cli/main.py +222 -0
  7. dicompare-0.1.38/dicompare/config.py +197 -0
  8. dicompare-0.1.38/dicompare/data_utils.py +259 -0
  9. dicompare-0.1.38/dicompare/interface/__init__.py +15 -0
  10. dicompare-0.1.38/dicompare/interface/web_utils.py +287 -0
  11. dicompare-0.1.38/dicompare/io/__init__.py +76 -0
  12. dicompare-0.1.38/dicompare/io/dicom.py +773 -0
  13. dicompare-0.1.38/dicompare/io/dicom_generator.py +568 -0
  14. dicompare-0.1.38/dicompare/io/json.py +120 -0
  15. dicompare-0.1.38/dicompare/io/pro.py +1430 -0
  16. dicompare-0.1.38/dicompare/io/special_fields.py +304 -0
  17. dicompare-0.1.38/dicompare/processing/__init__.py +26 -0
  18. dicompare-0.1.38/dicompare/processing/parallel_utils.py +75 -0
  19. dicompare-0.1.38/dicompare/processing/progress_utils.py +158 -0
  20. dicompare-0.1.38/dicompare/schema/__init__.py +32 -0
  21. dicompare-0.1.38/dicompare/schema/build_schema.py +100 -0
  22. dicompare-0.1.38/dicompare/schema/tags.py +293 -0
  23. dicompare-0.1.38/dicompare/session/__init__.py +24 -0
  24. dicompare-0.1.38/dicompare/session/acquisition.py +500 -0
  25. dicompare-0.1.38/dicompare/session/mapping.py +389 -0
  26. dicompare-0.1.38/dicompare/tests/__init__.py +0 -0
  27. dicompare-0.1.38/dicompare/tests/fixtures/__init__.py +0 -0
  28. dicompare-0.1.38/dicompare/tests/fixtures/fixtures.py +203 -0
  29. dicompare-0.1.38/dicompare/tests/fixtures/ref_empty.py +0 -0
  30. dicompare-0.1.38/dicompare/tests/generate_test_dataset.py +262 -0
  31. dicompare-0.1.38/dicompare/tests/test_acquisition_assignment.py +180 -0
  32. dicompare-0.1.38/dicompare/tests/test_cli.py +162 -0
  33. dicompare-0.1.38/dicompare/tests/test_compliance.py +864 -0
  34. dicompare-0.1.38/dicompare/tests/test_compliance_series_fix.py +133 -0
  35. dicompare-0.1.38/dicompare/tests/test_compliance_unified.py +322 -0
  36. dicompare-0.1.38/dicompare/tests/test_data_utils.py +345 -0
  37. dicompare-0.1.38/dicompare/tests/test_dicom_factory.py +402 -0
  38. dicompare-0.1.38/dicompare/tests/test_dicom_generator.py +177 -0
  39. dicompare-0.1.38/dicompare/tests/test_generate_schema.py +243 -0
  40. dicompare-0.1.38/dicompare/tests/test_helpers.py +54 -0
  41. dicompare-0.1.38/dicompare/tests/test_integration_hybrid.py +280 -0
  42. dicompare-0.1.38/dicompare/tests/test_io.py +767 -0
  43. dicompare-0.1.38/dicompare/tests/test_multiband_extraction.py +109 -0
  44. dicompare-0.1.38/dicompare/tests/test_pro_files.py +266 -0
  45. dicompare-0.1.38/dicompare/tests/test_ref_dicom.py +19 -0
  46. dicompare-0.1.38/dicompare/tests/test_schema_integration.py +659 -0
  47. dicompare-0.1.38/dicompare/tests/test_schema_json_generator.py +293 -0
  48. dicompare-0.1.38/dicompare/tests/test_serialization.py +295 -0
  49. dicompare-0.1.38/dicompare/tests/test_tags.py +248 -0
  50. dicompare-0.1.38/dicompare/tests/test_utils_enhanced.py +207 -0
  51. dicompare-0.1.38/dicompare/tests/test_validation_dynamic.py +276 -0
  52. dicompare-0.1.38/dicompare/tests/test_visualization.py +18 -0
  53. dicompare-0.1.38/dicompare/tests/test_web_utils.py +103 -0
  54. dicompare-0.1.38/dicompare/utils.py +215 -0
  55. dicompare-0.1.38/dicompare/validation/__init__.py +61 -0
  56. dicompare-0.1.38/dicompare/validation/compliance.py +326 -0
  57. dicompare-0.1.38/dicompare/validation/core.py +458 -0
  58. dicompare-0.1.38/dicompare/validation/helpers.py +448 -0
  59. dicompare-0.1.38/dicompare.egg-info/PKG-INFO +227 -0
  60. dicompare-0.1.38/dicompare.egg-info/SOURCES.txt +65 -0
  61. dicompare-0.1.38/dicompare.egg-info/dependency_links.txt +1 -0
  62. dicompare-0.1.38/dicompare.egg-info/entry_points.txt +2 -0
  63. dicompare-0.1.38/dicompare.egg-info/requires.txt +13 -0
  64. dicompare-0.1.38/dicompare.egg-info/top_level.txt +1 -0
  65. dicompare-0.1.38/pyproject.toml +7 -0
  66. dicompare-0.1.38/setup.cfg +4 -0
  67. dicompare-0.1.38/setup.py +49 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Ashley Stewart
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,227 @@
1
+ Metadata-Version: 2.4
2
+ Name: dicompare
3
+ Version: 0.1.38
4
+ Summary: A tool for checking DICOM compliance against a template
5
+ Home-page: https://github.com/astewartau/dicompare
6
+ Author: Ashley Stewart
7
+ Keywords: DICOM compliance validation medical imaging
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pydicom==2.4.4
15
+ Requires-Dist: pandas
16
+ Requires-Dist: tabulate
17
+ Requires-Dist: scipy
18
+ Requires-Dist: tqdm
19
+ Requires-Dist: nibabel
20
+ Requires-Dist: twixtools
21
+ Provides-Extra: interactive
22
+ Requires-Dist: curses; extra == "interactive"
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest-asyncio; extra == "test"
25
+ Dynamic: author
26
+ Dynamic: classifier
27
+ Dynamic: description
28
+ Dynamic: description-content-type
29
+ Dynamic: home-page
30
+ Dynamic: keywords
31
+ Dynamic: license-file
32
+ Dynamic: provides-extra
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
35
+ Dynamic: summary
36
+
37
+ # dicompare
38
+
39
+ [![](img/button.png)](https://dicompare-web.vercel.app/)
40
+
41
+ dicompare is a DICOM validation tool designed to ensure compliance with study-specific imaging protocols and domain-specific guidelines while preserving data privacy. It provides multiple interfaces, including support for validation directly in the browser at [dicompare-web.vercel.app](https://dicompare-web.vercel.app/), leveraging WebAssembly (WASM), Pyodide, and the underlying pip package `dicompare`. dicompare is suitable for multi-site studies and clinical environments without requiring software installation or external data uploads.
42
+
43
+ dicompare supports DICOM session validation against templates based on:
44
+
45
+ - **Reference sessions**: JSON schema files can be generated based on a reference MRI scanning session;
46
+ - **[TESTING] domain guidelines**: Flexible guidelines for specific domains (currently [QSM](https://doi.org/10.1002/mrm.30006));
47
+ - **[FUTURE] landmark studies**: Schema files based on landmark studies such as the [HCP](https://doi.org/10.1038/s41586-018-0579-z), [ABCD](https://doi.org/10.1016/j.dcn.2018.03.001), and [UK BioBank](https://doi.org/10.1038/s41586-018-0579-z) projects.
48
+
49
+ # Command-line interface (CLI) and application programming interface (API)
50
+
51
+ While you can run [dicompare](https://dicompare-web.vercel.app/) in your browser now without any installation, you may also use the underlying `dicompare` pip package if you wish to use the command-line interface (CLI) or application programming interface (API).
52
+
53
+ ```bash
54
+ pip install dicompare
55
+ ```
56
+
57
+ ## Command-line interface (CLI)
58
+
59
+ The package provides a unified `dicompare` command with two subcommands:
60
+
61
+ - **`dicompare build`**: Generate a JSON schema from a reference DICOM session
62
+ - **`dicompare check`**: Validate DICOM sessions against a JSON schema
63
+
64
+ ### 1. Build a JSON schema from a reference session
65
+
66
+ ```bash
67
+ dicompare build /path/to/dicom/session schema.json
68
+ ```
69
+
70
+ This creates a JSON schema describing the session based on default reference fields present in the data.
71
+
72
+ ### 2. Check a DICOM session against a schema
73
+
74
+ ```bash
75
+ dicompare check /path/to/dicom/session schema.json
76
+ ```
77
+
78
+ The tool will output a compliance summary, indicating deviations from the schema.
79
+
80
+ ### 3. Check with report output
81
+
82
+ ```bash
83
+ dicompare check /path/to/dicom/session schema.json compliance_report.json
84
+ ```
85
+
86
+ This saves the compliance report to a JSON file.
87
+
88
+ ### 4. Automatic acquisition mapping
89
+
90
+ ```bash
91
+ dicompare check /path/to/dicom/session schema.json --auto-yes
92
+ ```
93
+
94
+ Use `--auto-yes` or `-y` to automatically map acquisitions without interactive prompts.
95
+
96
+ ## Python API
97
+
98
+ The `dicompare` package provides a comprehensive Python API for programmatic schema generation, validation, and DICOM processing.
99
+
100
+ ### Loading DICOM data
101
+
102
+ **Load a DICOM session:**
103
+
104
+ ```python
105
+ from dicompare import load_dicom_session
106
+
107
+ session_df = load_dicom_session(
108
+ session_dir="/path/to/dicom/session",
109
+ show_progress=True
110
+ )
111
+ ```
112
+
113
+ **Load individual DICOM files:**
114
+
115
+ ```python
116
+ from dicompare import load_dicom
117
+
118
+ dicom_data = load_dicom(
119
+ dicom_paths=["/path/to/file1.dcm", "/path/to/file2.dcm"],
120
+ show_progress=True
121
+ )
122
+ ```
123
+
124
+ **Load Siemens .pro files:**
125
+
126
+ ```python
127
+ from dicompare import load_pro_session
128
+
129
+ pro_session = load_pro_session(
130
+ session_dir="/path/to/pro/files",
131
+ show_progress=True
132
+ )
133
+ ```
134
+
135
+ ### Build a JSON schema
136
+
137
+ ```python
138
+ from dicompare import load_dicom_session, build_schema, make_json_serializable
139
+ from dicompare.config import DEFAULT_SETTINGS_FIELDS
140
+ import json
141
+
142
+ # Load the reference session
143
+ session_df = load_dicom_session(
144
+ session_dir="/path/to/dicom/session",
145
+ show_progress=True
146
+ )
147
+
148
+ # Build the schema
149
+ json_schema = build_schema(session_df)
150
+
151
+ # Save the schema
152
+ serializable_schema = make_json_serializable(json_schema)
153
+ with open("schema.json", "w") as f:
154
+ json.dump(serializable_schema, f, indent=4)
155
+ ```
156
+
157
+ ### Validate a session against a JSON schema
158
+
159
+ ```python
160
+ from dicompare import (
161
+ load_schema,
162
+ load_dicom_session,
163
+ check_acquisition_compliance,
164
+ map_to_json_reference,
165
+ assign_acquisition_and_run_numbers
166
+ )
167
+
168
+ # Load the JSON schema
169
+ reference_fields, json_schema, validation_rules = load_schema(json_schema_path="schema.json")
170
+
171
+ # Load the input session
172
+ in_session = load_dicom_session(
173
+ session_dir="/path/to/dicom/session",
174
+ show_progress=True
175
+ )
176
+
177
+ # Assign acquisition and run numbers
178
+ in_session = assign_acquisition_and_run_numbers(in_session)
179
+
180
+ # Map acquisitions to schema
181
+ session_map = map_to_json_reference(in_session, json_schema)
182
+
183
+ # Check compliance for each acquisition
184
+ compliance_summary = []
185
+ for ref_acq_name, schema_acq in json_schema["acquisitions"].items():
186
+ if ref_acq_name not in session_map:
187
+ continue
188
+
189
+ input_acq_name = session_map[ref_acq_name]
190
+ acq_validation_rules = validation_rules.get(ref_acq_name) if validation_rules else None
191
+
192
+ results = check_acquisition_compliance(
193
+ in_session,
194
+ schema_acq,
195
+ acquisition_name=input_acq_name,
196
+ validation_rules=acq_validation_rules
197
+ )
198
+ compliance_summary.extend(results)
199
+
200
+ # Display results
201
+ for entry in compliance_summary:
202
+ print(entry)
203
+ ```
204
+
205
+ ### Additional utilities
206
+
207
+ **Assign acquisition and run numbers:**
208
+
209
+ ```python
210
+ from dicompare import assign_acquisition_and_run_numbers
211
+
212
+ session_df = assign_acquisition_and_run_numbers(session_df)
213
+ ```
214
+
215
+ **Get DICOM tag information:**
216
+
217
+ ```python
218
+ from dicompare import get_tag_info, get_all_tags_in_dataset
219
+
220
+ # Get info about a specific tag
221
+ tag_info = get_tag_info("EchoTime")
222
+ print(tag_info) # {'tag': '(0018,0081)', 'name': 'Echo Time', 'type': 'float'}
223
+
224
+ # Get all tags in a dataset
225
+ all_tags = get_all_tags_in_dataset(dicom_metadata)
226
+ ```
227
+
@@ -0,0 +1,191 @@
1
+ # dicompare
2
+
3
+ [![](img/button.png)](https://dicompare-web.vercel.app/)
4
+
5
+ dicompare is a DICOM validation tool designed to ensure compliance with study-specific imaging protocols and domain-specific guidelines while preserving data privacy. It provides multiple interfaces, including support for validation directly in the browser at [dicompare-web.vercel.app](https://dicompare-web.vercel.app/), leveraging WebAssembly (WASM), Pyodide, and the underlying pip package `dicompare`. dicompare is suitable for multi-site studies and clinical environments without requiring software installation or external data uploads.
6
+
7
+ dicompare supports DICOM session validation against templates based on:
8
+
9
+ - **Reference sessions**: JSON schema files can be generated based on a reference MRI scanning session;
10
+ - **[TESTING] domain guidelines**: Flexible guidelines for specific domains (currently [QSM](https://doi.org/10.1002/mrm.30006));
11
+ - **[FUTURE] landmark studies**: Schema files based on landmark studies such as the [HCP](https://doi.org/10.1038/s41586-018-0579-z), [ABCD](https://doi.org/10.1016/j.dcn.2018.03.001), and [UK BioBank](https://doi.org/10.1038/s41586-018-0579-z) projects.
12
+
13
+ # Command-line interface (CLI) and application programming interface (API)
14
+
15
+ While you can run [dicompare](https://dicompare-web.vercel.app/) in your browser now without any installation, you may also use the underlying `dicompare` pip package if you wish to use the command-line interface (CLI) or application programming interface (API).
16
+
17
+ ```bash
18
+ pip install dicompare
19
+ ```
20
+
21
+ ## Command-line interface (CLI)
22
+
23
+ The package provides a unified `dicompare` command with two subcommands:
24
+
25
+ - **`dicompare build`**: Generate a JSON schema from a reference DICOM session
26
+ - **`dicompare check`**: Validate DICOM sessions against a JSON schema
27
+
28
+ ### 1. Build a JSON schema from a reference session
29
+
30
+ ```bash
31
+ dicompare build /path/to/dicom/session schema.json
32
+ ```
33
+
34
+ This creates a JSON schema describing the session based on default reference fields present in the data.
35
+
36
+ ### 2. Check a DICOM session against a schema
37
+
38
+ ```bash
39
+ dicompare check /path/to/dicom/session schema.json
40
+ ```
41
+
42
+ The tool will output a compliance summary, indicating deviations from the schema.
43
+
44
+ ### 3. Check with report output
45
+
46
+ ```bash
47
+ dicompare check /path/to/dicom/session schema.json compliance_report.json
48
+ ```
49
+
50
+ This saves the compliance report to a JSON file.
51
+
52
+ ### 4. Automatic acquisition mapping
53
+
54
+ ```bash
55
+ dicompare check /path/to/dicom/session schema.json --auto-yes
56
+ ```
57
+
58
+ Use `--auto-yes` or `-y` to automatically map acquisitions without interactive prompts.
59
+
60
+ ## Python API
61
+
62
+ The `dicompare` package provides a comprehensive Python API for programmatic schema generation, validation, and DICOM processing.
63
+
64
+ ### Loading DICOM data
65
+
66
+ **Load a DICOM session:**
67
+
68
+ ```python
69
+ from dicompare import load_dicom_session
70
+
71
+ session_df = load_dicom_session(
72
+ session_dir="/path/to/dicom/session",
73
+ show_progress=True
74
+ )
75
+ ```
76
+
77
+ **Load individual DICOM files:**
78
+
79
+ ```python
80
+ from dicompare import load_dicom
81
+
82
+ dicom_data = load_dicom(
83
+ dicom_paths=["/path/to/file1.dcm", "/path/to/file2.dcm"],
84
+ show_progress=True
85
+ )
86
+ ```
87
+
88
+ **Load Siemens .pro files:**
89
+
90
+ ```python
91
+ from dicompare import load_pro_session
92
+
93
+ pro_session = load_pro_session(
94
+ session_dir="/path/to/pro/files",
95
+ show_progress=True
96
+ )
97
+ ```
98
+
99
+ ### Build a JSON schema
100
+
101
+ ```python
102
+ from dicompare import load_dicom_session, build_schema, make_json_serializable
103
+ from dicompare.config import DEFAULT_SETTINGS_FIELDS
104
+ import json
105
+
106
+ # Load the reference session
107
+ session_df = load_dicom_session(
108
+ session_dir="/path/to/dicom/session",
109
+ show_progress=True
110
+ )
111
+
112
+ # Build the schema
113
+ json_schema = build_schema(session_df)
114
+
115
+ # Save the schema
116
+ serializable_schema = make_json_serializable(json_schema)
117
+ with open("schema.json", "w") as f:
118
+ json.dump(serializable_schema, f, indent=4)
119
+ ```
120
+
121
+ ### Validate a session against a JSON schema
122
+
123
+ ```python
124
+ from dicompare import (
125
+ load_schema,
126
+ load_dicom_session,
127
+ check_acquisition_compliance,
128
+ map_to_json_reference,
129
+ assign_acquisition_and_run_numbers
130
+ )
131
+
132
+ # Load the JSON schema
133
+ reference_fields, json_schema, validation_rules = load_schema(json_schema_path="schema.json")
134
+
135
+ # Load the input session
136
+ in_session = load_dicom_session(
137
+ session_dir="/path/to/dicom/session",
138
+ show_progress=True
139
+ )
140
+
141
+ # Assign acquisition and run numbers
142
+ in_session = assign_acquisition_and_run_numbers(in_session)
143
+
144
+ # Map acquisitions to schema
145
+ session_map = map_to_json_reference(in_session, json_schema)
146
+
147
+ # Check compliance for each acquisition
148
+ compliance_summary = []
149
+ for ref_acq_name, schema_acq in json_schema["acquisitions"].items():
150
+ if ref_acq_name not in session_map:
151
+ continue
152
+
153
+ input_acq_name = session_map[ref_acq_name]
154
+ acq_validation_rules = validation_rules.get(ref_acq_name) if validation_rules else None
155
+
156
+ results = check_acquisition_compliance(
157
+ in_session,
158
+ schema_acq,
159
+ acquisition_name=input_acq_name,
160
+ validation_rules=acq_validation_rules
161
+ )
162
+ compliance_summary.extend(results)
163
+
164
+ # Display results
165
+ for entry in compliance_summary:
166
+ print(entry)
167
+ ```
168
+
169
+ ### Additional utilities
170
+
171
+ **Assign acquisition and run numbers:**
172
+
173
+ ```python
174
+ from dicompare import assign_acquisition_and_run_numbers
175
+
176
+ session_df = assign_acquisition_and_run_numbers(session_df)
177
+ ```
178
+
179
+ **Get DICOM tag information:**
180
+
181
+ ```python
182
+ from dicompare import get_tag_info, get_all_tags_in_dataset
183
+
184
+ # Get info about a specific tag
185
+ tag_info = get_tag_info("EchoTime")
186
+ print(tag_info) # {'tag': '(0018,0081)', 'name': 'Echo Time', 'type': 'float'}
187
+
188
+ # Get all tags in a dataset
189
+ all_tags = get_all_tags_in_dataset(dicom_metadata)
190
+ ```
191
+
@@ -0,0 +1,18 @@
1
+ __version__ = "0.1.38"
2
+
3
+ # Import core functionalities
4
+ from .io import get_dicom_values, load_dicom, load_schema, load_dicom_session, async_load_dicom_session, load_nifti_session, load_pro_file, load_pro_session, generate_test_dicoms_from_schema, generate_test_dicoms_from_schema_json, load_pro_file_schema_format
5
+ from .validation import check_acquisition_compliance
6
+ from .session import assign_acquisition_and_run_numbers
7
+ from .session import map_to_json_reference, interactive_mapping_to_json_reference
8
+ from .validation import BaseValidationModel, ValidationError, ValidationWarning, validator, safe_exec_rule, create_validation_model_from_rules, create_validation_models_from_rules
9
+ from .config import DEFAULT_SETTINGS_FIELDS, DEFAULT_ACQUISITION_FIELDS, DEFAULT_DICOM_FIELDS
10
+ from .schema import get_tag_info, get_all_tags_in_dataset
11
+
12
+ # Import enhanced functionality for web interfaces
13
+ from .schema import build_schema
14
+ from .io import make_json_serializable
15
+ from .utils import detect_constant_fields, clean_string, make_hashable
16
+ from .interface import (
17
+ analyze_dicom_files_for_web,
18
+ )
File without changes
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env python
2
+
3
+ import sys
4
+ import json
5
+ import argparse
6
+ import logging
7
+ import pandas as pd
8
+
9
+ from dicompare.io import load_dicom_session, load_schema
10
+ from dicompare.io.json import make_json_serializable
11
+ from dicompare.schema import build_schema
12
+ from dicompare.validation import check_acquisition_compliance
13
+ from dicompare.validation.helpers import ComplianceStatus, create_compliance_record
14
+ from dicompare.session import map_to_json_reference, interactive_mapping_to_json_reference, assign_acquisition_and_run_numbers
15
+
16
+ # Set up logging
17
+ logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def build_command(args) -> None:
22
+ """Generate a JSON schema from a DICOM session."""
23
+ # Read DICOM session
24
+ session_data = load_dicom_session(
25
+ session_dir=args.dicoms,
26
+ show_progress=True
27
+ )
28
+
29
+ # Generate JSON schema
30
+ json_schema = build_schema(session_df=session_data)
31
+
32
+ # Write JSON to output file
33
+ serializable_schema = make_json_serializable(json_schema)
34
+ with open(args.schema, "w") as f:
35
+ json.dump(serializable_schema, f, indent=4)
36
+ logger.info(f"JSON schema saved to {args.schema}")
37
+
38
+
39
+ def check_command(args) -> None:
40
+ """Check a DICOM session against a schema for compliance."""
41
+ # Load the schema
42
+ reference_fields, json_schema, validation_rules = load_schema(json_schema_path=args.schema)
43
+
44
+ # Load the input session
45
+ in_session = load_dicom_session(
46
+ session_dir=args.dicoms,
47
+ )
48
+
49
+ # Assign acquisition and series using canonical process
50
+ in_session = assign_acquisition_and_run_numbers(in_session)
51
+
52
+ # Map and perform compliance check
53
+ session_map = map_to_json_reference(in_session, json_schema)
54
+ if not args.auto_yes and sys.stdin.isatty():
55
+ session_map = interactive_mapping_to_json_reference(in_session, json_schema, initial_mapping=session_map)
56
+
57
+ # Check compliance for each acquisition
58
+ compliance_summary = []
59
+ for ref_acq_name, schema_acq in json_schema["acquisitions"].items():
60
+ if ref_acq_name not in session_map:
61
+ compliance_summary.append(create_compliance_record(
62
+ field="Acquisition Mapping",
63
+ message=f"Reference acquisition '{ref_acq_name}' is not mapped to any input acquisition.",
64
+ status=ComplianceStatus.ERROR,
65
+ expected=f"Acquisition '{ref_acq_name}' to be mapped"
66
+ ))
67
+ continue
68
+
69
+ input_acq_name = session_map[ref_acq_name]
70
+ acq_validation_rules = validation_rules.get(ref_acq_name) if validation_rules else None
71
+ results = check_acquisition_compliance(
72
+ in_session,
73
+ schema_acq,
74
+ acquisition_name=input_acq_name,
75
+ validation_rules=acq_validation_rules
76
+ )
77
+ compliance_summary.extend(results)
78
+ compliance_df = pd.DataFrame(compliance_summary)
79
+
80
+ # If compliance_df is empty, log message and exit
81
+ if compliance_df.empty:
82
+ logger.info("Session is fully compliant with the schema model.")
83
+ return
84
+
85
+ # Inline summary output
86
+ for entry in compliance_summary:
87
+ if entry.get('input acquisition'):
88
+ acq_text = f"Acquisition: {entry.get('input acquisition')}"
89
+ if entry.get('reference acquisition'):
90
+ acq_text += f" ({entry.get('reference acquisition')})"
91
+ logger.info(acq_text)
92
+ # Handle 'field' (single) or derive from 'expected' keys
93
+ if entry.get('field'):
94
+ logger.info(f"Field: {entry.get('field')}")
95
+ elif entry.get('expected') and isinstance(entry.get('expected'), dict):
96
+ logger.info(f"Fields: {list(entry.get('expected').keys())}")
97
+ if entry.get('series'): logger.info(f"Series: {entry.get('series')}")
98
+ if entry.get('expected') is not None: logger.info(f"Expected: {entry.get('expected')}")
99
+ if entry.get('value') is not None: logger.info(f"Value: {entry.get('value')}")
100
+ if entry.get('message'): logger.info(f"Message: {entry.get('message')}")
101
+ logger.info("-" * 40)
102
+
103
+ # Save compliance summary to JSON
104
+ if args.report:
105
+ with open(args.report, "w") as f:
106
+ json.dump(compliance_summary, f, indent=4)
107
+ logger.info(f"Compliance report saved to {args.report}")
108
+
109
+
110
+ def main() -> None:
111
+ parser = argparse.ArgumentParser(
112
+ description="DICOM compliance validation tool",
113
+ prog="dicompare"
114
+ )
115
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
116
+
117
+ # Build subcommand
118
+ build_parser = subparsers.add_parser(
119
+ "build",
120
+ help="Build a JSON schema from a DICOM session"
121
+ )
122
+ build_parser.add_argument(
123
+ "dicoms",
124
+ nargs="?",
125
+ help="Directory containing DICOM files"
126
+ )
127
+ build_parser.add_argument(
128
+ "schema",
129
+ nargs="?",
130
+ help="Output path for the JSON schema"
131
+ )
132
+ build_parser.add_argument(
133
+ "--dicoms",
134
+ dest="dicoms_named",
135
+ metavar="PATH",
136
+ help="Directory containing DICOM files"
137
+ )
138
+ build_parser.add_argument(
139
+ "--schema",
140
+ dest="schema_named",
141
+ metavar="PATH",
142
+ help="Output path for the JSON schema"
143
+ )
144
+ build_parser.add_argument(
145
+ "--name-template",
146
+ default="{ProtocolName}",
147
+ help="Naming template for acquisitions (default: {ProtocolName})"
148
+ )
149
+
150
+ # Check subcommand
151
+ check_parser = subparsers.add_parser(
152
+ "check",
153
+ help="Check a DICOM session against a schema"
154
+ )
155
+ check_parser.add_argument(
156
+ "dicoms",
157
+ nargs="?",
158
+ help="Directory containing DICOM files to check"
159
+ )
160
+ check_parser.add_argument(
161
+ "schema",
162
+ nargs="?",
163
+ help="Path to the JSON schema file"
164
+ )
165
+ check_parser.add_argument(
166
+ "report",
167
+ nargs="?",
168
+ help="Output path for the compliance report"
169
+ )
170
+ check_parser.add_argument(
171
+ "--dicoms",
172
+ dest="dicoms_named",
173
+ metavar="PATH",
174
+ help="Directory containing DICOM files to check"
175
+ )
176
+ check_parser.add_argument(
177
+ "--schema",
178
+ dest="schema_named",
179
+ metavar="PATH",
180
+ help="Path to the JSON schema file"
181
+ )
182
+ check_parser.add_argument(
183
+ "--report",
184
+ dest="report_named",
185
+ metavar="PATH",
186
+ help="Output path for the compliance report"
187
+ )
188
+ check_parser.add_argument(
189
+ "--auto-yes", "-y",
190
+ action="store_true",
191
+ help="Automatically map acquisitions without prompting"
192
+ )
193
+
194
+ args = parser.parse_args()
195
+
196
+ if not args.command:
197
+ parser.print_help()
198
+ sys.exit(1)
199
+
200
+ # Resolve positional vs named arguments
201
+ if args.command == "build":
202
+ args.dicoms = args.dicoms or args.dicoms_named
203
+ args.schema = args.schema or args.schema_named
204
+
205
+ if not args.dicoms or not args.schema:
206
+ build_parser.error("the following arguments are required: dicoms, schema")
207
+
208
+ build_command(args)
209
+
210
+ elif args.command == "check":
211
+ args.dicoms = args.dicoms or args.dicoms_named
212
+ args.schema = args.schema or args.schema_named
213
+ args.report = args.report or args.report_named
214
+
215
+ if not args.dicoms or not args.schema:
216
+ check_parser.error("the following arguments are required: dicoms, schema")
217
+
218
+ check_command(args)
219
+
220
+
221
+ if __name__ == "__main__":
222
+ main()