dicompare 0.1.8__py3-none-any.whl
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.
- dicompare/__init__.py +7 -0
- dicompare/cli/__init__.py +0 -0
- dicompare/cli/check_session.py +93 -0
- dicompare/cli/gen_session.py +110 -0
- dicompare/cli/start_web.py +18 -0
- dicompare/compliance.py +343 -0
- dicompare/io.py +264 -0
- dicompare/mapping.py +578 -0
- dicompare/tests/__init__.py +0 -0
- dicompare/tests/fixtures/__init__.py +0 -0
- dicompare/tests/fixtures/fixtures.py +88 -0
- dicompare/tests/fixtures/ref_empty.py +0 -0
- dicompare/tests/fixtures/ref_qsm.py +163 -0
- dicompare/tests/test_io.py +202 -0
- dicompare/tests/test_ref_dicom.py +36 -0
- dicompare/utils.py +130 -0
- dicompare/validation.py +241 -0
- dicompare-0.1.8.dist-info/LICENSE +21 -0
- dicompare-0.1.8.dist-info/METADATA +122 -0
- dicompare-0.1.8.dist-info/RECORD +23 -0
- dicompare-0.1.8.dist-info/WHEEL +5 -0
- dicompare-0.1.8.dist-info/entry_points.txt +4 -0
- dicompare-0.1.8.dist-info/top_level.txt +1 -0
dicompare/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
__version__ = "0.1.8"
|
|
2
|
+
|
|
3
|
+
# Import core functionalities
|
|
4
|
+
from .io import get_dicom_values, load_dicom, load_json_session, load_dicom_session, load_python_session
|
|
5
|
+
from .compliance import check_session_compliance_with_json_reference, check_session_compliance_with_python_module, check_dicom_compliance, is_session_compliant, is_dicom_compliant
|
|
6
|
+
from .mapping import map_to_json_reference, interactive_mapping_to_json_reference, interactive_mapping_to_python_reference
|
|
7
|
+
from .validation import BaseValidationModel, ValidationError, validator
|
|
File without changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import argparse
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from dicompare.io import load_json_session, load_python_session, load_dicom_session
|
|
7
|
+
from dicompare.compliance import check_session_compliance_with_json_reference, check_session_compliance_with_python_module
|
|
8
|
+
from dicompare.mapping import map_to_json_reference, interactive_mapping_to_json_reference, interactive_mapping_to_python_reference
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
parser = argparse.ArgumentParser(description="Generate compliance summaries for a DICOM session.")
|
|
12
|
+
parser.add_argument("--json_ref", help="Path to the JSON reference file.")
|
|
13
|
+
parser.add_argument("--python_ref", help="Path to the Python module containing validation models.")
|
|
14
|
+
parser.add_argument("--in_session", required=True, help="Directory path for the DICOM session.")
|
|
15
|
+
parser.add_argument("--out_json", default="compliance_report.json", help="Path to save the JSON compliance summary report.")
|
|
16
|
+
parser.add_argument("--auto_yes", action="store_true", help="Automatically map acquisitions to series.")
|
|
17
|
+
args = parser.parse_args()
|
|
18
|
+
|
|
19
|
+
if not (args.json_ref or args.python_ref):
|
|
20
|
+
raise ValueError("You must provide either --json_ref or --python_ref.")
|
|
21
|
+
|
|
22
|
+
# Load the reference models and fields
|
|
23
|
+
if args.json_ref:
|
|
24
|
+
acquisition_fields, reference_fields, ref_session = load_json_session(json_ref=args.json_ref)
|
|
25
|
+
elif args.python_ref:
|
|
26
|
+
ref_models = load_python_session(module_path=args.python_ref)
|
|
27
|
+
acquisition_fields = ["ProtocolName"]
|
|
28
|
+
|
|
29
|
+
# Load the input session
|
|
30
|
+
in_session = load_dicom_session(
|
|
31
|
+
session_dir=args.in_session,
|
|
32
|
+
acquisition_fields=acquisition_fields,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if args.json_ref:
|
|
36
|
+
# Group by all existing unique combinations of reference fields
|
|
37
|
+
in_session = (
|
|
38
|
+
in_session.groupby(reference_fields)
|
|
39
|
+
.apply(lambda x: x.reset_index(drop=True))
|
|
40
|
+
.reset_index(drop=True) # Reset the index to avoid index/column ambiguity
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Assign unique group numbers for each combination of reference fields
|
|
44
|
+
in_session["Series"] = (
|
|
45
|
+
in_session.groupby(reference_fields, dropna=False).ngroup().add(1).apply(lambda x: f"Series {x}")
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if args.json_ref:
|
|
49
|
+
session_map = map_to_json_reference(in_session, ref_session)
|
|
50
|
+
if not args.auto_yes and sys.stdin.isatty():
|
|
51
|
+
session_map = interactive_mapping_to_json_reference(in_session, ref_session, initial_mapping=session_map)
|
|
52
|
+
else:
|
|
53
|
+
session_map = interactive_mapping_to_python_reference(in_session, ref_models)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Perform compliance check
|
|
57
|
+
if args.json_ref:
|
|
58
|
+
compliance_summary = check_session_compliance_with_json_reference(
|
|
59
|
+
in_session=in_session,
|
|
60
|
+
ref_session=ref_session,
|
|
61
|
+
session_map=session_map
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
compliance_summary = check_session_compliance_with_python_module(
|
|
65
|
+
in_session=in_session,
|
|
66
|
+
ref_models=ref_models,
|
|
67
|
+
session_map=session_map
|
|
68
|
+
)
|
|
69
|
+
compliance_df = pd.DataFrame(compliance_summary)
|
|
70
|
+
|
|
71
|
+
# If compliance_df is empty, print message and exit
|
|
72
|
+
if compliance_df.empty:
|
|
73
|
+
print("Session is fully compliant with the reference model.")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Inline summary output
|
|
77
|
+
for entry in compliance_summary:
|
|
78
|
+
if entry.get('acquisition'): print(f"Acquisition: {entry.get('acquisition')}")
|
|
79
|
+
if entry.get('field'): print(f"Field: {entry.get('field')}")
|
|
80
|
+
if entry.get('value'): print(f"Value: {entry.get('value')}")
|
|
81
|
+
if entry.get('rule'): print(f"Rule: {entry.get('rule')}")
|
|
82
|
+
if entry.get('message'): print(f"Message: {entry.get('message')}")
|
|
83
|
+
if entry.get('passed'): print(f"Passed: {entry.get('passed')}")
|
|
84
|
+
print("-" * 40)
|
|
85
|
+
|
|
86
|
+
# Save compliance summary to JSON
|
|
87
|
+
if args.out_json:
|
|
88
|
+
with open(args.out_json, "w") as f:
|
|
89
|
+
json.dump(compliance_summary, f)
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
main()
|
|
93
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from dicompare.io import load_dicom_session
|
|
7
|
+
from dicompare.utils import clean_string
|
|
8
|
+
|
|
9
|
+
def make_hashable(value):
|
|
10
|
+
"""
|
|
11
|
+
Convert a value into a hashable format.
|
|
12
|
+
Handles lists, dictionaries, and other non-hashable types.
|
|
13
|
+
"""
|
|
14
|
+
if isinstance(value, list):
|
|
15
|
+
return tuple(value)
|
|
16
|
+
elif isinstance(value, dict):
|
|
17
|
+
return tuple((k, make_hashable(v)) for k, v in value.items())
|
|
18
|
+
elif isinstance(value, set):
|
|
19
|
+
return tuple(sorted(make_hashable(v) for v in value))
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_json_reference(session_df, acquisition_fields, reference_fields, name_template="{ProtocolName}"):
|
|
24
|
+
"""
|
|
25
|
+
Create a JSON reference from the session DataFrame.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
session_df (pd.DataFrame): DataFrame of the DICOM session.
|
|
29
|
+
acquisition_fields (List[str]): Fields to uniquely identify each acquisition.
|
|
30
|
+
reference_fields (List[str]): Fields to include in JSON reference.
|
|
31
|
+
name_template (str): Naming template for acquisitions/series.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict: JSON structure representing the reference.
|
|
35
|
+
"""
|
|
36
|
+
# Ensure all values in the DataFrame are hashable
|
|
37
|
+
for col in session_df.columns:
|
|
38
|
+
session_df[col] = session_df[col].apply(make_hashable)
|
|
39
|
+
|
|
40
|
+
json_reference = {"acquisitions": {}}
|
|
41
|
+
|
|
42
|
+
# Group by acquisition
|
|
43
|
+
for acquisition_name, group in session_df.groupby("Acquisition"):
|
|
44
|
+
acquisition_entry = {"fields": [], "series": []}
|
|
45
|
+
|
|
46
|
+
# Add acquisition-level fields
|
|
47
|
+
for field in acquisition_fields:
|
|
48
|
+
unique_values = group[field].dropna().unique()
|
|
49
|
+
if len(unique_values) == 1:
|
|
50
|
+
acquisition_entry["fields"].append({"field": field, "value": unique_values[0]})
|
|
51
|
+
|
|
52
|
+
# Group by series within each acquisition
|
|
53
|
+
series_fields = list(set(reference_fields) - set(acquisition_fields))
|
|
54
|
+
if series_fields:
|
|
55
|
+
series_groups = group.groupby(series_fields, dropna=False)
|
|
56
|
+
|
|
57
|
+
for i, (series_key, series_group) in enumerate(series_groups, start=1):
|
|
58
|
+
series_entry = {
|
|
59
|
+
"name": f"Series {i}",
|
|
60
|
+
"fields": [{"field": field, "value": series_key[j]} for j, field in enumerate(series_fields)]
|
|
61
|
+
}
|
|
62
|
+
acquisition_entry["series"].append(series_entry)
|
|
63
|
+
|
|
64
|
+
# Exclude reference fields from acquisition-level fields if they appear in series
|
|
65
|
+
acquisition_entry["fields"] = [
|
|
66
|
+
field for field in acquisition_entry["fields"] if field["field"] not in reference_fields
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# Add to JSON reference
|
|
70
|
+
json_reference["acquisitions"][clean_string(acquisition_name)] = acquisition_entry
|
|
71
|
+
|
|
72
|
+
return json_reference
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main():
|
|
77
|
+
parser = argparse.ArgumentParser(description="Generate a JSON reference for DICOM compliance.")
|
|
78
|
+
parser.add_argument("--in_session_dir", required=True, help="Directory containing DICOM files for the session.")
|
|
79
|
+
parser.add_argument("--out_json_ref", required=True, help="Path to save the generated JSON reference.")
|
|
80
|
+
parser.add_argument("--acquisition_fields", nargs="+", required=True, help="Fields to uniquely identify each acquisition.")
|
|
81
|
+
parser.add_argument("--reference_fields", nargs="+", required=True, help="Fields to include in JSON reference with their values.")
|
|
82
|
+
parser.add_argument("--name_template", default="{ProtocolName}", help="Naming template for each acquisition series.")
|
|
83
|
+
args = parser.parse_args()
|
|
84
|
+
|
|
85
|
+
# Read DICOM session
|
|
86
|
+
session_data = load_dicom_session(
|
|
87
|
+
session_dir=args.in_session_dir,
|
|
88
|
+
acquisition_fields=args.acquisition_fields,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Filter fields in DataFrame
|
|
92
|
+
relevant_fields = set(args.acquisition_fields + args.reference_fields)
|
|
93
|
+
session_data = session_data[list(relevant_fields.intersection(session_data.columns)) + ["Acquisition"]]
|
|
94
|
+
|
|
95
|
+
# Generate JSON reference
|
|
96
|
+
json_reference = create_json_reference(
|
|
97
|
+
session_df=session_data,
|
|
98
|
+
acquisition_fields=args.acquisition_fields,
|
|
99
|
+
reference_fields=args.reference_fields,
|
|
100
|
+
name_template=args.name_template,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Write JSON to output file
|
|
104
|
+
with open(args.out_json_ref, "w") as f:
|
|
105
|
+
json.dump(json_reference, f, indent=4)
|
|
106
|
+
print(f"JSON reference saved to {args.out_json_ref}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import webbrowser
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
# Get the package base directory using `importlib.resources`
|
|
7
|
+
package_dir = files("dicompare").joinpath("docs", "index.html")
|
|
8
|
+
|
|
9
|
+
# Convert the resource path to an absolute file path
|
|
10
|
+
docs_path = str(package_dir)
|
|
11
|
+
|
|
12
|
+
# Check if the file exists
|
|
13
|
+
if not os.path.exists(docs_path):
|
|
14
|
+
print(f"Error: Documentation not found at {docs_path}.")
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
# Open the documentation in the default web browser
|
|
18
|
+
webbrowser.open(f"file://{docs_path}")
|
dicompare/compliance.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides functions for validating a DICOM sessions.
|
|
3
|
+
|
|
4
|
+
The module supports compliance checks for JSON-based reference sessions and Python module-based validation models.
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Dict, Any, Tuple
|
|
9
|
+
from dicompare.validation import BaseValidationModel
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
def check_session_compliance_with_json_reference(
|
|
13
|
+
in_session: pd.DataFrame,
|
|
14
|
+
ref_session: Dict[str, Any],
|
|
15
|
+
session_map: Dict[Tuple[str, str], Tuple[str, str]]
|
|
16
|
+
) -> List[Dict[str, Any]]:
|
|
17
|
+
"""
|
|
18
|
+
Validate a DICOM session against a JSON reference session.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
in_session (pd.DataFrame): Input session DataFrame containing DICOM metadata.
|
|
22
|
+
ref_session (Dict[str, Any]): Reference session data loaded from a JSON file.
|
|
23
|
+
session_map (Dict[Tuple[str, str], Tuple[str, str]]): Mapping of input acquisitions/series
|
|
24
|
+
to reference acquisitions/series.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List[Dict[str, Any]]: A list of compliance issues, where each issue is represented as a dictionary.
|
|
28
|
+
"""
|
|
29
|
+
compliance_summary = []
|
|
30
|
+
|
|
31
|
+
# Iterate over the session mapping
|
|
32
|
+
for (in_acq_name, in_series_name), (ref_acq_name, ref_series_name) in session_map.items():
|
|
33
|
+
# Filter the input session for the current acquisition and series
|
|
34
|
+
in_acq_series = in_session[
|
|
35
|
+
(in_session["Acquisition"] == in_acq_name) &
|
|
36
|
+
(in_session["Series"] == in_series_name)
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
if in_acq_series.empty:
|
|
40
|
+
compliance_summary.append({
|
|
41
|
+
"reference acquisition": (ref_acq_name, ref_series_name),
|
|
42
|
+
"input acquisition": (in_acq_name, in_series_name),
|
|
43
|
+
"field": "Acquisition-Level Error",
|
|
44
|
+
"value": None,
|
|
45
|
+
"rule": "Input acquisition and series must be present.",
|
|
46
|
+
"message": "Input acquisition or series not found.",
|
|
47
|
+
"passed": "❌"
|
|
48
|
+
})
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
# Filter the reference session for the current acquisition and series
|
|
52
|
+
ref_acq = ref_session["acquisitions"].get(ref_acq_name, {})
|
|
53
|
+
ref_series = next(
|
|
54
|
+
(series for series in ref_acq.get("series", []) if series["name"] == ref_series_name),
|
|
55
|
+
None
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if not ref_series:
|
|
59
|
+
compliance_summary.append({
|
|
60
|
+
"reference acquisition": (ref_acq_name, ref_series_name),
|
|
61
|
+
"input acquisition": (in_acq_name, in_series_name),
|
|
62
|
+
"field": "Reference-Level Error",
|
|
63
|
+
"value": None,
|
|
64
|
+
"rule": "Reference acquisition and series must be present.",
|
|
65
|
+
"message": "Reference acquisition or series not found.",
|
|
66
|
+
"passed": "❌"
|
|
67
|
+
})
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
# Iterate through the reference fields and check compliance
|
|
71
|
+
for ref_field in ref_series.get("fields", []):
|
|
72
|
+
field_name = ref_field["field"]
|
|
73
|
+
expected_value = ref_field.get("value")
|
|
74
|
+
tolerance = ref_field.get("tolerance")
|
|
75
|
+
contains = ref_field.get("contains")
|
|
76
|
+
|
|
77
|
+
# Check the corresponding field in the input session DataFrame
|
|
78
|
+
if field_name not in in_acq_series.columns:
|
|
79
|
+
compliance_summary.append({
|
|
80
|
+
"reference acquisition": (ref_acq_name, ref_series_name),
|
|
81
|
+
"input acquisition": (in_acq_name, in_series_name),
|
|
82
|
+
"field": field_name,
|
|
83
|
+
"value": None,
|
|
84
|
+
"rule": "Field must be present.",
|
|
85
|
+
"message": "Field not found in input session.",
|
|
86
|
+
"passed": "❌"
|
|
87
|
+
})
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
actual_value = in_acq_series[field_name].iloc[0]
|
|
91
|
+
|
|
92
|
+
# Contains check
|
|
93
|
+
if contains is not None:
|
|
94
|
+
if not isinstance(actual_value, list) or contains not in actual_value:
|
|
95
|
+
compliance_summary.append({
|
|
96
|
+
"reference acquisition": (ref_acq_name, ref_series_name),
|
|
97
|
+
"input acquisition": (in_acq_name, in_series_name),
|
|
98
|
+
"field": field_name,
|
|
99
|
+
"value": actual_value,
|
|
100
|
+
"rule": "Field must contain value.",
|
|
101
|
+
"message": f"Expected to contain {contains}, got {actual_value}.",
|
|
102
|
+
"passed": "❌"
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
# Tolerance check
|
|
106
|
+
elif tolerance is not None and isinstance(actual_value, (int, float)):
|
|
107
|
+
if not (expected_value - tolerance <= actual_value <= expected_value + tolerance):
|
|
108
|
+
compliance_summary.append({
|
|
109
|
+
"reference acquisition": (ref_acq_name, ref_series_name),
|
|
110
|
+
"input acquisition": (in_acq_name, in_series_name),
|
|
111
|
+
"field": field_name,
|
|
112
|
+
"value": actual_value,
|
|
113
|
+
"rule": "Field must be within tolerance.",
|
|
114
|
+
"message": f"Expected {expected_value} ± {tolerance}, got {actual_value}.",
|
|
115
|
+
"passed": "❌"
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
# Exact match check
|
|
119
|
+
elif expected_value is not None and actual_value != expected_value:
|
|
120
|
+
compliance_summary.append({
|
|
121
|
+
"reference acquisition": (ref_acq_name, ref_series_name),
|
|
122
|
+
"input acquisition": (in_acq_name, in_series_name),
|
|
123
|
+
"field": field_name,
|
|
124
|
+
"value": actual_value,
|
|
125
|
+
"rule": "Field must match expected value.",
|
|
126
|
+
"message": f"Expected {expected_value}, got {actual_value}.",
|
|
127
|
+
"passed": "❌"
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
return compliance_summary
|
|
131
|
+
|
|
132
|
+
def check_session_compliance_with_python_module(
|
|
133
|
+
in_session: pd.DataFrame,
|
|
134
|
+
ref_models: Dict[str, BaseValidationModel],
|
|
135
|
+
session_map: Dict[str, str],
|
|
136
|
+
raise_errors: bool = False
|
|
137
|
+
) -> List[Dict[str, Any]]:
|
|
138
|
+
"""
|
|
139
|
+
Validate a DICOM session against Python module-based validation models.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
in_session (pd.DataFrame): Input session DataFrame containing DICOM metadata.
|
|
143
|
+
ref_models (Dict[str, BaseValidationModel]): Dictionary mapping acquisition names to
|
|
144
|
+
validation models.
|
|
145
|
+
session_map (Dict[str, str]): Mapping of reference acquisitions to input acquisitions.
|
|
146
|
+
raise_errors (bool): Whether to raise exceptions for validation failures. Defaults to False.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
List[Dict[str, Any]]: A list of compliance issues, where each issue is represented as a dictionary.
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
ValueError: If `raise_errors` is True and validation fails for any acquisition.
|
|
153
|
+
"""
|
|
154
|
+
compliance_summary = []
|
|
155
|
+
|
|
156
|
+
for ref_acq_name, in_acq_name in session_map.items():
|
|
157
|
+
# Filter the input session for the current acquisition
|
|
158
|
+
in_acq = in_session[in_session["Acquisition"] == in_acq_name]
|
|
159
|
+
|
|
160
|
+
if in_acq.empty:
|
|
161
|
+
compliance_summary.append({
|
|
162
|
+
"reference acquisition": ref_acq_name,
|
|
163
|
+
"input acquisition": in_acq_name,
|
|
164
|
+
"field": "Acquisition-Level Error",
|
|
165
|
+
"value": None,
|
|
166
|
+
"rule": "Input acquisition must be present.",
|
|
167
|
+
"message": f"Input acquisition '{in_acq_name}' not found.",
|
|
168
|
+
"passed": "❌"
|
|
169
|
+
})
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
# Retrieve reference model
|
|
173
|
+
ref_model_cls = ref_models.get(ref_acq_name)
|
|
174
|
+
if not ref_model_cls:
|
|
175
|
+
compliance_summary.append({
|
|
176
|
+
"reference acquisition": ref_acq_name,
|
|
177
|
+
"input acquisition": in_acq_name,
|
|
178
|
+
"field": "Model Error",
|
|
179
|
+
"value": None,
|
|
180
|
+
"rule": "Reference model must exist.",
|
|
181
|
+
"message": f"No model found for reference acquisition '{ref_acq_name}'.",
|
|
182
|
+
"passed": "❌"
|
|
183
|
+
})
|
|
184
|
+
continue
|
|
185
|
+
ref_model = ref_model_cls()
|
|
186
|
+
|
|
187
|
+
# Prepare acquisition data as a single DataFrame
|
|
188
|
+
acquisition_df = in_acq.copy()
|
|
189
|
+
|
|
190
|
+
# Validate using the reference model
|
|
191
|
+
success, errors, passes = ref_model.validate(data=acquisition_df)
|
|
192
|
+
|
|
193
|
+
# Record errors
|
|
194
|
+
for error in errors:
|
|
195
|
+
compliance_summary.append({
|
|
196
|
+
"reference acquisition": ref_acq_name,
|
|
197
|
+
"reference series": None,
|
|
198
|
+
"input acquisition": in_acq_name,
|
|
199
|
+
"input series": None,
|
|
200
|
+
"field": error['field'],
|
|
201
|
+
"value": error['value'],
|
|
202
|
+
"rule": error['rule'],
|
|
203
|
+
"message": error['message'],
|
|
204
|
+
"passed": "❌"
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
# Record passes
|
|
208
|
+
for passed_test in passes:
|
|
209
|
+
compliance_summary.append({
|
|
210
|
+
"reference acquisition": ref_acq_name,
|
|
211
|
+
"reference series": None,
|
|
212
|
+
"input acquisition": in_acq_name,
|
|
213
|
+
"input series": None,
|
|
214
|
+
"field": passed_test['field'],
|
|
215
|
+
"value": passed_test['value'],
|
|
216
|
+
"rule": passed_test['rule'],
|
|
217
|
+
"message": passed_test['message'],
|
|
218
|
+
"passed": "✅"
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
# Raise an error if validation fails and `raise_errors` is True
|
|
222
|
+
if raise_errors and not success:
|
|
223
|
+
raise ValueError(f"Validation failed for acquisition '{in_acq_name}'.")
|
|
224
|
+
|
|
225
|
+
return compliance_summary
|
|
226
|
+
|
|
227
|
+
def check_dicom_compliance(
|
|
228
|
+
reference_fields: List[Dict[str, Any]],
|
|
229
|
+
dicom_values: Dict[str, Any]
|
|
230
|
+
) -> List[Dict[str, Any]]:
|
|
231
|
+
"""
|
|
232
|
+
Validate individual DICOM values against reference fields.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
reference_fields (List[Dict[str, Any]]): A list of dictionaries defining the expected values
|
|
236
|
+
and rules for validation (e.g., tolerance, contains).
|
|
237
|
+
dicom_values (Dict[str, Any]): Dictionary of DICOM metadata values to be validated.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
List[Dict[str, Any]]: A list of compliance issues, where each issue is represented as a dictionary.
|
|
241
|
+
"""
|
|
242
|
+
compliance_summary = []
|
|
243
|
+
|
|
244
|
+
for ref_field in reference_fields:
|
|
245
|
+
field_name = ref_field["field"]
|
|
246
|
+
expected_value = ref_field.get("value")
|
|
247
|
+
tolerance = ref_field.get("tolerance")
|
|
248
|
+
contains = ref_field.get("contains")
|
|
249
|
+
actual_value = dicom_values.get(field_name, "N/A")
|
|
250
|
+
|
|
251
|
+
# Convert lists to tuples for comparison
|
|
252
|
+
if expected_value is not None and isinstance(expected_value, list):
|
|
253
|
+
expected_value = tuple(expected_value)
|
|
254
|
+
if actual_value is not None and isinstance(actual_value, list):
|
|
255
|
+
actual_value = tuple(actual_value)
|
|
256
|
+
|
|
257
|
+
# Check for missing field
|
|
258
|
+
if actual_value == "N/A":
|
|
259
|
+
compliance_summary.append({
|
|
260
|
+
"field": field_name,
|
|
261
|
+
"value": actual_value,
|
|
262
|
+
"rule": "Field must be present.",
|
|
263
|
+
"message": "Field not found.",
|
|
264
|
+
"passed": "❌",
|
|
265
|
+
})
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
# Contains check
|
|
269
|
+
if contains is not None:
|
|
270
|
+
if not isinstance(actual_value, list) or contains not in actual_value:
|
|
271
|
+
compliance_summary.append({
|
|
272
|
+
"field": field_name,
|
|
273
|
+
"value": actual_value,
|
|
274
|
+
"rule": "Field must contain value.",
|
|
275
|
+
"message": f"Expected to contain {contains}, got {actual_value}.",
|
|
276
|
+
"passed": "❌",
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
# Tolerance check
|
|
280
|
+
elif tolerance is not None and isinstance(actual_value, (int, float)):
|
|
281
|
+
if not (expected_value - tolerance <= actual_value <= expected_value + tolerance):
|
|
282
|
+
compliance_summary.append({
|
|
283
|
+
"field": field_name,
|
|
284
|
+
"value": actual_value,
|
|
285
|
+
"rule": "Field must be within tolerance.",
|
|
286
|
+
"message": f"Expected {expected_value} ± {tolerance}, got {actual_value}.",
|
|
287
|
+
"passed": "❌",
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
# Exact match check
|
|
291
|
+
elif expected_value is not None and actual_value != expected_value:
|
|
292
|
+
compliance_summary.append({
|
|
293
|
+
"field": field_name,
|
|
294
|
+
"value": actual_value,
|
|
295
|
+
"rule": "Field must match expected value.",
|
|
296
|
+
"message": f"Expected {expected_value}, got {actual_value}.",
|
|
297
|
+
"passed": "❌",
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
return compliance_summary
|
|
301
|
+
|
|
302
|
+
def is_session_compliant(
|
|
303
|
+
in_session: Dict[str, Dict[str, Any]],
|
|
304
|
+
ref_session: Dict[str, Dict[str, Any]],
|
|
305
|
+
session_map: Dict[Tuple[str, str], Tuple[str, str]]
|
|
306
|
+
) -> bool:
|
|
307
|
+
"""
|
|
308
|
+
Check if the entire DICOM session complies with the reference session.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
in_session (Dict): Input session data containing DICOM metadata.
|
|
312
|
+
ref_session (Dict): Reference session data containing expected metadata and rules.
|
|
313
|
+
session_map (Dict): Mapping of input acquisitions/series to reference acquisitions/series.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
bool: True if the session is fully compliant, False otherwise.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
compliance_issues = check_session_compliance_with_json_reference(in_session, ref_session, session_map)
|
|
320
|
+
return len(compliance_issues) == 0
|
|
321
|
+
|
|
322
|
+
def is_dicom_compliant(
|
|
323
|
+
reference_model: BaseValidationModel,
|
|
324
|
+
dicom_values: Dict[str, Any]
|
|
325
|
+
) -> bool:
|
|
326
|
+
"""
|
|
327
|
+
Check if a DICOM file's metadata complies with a validation model.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
reference_model (BaseValidationModel): The validation model defining expected metadata.
|
|
331
|
+
dicom_values (Dict[str, Any]): Dictionary of DICOM metadata values to be validated.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
bool: True if the DICOM metadata is compliant, False otherwise.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
compliance_issues = check_dicom_compliance(
|
|
338
|
+
reference_model.fields,
|
|
339
|
+
dicom_values
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return len(compliance_issues) == 0
|
|
343
|
+
|