Source code for xnat_ingest.api.deidentify_

import os
import json
import traceback
from pathlib import Path
import typing as ty

from cryptography.fernet import Fernet
from fileformats.core import extra_implementation, from_mime
from fileformats.medimage.base import MedicalImagingData
from fileformats.medimage.dicom import DicomImage
from fileformats.core import FileSet
from tqdm import tqdm

from xnat_ingest.helpers.remotes import LocalSessionListing, list_session_dirs

from ..helpers.logging import logger
from ..model.session import ImagingSession

DEFAULT_SPEC_DIR = "__default__"


[docs] def deidentify( input_dir: Path, output_dir: Path, spec_dir: Path, reid_dir: Path, avoid_clashes: bool = False, raise_errors: bool = False, copy_mode: FileSet.CopyMode = FileSet.CopyMode.copy, require_manifest: bool = True, unlink_source: str | None = None, reid_encrypt_key: bytes | None = None, ) -> list[str]: sessions: list[LocalSessionListing] = [ LocalSessionListing(d) for d in list_session_dirs(input_dir) ] num_sessions = len(sessions) logger.info( "Found %d sessions in staging directory to stage'%s'", num_sessions, input_dir, ) # Ensure the output and reid directories exist output_dir.mkdir(parents=True, exist_ok=True) reid_dir.mkdir(parents=True, exist_ok=True) errors: list[str] = [] default_spec = load_specs(spec_dir / DEFAULT_SPEC_DIR) for session_listing in tqdm( sessions, total=num_sessions, desc=f"Processing staged sessions found in '{input_dir}'", ): try: session = ImagingSession.load( session_listing.cache_path, require_manifest=require_manifest, check_checksums=False, ) # Get the project-specific deidentification specs for this session # for each file type specs = load_specs(spec_dir / session.project_id) if specs is None: if default_spec is None: raise ValueError( f"No deidentification specs found for project '{session.project_id}' " "and no default specs provided." ) specs = default_spec deidentified_session, reid_mdata = session.deidentify( output_dir, copy_mode=copy_mode, avoid_clashes=avoid_clashes, specs=specs, ) deidentified_session.save(output_dir / session_listing.name) reid_mdata_json = json.dumps(reid_mdata, indent=2).encode() if reid_encrypt_key is not None: reid_fspath = reid_dir / f"{session_listing.name}.json.enc" reid_fspath.write_bytes( Fernet(reid_encrypt_key).encrypt(reid_mdata_json) ) else: reid_fspath = reid_dir / f"{session_listing.name}.json" reid_fspath.write_bytes(reid_mdata_json) except Exception as e: if raise_errors: raise logger.error( "Error deidentifying session '%s': %s", session_listing.session_id, str(e), ) logger.debug(traceback.format_exc()) errors.append(str(e)) else: if unlink_source == "all": # remove the original (assigned) session directory in its entirety session_listing.session_dir.rmdir() elif unlink_source == "keep-metadata": # remove just the resource data, leaving the session/scan-level # metadata behind as a lightweight skeleton session.unlink(keep_metadata=True) return errors
def load_specs(spec_dir: Path) -> ty.Mapping[ty.Type[MedicalImagingData], Path] | None: """Loads the deidentification specifications from the given directory, returning a mapping of file-formats to their corresponding spec file paths. The spec files should be named in the format '{mime_type}.json', where the mime type is transformed by replacing '/' with '@' to be filesystem-friendly. If the spec directory does not exist, returns None Parameters ---------- spec_dir : Path the directory containing the deidentification specification files Returns ------- dict or None A mapping of file-format types to their corresponding spec file paths, or None if the spec directory does not exist. """ if not spec_dir.exists(): return None return { from_mime(p.name.replace("@", "/")): p for p in spec_dir.iterdir() if "@" in p.name } @extra_implementation(MedicalImagingData.deidentify) def dicom_deidentify( dicom: DicomImage, spec: ty.Any = None, out_dir: os.PathLike[str] | None = None, ) -> tuple[DicomImage, ty.Mapping[str, ty.Any]]: raise NotImplementedError