Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for python 3.12, pyproject.toml, minor naming cleanups #57

Merged
merged 15 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 25 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,10 @@ If you want to use the **regexp** action in a dictionary:

Here is a small example which keeps all metadata but updates the series description
by adding a suffix passed as a parameter.

```python
import argparse
from dicomanonymizer import *
from dicomanonymizer import ALL_TAGS, anonymize, keep

def main():
parser = argparse.ArgumentParser(add_help=True)
Expand All @@ -155,84 +156,58 @@ def main():
input_dicom_path = args.input
output_dicom_path = args.output

extraAnonymizationRules = {}
extra_anonymization_rules = {}

def setupSeriesDescription(dataset, tag):
def setup_series_description(dataset, tag):
element = dataset.get(tag)
if element is not None:
element.value = element.value + '-' + args.suffix
element.value = f'{element.value}-{args.suffix}'

# ALL_TAGS variable is defined on file dicomfields.py
# the 'keep' method is already defined into the dicom-anonymizer
# It will overrides the default behaviour
for i in allTags:
extraAnonymizationRules[i] = keep
for i in ALL_TAGS:
extra_anonymization_rules[i] = keep

if args.suffix:
extraAnonymizationRules[(0x0008, 0x103E)] = setupSeriesDescription
extra_anonymization_rules[(0x0008, 0x103E)] = setup_series_description

# Launch the anonymization
anonymize(input_dicom_path, output_dicom_path, extraAnonymizationRules)
anonymize(input_dicom_path, output_dicom_path, extra_anonymization_rules, delete_private_tags=False)

if __name__ == "__main__":
if __name__ == '__main__':
main()
```

See the full application in the `examples` folder.

In your own file, you'll have to define:
- Your custom functions. Be careful, your functions always have in inputs a dataset and a tag
- A dictionary which map your functions to a tag

## Anonymize dicom tags without dicom file
## Anonymize dicom tags for a dataset

