Skip to content

Commit

Permalink
✨ change_header with sphinx docs
Browse files Browse the repository at this point in the history
  • Loading branch information
WillForan committed Oct 21, 2024
1 parent 6428ade commit 9f87139
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: documentation
on: [push, pull_request, workflow_dispatch]
permissions:
contents: write

jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Install dependencies
run: |
make .venv
- name: Sphinx build
run: |
make docs/
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
with:
publish_branch: gh-pages
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: docs/
force_orphan: true
19 changes: 19 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
.PHONY: test

# how to get into the python virtual environment
source_venv := source .venv/bin/activate

docs/: .venv/ $(wildcard *.py) sphinx/conf.py $(wildcard sphinx/*.rst)
$(source_venv) && sphinx-build sphinx/ docs/

test:
$(source_venv) && python3 -m doctest change_header.py

.venv/:
python -m venv .venv && $(source_venv) && pip install -r requirements.txt


db.sqlite:
sqlite3 < schema.sql

# TODO: replace me actual code
db.txt:
./00_build_db.bash
76 changes: 76 additions & 0 deletions change_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""
Modify protocol names.
"""
import os
from pathlib import Path
from typing import List, Optional
from itertools import chain
import pydicom


def change_protocol_name(
dcm_dir: Path, new_data: List[pydicom.DataElement], out_dir: Optional[Path] = None
):
"""
Change specified tags of all dicoms in a directory. Optionally make copies in out_dir.
:param dcm_dir: input directory with dicom files (``MR*``, ``*IMA`` , or ``*dcm``)
:param new_data: list of data elements to replace
like ``[pydicom.DataElement(value="newpname", VR="LO", tag=(0x0018, 0x1030))]``
:param out_dir: Optional. Where to save modified dicoms
:return: example modified dicom. last if out_dir, first and only if no ``out_dir``.
sideffect: writes copies of dcm_dir dicoms inot out_dir unless out_dir is None.
>>> new_data = [pydicom.DataElement(value="newpname", VR="LO", tag=(0x0018, 0x1030))]
>>> ex_path = Path('example/dicom/11903_20221222/HabitTask_704x752.18/')
>>> ex = change_protocol_name(ex_path, new_data)
>>> ex.ProtocolName
'newpname'
"""
all_dicoms = chain(dcm_dir.glob("MR*"), dcm_dir.glob("*IMA"), dcm_dir.glob("*dcm"))
ex_dcm = None
for ex_dcm_file in all_dicoms:
ex_dcm = pydicom.dcmread(ex_dcm_file)

for datum in new_data:
ex_dcm[datum.tag] = datum

# dont need to do anything if not writing files
if out_dir is None:
return ex_dcm

new_file = os.path.join(out_dir, os.path.basename(ex_dcm_file))
# assume if we have one, we have them all (leave loop at first existing)
if os.path.exists(new_file):
return ex_dcm

# and save out
os.makedirs(out_dir, exist_ok=True)
ex_dcm.save_as(new_file)

return ex_dcm


if __name__ == "__main__":
new_tags = [
# Repetition Time
pydicom.DataElement(value="1301", VR="DS", tag=(0x0018, 0x0080)),
# Patient ID
pydicom.DataElement(value="mod1", VR="PN", tag=(0x0010, 0x0010)),
pydicom.DataElement(value="mod1", VR="LO", tag=(0x0010, 0x0020)),
## anonymize
# DOB
pydicom.DataElement(value="19991231", VR="DA", tag=(0x0010, 0x0030)),
# age
pydicom.DataElement(value="100Y", VR="AS", tag=(0x0010, 0x1010)),
# sex
pydicom.DataElement(value="20240131", VR="CS", tag=(0x0010, 0x0040)),
]

change_protocol_name(
Path("example/dicom/11903_20221222/HabitTask_704x752.18/"),
new_tags,
Path("example/dicom/mod1/HabitTask/"),
)
19 changes: 19 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# MRRC Dicom Header Quality Assurance
Parse dicoms into a template database and alert on non-conforming sequences.

See
* `make docs/` for building sphinx documentation
* locally in [`sphinx/index.rst`](sphinx/index.rst))
* reference for restructured text [`sphinx docstrings`](https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html)
* `make test` for using `doctests`
* [`schema.sql`](schema.sql) for DB schema

## Strategy

* build sqlite db of all acquisitions with subset of parameters
* use db summary to pull out "ideal template"
* check new sessions' acquisitions against template to alert

## Prior Art
* mrQA
* sister project https://github.com/NPACore/mrqart/
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pydicom
mrQA
dicom-parser
sphinx
31 changes: 31 additions & 0 deletions schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-- acquisition. time id
create table acq (
param_id integer, -- join to session-consitent settings
AcqTime text,
AcqDate text,
SeiresNumber text,
SubID text,
Operator text
);

-- acq params that should match across sessions
create table acq_param (
is_ideal timestamp,
Project text,
SequenceName text,
-- TODO: should this be json blob? to extend easier?
iPAT text,
Comments text,
SequenceType text,
PED_major text,
TR text,
TE text,
Matrix text,
PixelResol text,
BWP text,
BWPPE text,
FA text,
TA text,
FoV text
-- TODO: add shim settings from CSA
);
33 changes: 33 additions & 0 deletions sphinx/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import sys
import os
sys.path.insert(0, os.path.abspath('../')) # Source code dir relative to this file
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = 'mrrc-hdr-qa'
copyright = '2024, EH, WF'
author = 'EH, WF'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary']
autodoc_default_options = { 'members': True }
autodoc_typehints = "description"
autosummary_generate = True

templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'docs', '.venv', 'lib']



# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = 'alabaster'
html_static_path = ['_static']
19 changes: 19 additions & 0 deletions sphinx/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.. mrrc-hdr-qa documentation master file, created by
sphinx-quickstart on Mon Oct 21 19:00:25 2024.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
mrrc-hdr-qa documentation
=========================

Code to parse dicoms into a template database and alert on non-conforming sequences.


.. toctree::
:caption: Contents:

.. autosummary::
:toctree: _autosummary
:recursive:

change_header

0 comments on commit 9f87139

Please sign in to comment.