Skip to content

Commit 8eb6edb

Browse files
authored
Merge pull request #197 from databricks-industry-solutions/feature/remove_un_tags
Enhance DICOM metadata extraction by removing UN VR elements
2 parents 48cd1bb + 846a83d commit 8eb6edb

3 files changed

Lines changed: 57 additions & 7 deletions

File tree

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,27 @@ metadata_df = DicomMetaAnonymizerExtractor(
165165
By setting the `anonym_mode` parameter to `"METADATA"`, the DICOM metadata will be anonymized during the ingestion process. This ensures that sensitive patient information is not stored in the catalog.
166166
The default configuration will save the anonymized DICOM files under `anonymization_base_path` property's path.
167167

168+
## Remove UN Tags
169+
DICOM files can contain elements with Value Representation `UN` (Unknown), which are tags that could not be resolved to a specific VR during parsing. These tags often carry unstructured or proprietary data that can bloat the extracted metadata, cause serialization issues, or introduce noise in downstream analytics.
170+
171+
Pixels provides a built-in option to strip all `UN` VR elements from the dataset before metadata extraction. The removal is recursive, so `UN` elements nested inside sequences (`SQ`) are also cleaned up.
172+
173+
To enable this feature, set `remove_un_tags=True` on the `DicomMetaExtractor`:
174+
```python
175+
from dbx.pixels import Catalog
176+
from dbx.pixels.dicom import *
177+
178+
catalog = Catalog(spark)
179+
catalog_df = catalog.catalog(<path>)
180+
181+
meta_df = DicomMetaExtractor(
182+
catalog,
183+
remove_un_tags=True
184+
).transform(catalog_df)
185+
186+
catalog.save(meta_df)
187+
```
188+
168189
---
169190
## OHIF Viewer
170191
Inside `dbx.pixels` resources folder, a pre-built version of [OHIF Viewer](https://github.com/OHIF/Viewers) with Databricks and [Unity Catalog Volumes](https://docs.databricks.com/en/sql/language-manual/sql-ref-volumes.html) extension is provided.

dbx/pixels/dicom/dicom_meta_extractor.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class DicomMetaExtractor(Transformer):
1818
headers over the network, maximizing I/O throughput on each Spark task.
1919
"""
2020

21-
MAX_WORKERS = 20
21+
MAX_WORKERS = 32
2222

2323
def __init__(
2424
self,
@@ -29,6 +29,7 @@ def __init__(
2929
deep=False,
3030
useVariant=True,
3131
maxWorkers=None,
32+
remove_un_tags=False,
3233
):
3334
self.inputCol = inputCol
3435
self.outputCol = outputCol
@@ -37,6 +38,7 @@ def __init__(
3738
self.deep = deep
3839
self.useVariant = useVariant
3940
self.maxWorkers = maxWorkers if maxWorkers is not None else self.MAX_WORKERS
41+
self.remove_un_tags = remove_un_tags
4042

4143
def check_input_type(self, schema):
4244
field = schema[self.inputCol]
@@ -69,6 +71,7 @@ def _transform(self, df):
6971
output_col = self.outputCol
7072
deep = self.deep
7173
max_workers = self.maxWorkers
74+
remove_un_tags = self.remove_un_tags
7275

7376
# Build output schema: all existing columns + the new meta column (as StringType initially)
7477
out_schema = t.StructType(
@@ -80,12 +83,12 @@ def _extract_meta(iterator: Iterator[pd.DataFrame]) -> Iterator[pd.DataFrame]:
8083
import simplejson as json
8184
from pydicom import dcmread
8285

83-
def _process_file(path: str, is_deep: bool, anon: bool) -> str:
86+
def _process_file(path: str, deep: bool, anon: bool, remove_un_tags: bool) -> str:
8487
try:
8588
fp, fsize = cloud_open(path, anon)
86-
with dcmread(fp, defer_size=1000, stop_before_pixels=(not is_deep)) as dataset:
87-
meta_js = extract_metadata(dataset, is_deep)
88-
if is_deep:
89+
with dcmread(fp, defer_size=1000, stop_before_pixels=(not deep)) as dataset:
90+
meta_js = extract_metadata(dataset, deep, remove_un_tags=remove_un_tags)
91+
if deep:
8992
meta_js["hash"] = hashlib.sha1(fp.read()).hexdigest()
9093
meta_js["file_size"] = fsize
9194
return json.dumps(meta_js, ignore_nan=True)
@@ -107,7 +110,7 @@ def _process_file(path: str, is_deep: bool, anon: bool) -> str:
107110
with ThreadPoolExecutor(max_workers=max_workers) as executor:
108111
meta_results = list(
109112
executor.map(
110-
lambda args: _process_file(args[0], deep, args[1]),
113+
lambda args: _process_file(args[0], deep, args[1], remove_un_tags),
111114
zip(paths, anon_flags),
112115
)
113116
)

dbx/pixels/dicom/dicom_utils.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,38 @@ def check_pixel_data(ds: Dataset) -> Dataset | None:
4646
return None
4747

4848

49-
def extract_metadata(ds: Dataset, deep: bool = True) -> dict:
49+
def remove_un_vr_elements(ds: Dataset) -> Dataset:
50+
"""
51+
Recursively removes all elements with VR='UN' from a pydicom dataset.
52+
"""
53+
# Create a list of tags to delete so we don't modify the dataset while iterating
54+
tags_to_delete = []
55+
56+
for elem in ds:
57+
if elem.VR == "UN":
58+
tags_to_delete.append(elem.tag)
59+
elif elem.VR == "SQ":
60+
# If the element is a sequence, recursively process each dataset within it
61+
for item in elem:
62+
remove_un_vr_elements(item)
63+
64+
# Delete the identified tags from the current dataset level
65+
for tag in tags_to_delete:
66+
del ds[tag]
67+
68+
return ds
69+
70+
71+
def extract_metadata(ds: Dataset, deep: bool = True, remove_un_tags: bool = False) -> dict:
5072
"""Extract metadata from header of dicom image file
5173
params:
5274
path -- local path like /dbfs/mnt/... or s3://<bucket>/path/to/object.dcm
5375
deep -- True if deep inspection of the Dicom header is required
76+
remove_un_vr_elements -- True if UN VR elements should be removed from the dataset
5477
"""
78+
if remove_un_tags:
79+
ds = remove_un_vr_elements(ds)
80+
5581
a = None
5682
js = {}
5783

0 commit comments

Comments
 (0)