If for some reason, you need to anonymize dicom fields without initial dicom file (extracted from a database for example). Here is how you can do it:
You can also anonymize dicom fields in-place for pydicom's DataSet using `anonymize_dataset`. See this example:
```python
from dicomanonymizer import *
from dicomanonymizer import anonymize_dataset
from pydicom.data import get_testdata_file
from pydicom import dcmread

def main():

# Create a list of tags object that should contains id, type and value
fields = [
{ # Replaced by Anonymized
'id': (0x0040, 0xA123),
'type': 'LO',
'value': 'Annie de la Fontaine',
},
{ # Replaced with empty value
'id': (0x0008, 0x0050),
'type': 'TM',
'value': 'bar',
},
{ # Deleted
'id': (0x0018, 0x4000),
'type': 'VR',
'value': 'foo',
}
]

# Create a readable dataset for pydicom
data = pydicom.Dataset()

# Add each field into the dataset
for field in fields:
data.add_new(field['id'], field['type'], field['value'])

anonymize_dataset(data)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you delete this example ? This can be useful.
Why not moving it in the examples folder you created ?

Copy link
Contributor Author

@smjoshiatglobus smjoshiatglobus Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this example and could not get it to work. I can fix the first warning about 'bar' not being valid for TM, but have no idea how to fix the error of missing file_meta for the dataset. I took the easier route of using an existing DICOM data file!

Here is the output:

C:\Users\smjoshi\.virtualenvs\examples-gxyY8XxM\Lib\site-packages\pydicom\valuerep.py:443: UserWarning: Invalid value for VR TM: 'bar'.
  warnings.warn(msg)
Traceback (most recent call last):
  File "C:\development\dicom-anonymizer\examples\anonymize_fields.py", line 33, in <module>
    main()
  File "C:\development\dicom-anonymizer\examples\anonymize_fields.py", line 30, in main
    anonymize_dataset(data)
  File "C:\development\dicom-anonymizer\dicomanonymizer\simpledicomanonymizer.py", line 436, in anonymize_dataset
    action(dataset.file_meta, tag)
           ^^^^^^^^^^^^^^^^^
  File "C:\Users\smjoshi\.virtualenvs\examples-gxyY8XxM\Lib\site-packages\pydicom\dataset.py", line 908, in __getattr__
    return object.__getattribute__(self, name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Dataset' object has no attribute 'file_meta'

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be fixed with #61

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I will pull in the changes into my branch and that should clean up this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have restored the example in README.md (actually, I copy-pasted your code, so the white-space differences are showing up!). Please re-review.

data_ds = dcmread(get_testdata_file("CT_small.dcm"))
anonymize_dataset(data_ds, delete_private_tags=True) # Anonymization is done in-place

if __name__ == "__main__":
main()
```
For more information about the pydicom's Dataset, please refer [here](https://github.com/pydicom/pydicom/blob/995ac6493188313f6a2e6355477baba9f543447b/pydicom/dataset.py).
You can also add a dictionary as previously :
```python
dictionary = {}

def newMethod(dataset, tag):
element = dataset.get(tag)
if element is not None:
element.value = element.value + '- generated with new method'
See the full application in the `examples` folder.

For more information about the pydicom's Dataset, please refer [here](https://pydicom.github.io/pydicom/stable/reference/generated/pydicom.dataset.Dataset.html).

dictionary[(0x0008, 0x103E)] = newMethod
anonymize_dataset(data, dictionary)
You can also add `extra_anonymization_rules` as above:
```python
anonymize_dataset(data_ds, extra_anonymization_rules, delete_private_tags=True)
```

# Actions list
Expand Down
8 changes: 4 additions & 4 deletions dicomanonymizer/anonymizer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import argparse
import ast
import importlib.metadata
import json
import pkg_resources
import os
import sys
import tqdm
Expand All @@ -21,7 +21,7 @@ def isDICOMType(filePath):
return False


def anonymize(input_path: str, output_path: str, anonymization_actions: dict, deletePrivateTags: bool) -> None:
def anonymize(input_path: str, output_path: str, anonymization_actions: dict, delete_private_tags: bool) -> None:
"""
Read data from input path (folder or file) and launch the anonymization.

Expand Down Expand Up @@ -62,7 +62,7 @@ def anonymize(input_path: str, output_path: str, anonymization_actions: dict, de

progress_bar = tqdm.tqdm(total=len(input_files_list))
for cpt in range(len(input_files_list)):
anonymize_dicom_file(input_files_list[cpt], output_files_list[cpt], anonymization_actions, deletePrivateTags)
anonymize_dicom_file(input_files_list[cpt], output_files_list[cpt], anonymization_actions, delete_private_tags)
progress_bar.update(1)

progress_bar.close()
Expand Down Expand Up @@ -98,7 +98,7 @@ def generate_actions_dictionary(map_action_tag, defined_action_map = {}) -> dict


def main(defined_action_map = {}):
version_info = pkg_resources.require("dicom_anonymizer")[0].version
version_info = importlib.metadata.version("dicom_anonymizer")
parser = argparse.ArgumentParser(add_help=True)
parser.add_argument('input', help='Path to the input dicom file or input directory which contains dicom files')
parser.add_argument('output', help='Path to the output dicom file or output directory which will contains dicom files')
Expand Down
2 changes: 2 additions & 0 deletions examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ignore Pipfile.lock to allow support across multiple Python and operating system versions.
Pipfile.lock
4 changes: 4 additions & 0 deletions examples/Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[packages]
# dicom-anonymizer = "*" # For 'released' version
dicom-anonymizer = {file = ".."} # For local version
pydicom = "*"
8 changes: 8 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#DicomAnonymizer Examples

This folder contains the following examples:
- `anonymize_dataset.py`: Anonymize dicom tags for a dataset
- `anonymize_extra_rules.py`: Anonymize dicom tags for a file with extra rules

The supporting files are:
- `Pipfile`: The [pipenv](https://packaging.python.org/en/latest/tutorials/managing-dependencies/) file.
14 changes: 14 additions & 0 deletions examples/anonymize_dataset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from dicomanonymizer import anonymize_dataset
from pydicom.data import get_testdata_file
from pydicom import dcmread

def main():
original_ds = dcmread(get_testdata_file("CT_small.dcm"))
data_ds = original_ds.copy()
anonymize_dataset(data_ds, delete_private_tags=True) # Anonymization is done in-place
print("Examples of original -> anonymized values:")
for tt in ["PatientName", "PatientID", "StudyDate"]:
print(f" {tt}: '{original_ds[tt].value}' -> '{data_ds[tt].value}'")

if __name__ == "__main__":
main()
34 changes: 34 additions & 0 deletions examples/anonymize_extra_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import argparse
from dicomanonymizer import ALL_TAGS, anonymize, keep

def main():
parser = argparse.ArgumentParser(add_help=True)
parser.add_argument('input', help='Path to the input dicom file or input directory which contains dicom files')
parser.add_argument('output', help='Path to the output dicom file or output directory which will contains dicom files')
parser.add_argument('--suffix', action='store', help='Suffix that will be added at the end of series description')
args = parser.parse_args()

input_dicom_path = args.input
output_dicom_path = args.output

extra_anonymization_rules = {}

def setup_series_description(dataset, tag):
element = dataset.get(tag)
if element is not None:
element.value = f'{element.value}-{args.suffix}'

# ALL_TAGS variable is defined on file dicomfields.py
# the 'keep' method is already defined into the dicom-anonymizer
# It will overrides the default behaviour
for i in ALL_TAGS:
extra_anonymization_rules[i] = keep

if args.suffix:
extra_anonymization_rules[(0x0008, 0x103E)] = setup_series_description

# Launch the anonymization
anonymize(input_dicom_path, output_dicom_path, extra_anonymization_rules, delete_private_tags=False)

if __name__ == '__main__':
main()
40 changes: 40 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
[project]
name = "dicom_anonymizer"
version = "1.0.11"
authors = [
{ name="Laurenn Lam", email="[email protected]" },
]
description = "Program to anonymize dicom files with default and custom rules"
readme = "README.md"
requires-python = ">=3.10"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why restrict to 3.10 and higher ? I tested with Python 3.9 and 3.8 and it seems to work

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my confusion. Per importlib.metadata documentation, it was "provisional" until 3.10, so I thought it was not available for versions 3.8 and 3.9. Since these worked, I will update this minimum requirement to 3.8.

Thank you for testing!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit b3dc158

classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Science/Research",
"Topic :: Software Development :: Build Tools",
"License :: OSI Approved :: BSD License",
"Natural Language :: English",
"Programming Language :: Python"
]
keywords = ["dicom", "anonymizer" , "medical"]

dependencies = [
"pydicom",
"tqdm",
]

[project.optional-dependencies]
dev = [
"pytest",
"setuptools", # Needed to load pydicom's test files
]

[project.scripts]
dicom-anonymizer = "dicomanonymizer.anonymizer:main"

[project.urls]
"Homepage" = "https://github.com/KitwareMedical/dicom-anonymizer"
"Bug Tracker" = "https://github.com/KitwareMedical/dicom-anonymizer/issues"

[tool.setuptools.packages]
find = {} # Scanning implicit namespaces is active by default

5 changes: 0 additions & 5 deletions setup.cfg

This file was deleted.

78 changes: 0 additions & 78 deletions setup.py

This file was deleted.