148 lines
7.7 KiB
Python
Executable File
148 lines
7.7 KiB
Python
Executable File
import numpy as np
|
|
from scipy import ndimage
|
|
from typing import List, Tuple, Optional, Union
|
|
|
|
"""
|
|
Extract lesion candidates from a softmax prediction
|
|
Authors: anindox8, matinhz, joeranbosma
|
|
"""
|
|
|
|
|
|
# Preprocess Softmax Volume (Clipping, Max Confidence)
|
|
def preprocess_softmax_static(softmax: np.ndarray,
|
|
threshold: float = 0.10,
|
|
min_voxels_detection: int = 10,
|
|
max_prob_round_decimals: Optional[int] = 4) -> Tuple[np.ndarray, List[Tuple[int, float]], np.ndarray]:
|
|
# Load and Preprocess Softmax Image
|
|
all_hard_blobs = np.zeros_like(softmax)
|
|
confidences = []
|
|
clipped_softmax = softmax.copy()
|
|
clipped_softmax[softmax < threshold] = 0
|
|
blobs_index, num_blobs = ndimage.label(clipped_softmax, np.ones((3, 3, 3)))
|
|
|
|
if num_blobs > 0: # For Each Prediction
|
|
for tumor in range(1, num_blobs+1):
|
|
# determine mask for current lesion
|
|
hard_mask = np.zeros_like(blobs_index)
|
|
hard_mask[blobs_index == tumor] = 1
|
|
|
|
if np.count_nonzero(hard_mask) <= min_voxels_detection:
|
|
# remove tiny detection of <= 0.009 cm^3
|
|
blobs_index[hard_mask.astype(bool)] = 0
|
|
continue
|
|
|
|
# add sufficiently sized detection
|
|
hard_blob = hard_mask * clipped_softmax
|
|
max_prob = np.max(hard_blob)
|
|
if max_prob_round_decimals is not None:
|
|
max_prob = np.round(max_prob, max_prob_round_decimals)
|
|
hard_blob[hard_blob > 0] = max_prob
|
|
all_hard_blobs += hard_blob
|
|
confidences.append((tumor, max_prob))
|
|
return all_hard_blobs, confidences, blobs_index
|
|
|
|
|
|
def preprocess_softmax_dynamic(softmax: np.ndarray,
|
|
min_voxels_detection: int = 10,
|
|
num_lesions_to_extract: int = 5,
|
|
dynamic_threshold_factor: float = 2.5,
|
|
max_prob_round_decimals: Optional[int] = None,
|
|
remove_adjacent_lesion_candidates: bool = True,
|
|
max_prob_failsafe_stopping_threshold: float = 0.01) -> Tuple[np.ndarray, List[Tuple[int, float]], np.ndarray]:
|
|
"""
|
|
Generate detection proposals using a dynamic threshold to determine the location and size of lesions.
|
|
Author: Joeran Bosma
|
|
"""
|
|
working_softmax = softmax.copy()
|
|
dynamic_hard_blobs = np.zeros_like(softmax)
|
|
confidences: List[Tuple[int, float]] = []
|
|
dynamic_indexed_blobs = np.zeros_like(softmax, dtype=int)
|
|
|
|
while len(confidences) < num_lesions_to_extract:
|
|
tumor_index = 1 + len(confidences)
|
|
|
|
# determine max. softmax
|
|
max_prob = np.max(working_softmax)
|
|
|
|
if max_prob < max_prob_failsafe_stopping_threshold:
|
|
break
|
|
|
|
# set dynamic threshold to half the max
|
|
threshold = max_prob / dynamic_threshold_factor
|
|
|
|
# extract blobs for dynamix threshold
|
|
all_hard_blobs, _, _ = preprocess_softmax_static(working_softmax, threshold=threshold,
|
|
min_voxels_detection=min_voxels_detection,
|
|
max_prob_round_decimals=max_prob_round_decimals)
|
|
|
|
# select blob with max. confidence
|
|
# note: the max_prob is re-computed in the (unlikely) case that the max. prob
|
|
# was inside a 'lesion candidate' of less than min_voxels_detection, which is
|
|
# thus removed in preprocess_softmax_static.
|
|
max_prob = np.max(all_hard_blobs)
|
|
mask_current_lesion = (all_hard_blobs == max_prob)
|
|
|
|
# ensure that mask is only a single lesion candidate (this assumption fails when multiple lesions have the same max. prob)
|
|
mask_current_lesion_indexed, _ = ndimage.label(mask_current_lesion, np.ones((3, 3, 3)))
|
|
mask_current_lesion = (mask_current_lesion_indexed == 1)
|
|
|
|
# create mask with its confidence
|
|
hard_blob = (all_hard_blobs * mask_current_lesion)
|
|
|
|
# Detect whether the extractted mask is a ring/hollow sphere
|
|
# around an existing lesion candidate. For confident lesions,
|
|
# the surroundings of the prediction are still quite confident,
|
|
# and can become a second 'detection'. For an # example, please
|
|
# see extracted lesion candidates nr. 4 and 5 at:
|
|
# https://repos.diagnijmegen.nl/trac/ticket/9299#comment:49
|
|
# Detection method: grow currently extracted lesions by one voxel,
|
|
# and check if they overlap with the current extracted lesion.
|
|
extracted_lesions_grown = ndimage.morphology.binary_dilation(dynamic_hard_blobs > 0)
|
|
current_lesion_has_overlap = (mask_current_lesion & extracted_lesions_grown).any()
|
|
|
|
# Check if lesion candidate should be retained
|
|
if (not remove_adjacent_lesion_candidates) or (not current_lesion_has_overlap):
|
|
# store extracted lesion
|
|
dynamic_hard_blobs += hard_blob
|
|
confidences += [(tumor_index, max_prob)]
|
|
dynamic_indexed_blobs += (mask_current_lesion * tumor_index)
|
|
|
|
# remove extracted lesion from working-softmax
|
|
working_softmax = (working_softmax * (~mask_current_lesion))
|
|
|
|
return dynamic_hard_blobs, confidences, dynamic_indexed_blobs
|
|
|
|
|
|
def preprocess_softmax(softmax: np.ndarray,
|
|
threshold: Union[str, float] = 0.10,
|
|
min_voxels_detection: int = 10,
|
|
num_lesions_to_extract: int = 5,
|
|
dynamic_threshold_factor: float = 2.5,
|
|
max_prob_round_decimals: Optional[int] = None,
|
|
remove_adjacent_lesion_candidates: bool = True) -> Tuple[np.ndarray, List[Tuple[int, float]], np.ndarray]:
|
|
"""
|
|
Generate detection proposals using a dynamic or static threshold to determine the size of lesions.
|
|
"""
|
|
if threshold == 'dynamic':
|
|
all_hard_blobs, confidences, indexed_pred = preprocess_softmax_dynamic(softmax, min_voxels_detection=min_voxels_detection,
|
|
dynamic_threshold_factor=dynamic_threshold_factor,
|
|
num_lesions_to_extract=num_lesions_to_extract,
|
|
remove_adjacent_lesion_candidates=remove_adjacent_lesion_candidates,
|
|
max_prob_round_decimals=max_prob_round_decimals)
|
|
elif threshold == 'dynamic-fast':
|
|
# determine max. softmax and set a per-case 'static' threshold based on that
|
|
max_prob = np.max(softmax)
|
|
threshold = float(max_prob / dynamic_threshold_factor)
|
|
all_hard_blobs, confidences, indexed_pred = preprocess_softmax_static(softmax, threshold=threshold,
|
|
min_voxels_detection=min_voxels_detection,
|
|
max_prob_round_decimals=max_prob_round_decimals)
|
|
else:
|
|
threshold = float(threshold) # convert threshold to float, if it wasn't already
|
|
all_hard_blobs, confidences, indexed_pred = preprocess_softmax_static(softmax, threshold=threshold,
|
|
min_voxels_detection=min_voxels_detection,
|
|
max_prob_round_decimals=max_prob_round_decimals)
|
|
|
|
|
|
|
|
|
|
return all_hard_blobs, confidences, indexed_pred |