Skip to content

Commit

Permalink
Merge pull request #89 from BrainLesion/59-feature-request-defacing-s…
Browse files Browse the repository at this point in the history
…upport

59 feature request defacing support
  • Loading branch information
neuronflow authored Oct 24, 2024
2 parents cbe3a39 + 1eb7478 commit 26dc3b8
Show file tree
Hide file tree
Showing 10 changed files with 735 additions and 129 deletions.
21 changes: 9 additions & 12 deletions brainles_preprocessing/brain_extraction/brain_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,29 @@ def extract(

def apply_mask(
self,
input_image_path: str,
mask_image_path: str,
masked_image_path: str,
input_image_path: Path,
mask_path: Path,
bet_image_path: Path,
) -> None:
"""
Apply a brain mask to an input image.
Parameters:
- input_image_path (str): Path to the input image (NIfTI format).
- mask_image_path (str): Path to the brain mask image (NIfTI format).
- masked_image_path (str): Path to save the resulting masked image (NIfTI format).
Returns:
- str: Path to the saved masked image.
Args:
input_image_path (str): Path to the input image (NIfTI format).
mask_path (str): Path to the brain mask image (NIfTI format).
bet_image_path (str): Path to save the resulting masked image (NIfTI format).
"""

# read data
input_data = read_nifti(input_image_path)
mask_data = read_nifti(mask_image_path)
mask_data = read_nifti(mask_path)

# mask and save it
masked_data = input_data * mask_data

write_nifti(
input_array=masked_data,
output_nifti_path=masked_image_path,
output_nifti_path=bet_image_path,
reference_nifti_path=input_image_path,
create_parent_directory=True,
)
Expand Down
10 changes: 10 additions & 0 deletions brainles_preprocessing/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from enum import IntEnum


class PreprocessorSteps(IntEnum):
INPUT = 0
COREGISTERED = 1
ATLAS_REGISTERED = 2
ATLAS_CORRECTED = 3
BET = 4
DEFACED = 5
2 changes: 2 additions & 0 deletions brainles_preprocessing/defacing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .defacer import Defacer
from .quickshear.quickshear import QuickshearDefacer
43 changes: 43 additions & 0 deletions brainles_preprocessing/defacing/defacer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from abc import abstractmethod
from pathlib import Path

from auxiliary.nifti.io import read_nifti, write_nifti


class Defacer:
@abstractmethod
def deface(
self,
input_image_path: Path,
mask_image_path: Path,
) -> None:
pass

def apply_mask(
self,
input_image_path: str,
mask_path: str,
defaced_image_path: str,
) -> None:
"""
Apply a brain mask to an input image.
Args:
input_image_path (str): Path to the input image (NIfTI format).
mask_path (str): Path to the brain mask image (NIfTI format).
defaced_image_path (str): Path to save the resulting defaced image (NIfTI format).
"""

# read data
input_data = read_nifti(input_image_path)
mask_data = read_nifti(mask_path)

# mask and save it
masked_data = input_data * mask_data

write_nifti(
input_array=masked_data,
output_nifti_path=defaced_image_path,
reference_nifti_path=input_image_path,
create_parent_directory=True,
)
Empty file.
223 changes: 223 additions & 0 deletions brainles_preprocessing/defacing/quickshear/nipy_quickshear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Code adapted from: https://github.com/nipy/quickshear/blob/master/quickshear.py (23.10.2024)
# Minor adaptions in terms of parameters and return values
# Original Author': Copyright (c) 2011, Nakeisha Schimke. All rights reserved.

import argparse
import logging

#!/usr/bin/python
import sys

import nibabel as nb
import numpy as np
from numpy.typing import NDArray

try:
from duecredit import BibTeX, due
except ImportError:
# Adapted from
# https://github.com/duecredit/duecredit/blob/2221bfd/duecredit/stub.py
class InactiveDueCreditCollector:
"""Just a stub at the Collector which would not do anything"""

def _donothing(self, *args, **kwargs):
"""Perform no good and no bad"""
pass

def dcite(self, *args, **kwargs):
"""If I could cite I would"""

def nondecorating_decorator(func):
return func

return nondecorating_decorator

cite = load = add = _donothing

def __repr__(self):
return self.__class__.__name__ + "()"

due = InactiveDueCreditCollector()

def BibTeX(*args, **kwargs):
pass


citation_text = """@inproceedings{Schimke2011,
abstract = {Data sharing offers many benefits to the neuroscience research
community. It encourages collaboration and interorganizational research
efforts, enables reproducibility and peer review, and allows meta-analysis and
data reuse. However, protecting subject privacy and implementing HIPAA
compliance measures can be a burdensome task. For high resolution structural
neuroimages, subject privacy is threatened by the neuroimage itself, which can
contain enough facial features to re-identify an individual. To sufficiently
de-identify an individual, the neuroimage pixel data must also be removed.
Quickshear Defacing accomplishes this task by effectively shearing facial
features while preserving desirable brain tissue.},
address = {San Francisco},
author = {Schimke, Nakeisha and Hale, John},
booktitle = {Proceedings of the 2nd USENIX Conference on Health Security and Privacy},
title = {{Quickshear Defacing for Neuroimages}},
year = {2011},
month = sep
}
"""
# __version__ = "1.3.0.dev0"


def edge_mask(mask):
"""Find the edges of a mask or masked image
Parameters
----------
mask : 3D array
Binary mask (or masked image) with axis orientation LPS or RPS, and the
non-brain region set to 0
Returns
-------
2D array
Outline of sagittal profile (PS orientation) of mask
"""
# Sagittal profile
brain = mask.any(axis=0)

# Simple edge detection
edgemask = (
4 * brain
- np.roll(brain, 1, 0)
- np.roll(brain, -1, 0)
- np.roll(brain, 1, 1)
- np.roll(brain, -1, 1)
!= 0
)
return edgemask.astype("uint8")


def convex_hull(brain):
"""Find the lower half of the convex hull of non-zero points
Implements Andrew's monotone chain algorithm [0].
[0] https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Convex_hull/Monotone_chain
Parameters
----------
brain : 2D array
2D array in PS axis ordering
Returns
-------
(2, N) array
Sequence of points in the lower half of the convex hull of brain
"""
# convert brain to a list of points in an n x 2 matrix where n_i = (x,y)
pts = np.vstack(np.nonzero(brain)).T

def cross(o, a, b):
return np.cross(a - o, b - o)

lower = []
for p in pts:
while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0:
lower.pop()
lower.append(p)

return np.array(lower).T


@due.dcite(
BibTeX(citation_text),
description="Geometric neuroimage defacer",
path="quickshear",
)
def run_quickshear(bet_img: nb.nifti1.Nifti1Image, buffer: int = 10) -> NDArray:
"""Deface image using Quickshear algorithm
Parameters
----------
bet_img : Nifti1Image
Nibabel image of skull-stripped brain mask or masked anatomical
buffer : int
Distance from mask to set shearing plane
Returns
-------
defaced_mask: NDArray
Defaced image mask
"""
src_ornt = nb.io_orientation(bet_img.affine)
tgt_ornt = nb.orientations.axcodes2ornt("RPS")
to_RPS = nb.orientations.ornt_transform(src_ornt, tgt_ornt)
from_RPS = nb.orientations.ornt_transform(tgt_ornt, src_ornt)

mask_RPS = nb.orientations.apply_orientation(bet_img.dataobj, to_RPS)

edgemask = edge_mask(mask_RPS)
low = convex_hull(edgemask)
xdiffs, ydiffs = np.diff(low)
slope = ydiffs[0] / xdiffs[0]

yint = low[1][0] - (low[0][0] * slope) - buffer
ys = np.arange(0, mask_RPS.shape[2]) * slope + yint
defaced_mask_RPS = np.ones(mask_RPS.shape, dtype="bool")

for x, y in zip(np.nonzero(ys > 0)[0], ys.astype(int)):
defaced_mask_RPS[:, x, :y] = 0

defaced_mask = nb.orientations.apply_orientation(defaced_mask_RPS, from_RPS)

# return anat_img.__class__(
# np.asanyarray(anat_img.dataobj) * defaced_mask,
# anat_img.affine,
# anat_img.header,
# )

return defaced_mask


# def main():
# logger = logging.getLogger(__name__)
# logger.setLevel(logging.DEBUG)
# ch = logging.StreamHandler()
# ch.setLevel(logging.DEBUG)
# logger.addHandler(ch)

# parser = argparse.ArgumentParser(
# description="Quickshear defacing for neuroimages",
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
# )
# parser.add_argument("anat_file", type=str, help="filename of neuroimage to deface")
# parser.add_argument("mask_file", type=str, help="filename of brain mask")
# parser.add_argument(
# "defaced_file", type=str, help="filename of defaced output image"
# )
# parser.add_argument(
# "buffer",
# type=float,
# nargs="?",
# default=10.0,
# help="buffer size (in voxels) between shearing plane and the brain",
# )

# opts = parser.parse_args()

# anat_img = nb.load(opts.anat_file)
# bet_img = nb.load(opts.mask_file)

# if not (
# anat_img.shape == bet_img.shape
# and np.allclose(anat_img.affine, bet_img.affine)
# ):
# logger.warning(
# "Anatomical and mask images do not have the same shape and affine."
# )
# return -1

# new_anat = quickshear(anat_img, bet_img, opts.buffer)
# new_anat.to_filename(opts.defaced_file)
# logger.info(f"Defaced file: {opts.defaced_file}")


# if __name__ == "__main__":
# sys.exit(main())
53 changes: 53 additions & 0 deletions brainles_preprocessing/defacing/quickshear/quickshear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from pathlib import Path

import nibabel as nib
from brainles_preprocessing.defacing.defacer import Defacer
from brainles_preprocessing.defacing.quickshear.nipy_quickshear import run_quickshear
from numpy.typing import NDArray
from auxiliary.nifti.io import write_nifti


class QuickshearDefacer(Defacer):
"""
Defacer using Quickshear algorithm.
Quickshear uses a skull stripped version of an anatomical images as a reference to deface the unaltered anatomical image.
Base publication:
- PDF: https://www.researchgate.net/profile/J-Hale/publication/262319696_Quickshear_defacing_for_neuroimages/links/570b97ee08aed09e917516b1/Quickshear-defacing-for-neuroimages.pdf
- Bibtex:
```
@article{schimke2011quickshear,
title={Quickshear Defacing for Neuroimages.},
author={Schimke, Nakeisha and Hale, John},
journal={HealthSec},
volume={11},
pages={11},
year={2011}
}
```
"""

def __init__(self, buffer: float = 10.0):
"""Initialize Quickshear defacer
Args:
buffer (float, optional): buffer parameter from quickshear algorithm. Defaults to 10.0.
"""
super().__init__()
self.buffer = buffer

def deface(self, mask_image_path: Path, bet_img_path: Path) -> None:
"""Deface image using Quickshear algorithm
Args:
bet_img_path (Path): Path to the brain extracted image
"""

bet_img = nib.load(bet_img_path)
mask = run_quickshear(bet_img=bet_img, buffer=self.buffer)
write_nifti(
input_array=mask,
output_nifti_path=mask_image_path,
reference_nifti_path=bet_img_path,
)
Loading

0 comments on commit 26dc3b8

Please sign in to comment.