AdritRao's picture
Upload 62 files
a3290d1
raw
history blame contribute delete
No virus
4.82 kB
from abc import ABC, abstractmethod
from typing import Callable, Sequence, Union
import numpy as np
def flatten_non_category_dims(
xs: Union[np.ndarray, Sequence[np.ndarray]], category_dim: int = None
):
"""Flattens all non-category dimensions into a single dimension.
Args:
xs (ndarrays): Sequence of ndarrays with the same category dimension.
category_dim: The dimension/axis corresponding to different categories.
i.e. `C`. If `None`, behaves like `np.flatten(x)`.
Returns:
ndarray: Shape (C, -1) if `category_dim` specified else shape (-1,)
"""
single_item = isinstance(xs, np.ndarray)
if single_item:
xs = [xs]
if category_dim is not None:
dims = (xs[0].shape[category_dim], -1)
xs = (np.moveaxis(x, category_dim, 0).reshape(dims) for x in xs)
else:
xs = (x.flatten() for x in xs)
if single_item:
return list(xs)[0]
else:
return xs
class Metric(Callable, ABC):
"""Interface for new metrics.
A metric should be implemented as a callable with explicitly defined
arguments. In other words, metrics should not have `**kwargs` or `**args`
options in the `__call__` method.
While not explicitly constrained to the return type, metrics typically
return float value(s). The number of values returned corresponds to the
number of categories.
* metrics should have different name() for different functionality.
* `category_dim` duck type if metric can process multiple categories at
once.
To compute metrics:
.. code-block:: python
metric = Metric()
results = metric(...)
"""
def __init__(self, units: str = ""):
self.units = units
def name(self):
return type(self).__name__
def display_name(self):
"""Name to use for pretty printing and display purposes."""
name = self.name()
return "{} {}".format(name, self.units) if self.units else name
@abstractmethod
def __call__(self, *args, **kwargs):
pass
class HounsfieldUnits(Metric):
FULL_NAME = "Hounsfield Unit"
def __init__(self, units="hu"):
super().__init__(units)
def __call__(self, mask, x, category_dim: int = None):
mask = mask.astype(np.bool)
if category_dim is None:
return np.mean(x[mask])
assert category_dim == -1
num_classes = mask.shape[-1]
return np.array([np.mean(x[mask[..., c]]) for c in range(num_classes)])
def name(self):
return self.FULL_NAME
class CrossSectionalArea(Metric):
def __call__(self, mask, spacing=None, category_dim: int = None):
pixel_area = np.prod(spacing) if spacing else 1
mask = mask.astype(np.bool)
mask = flatten_non_category_dims(mask, category_dim)
return pixel_area * np.count_nonzero(mask, -1) / 100.0
def name(self):
if self.units:
return "Cross-sectional Area ({})".format(self.units)
else:
return "Cross-sectional Area"
def manifest_to_map(manifest, model_type):
"""Converts a manifest to a map of metric name to metric instance.
Args:
manifest (dict): A dictionary of metric name to metric instance.
Returns:
dict: A dictionary of metric name to metric instance.
"""
# TODO: hacky. Update this
figure_text_key = {}
for manifest_dict in manifest:
try:
key = manifest_dict["Level"]
except BaseException:
key = ".".join((manifest_dict["File"].split("/")[-1]).split(".")[:-1])
muscle_hu = f"{manifest_dict['Hounsfield Unit (muscle)']:.2f}"
muscle_area = f"{manifest_dict['Cross-sectional Area (cm^2) (muscle)']:.2f}"
vat_hu = f"{manifest_dict['Hounsfield Unit (vat)']:.2f}"
vat_area = f"{manifest_dict['Cross-sectional Area (cm^2) (vat)']:.2f}"
sat_hu = f"{manifest_dict['Hounsfield Unit (sat)']:.2f}"
sat_area = f"{manifest_dict['Cross-sectional Area (cm^2) (sat)']:.2f}"
imat_hu = f"{manifest_dict['Hounsfield Unit (imat)']:.2f}"
imat_area = f"{manifest_dict['Cross-sectional Area (cm^2) (imat)']:.2f}"
if model_type.model_name == "abCT_v0.0.1":
figure_text_key[key] = [
muscle_hu,
muscle_area,
imat_hu,
imat_area,
vat_hu,
vat_area,
sat_hu,
sat_area,
]
else:
figure_text_key[key] = [
muscle_hu,
muscle_area,
vat_hu,
vat_area,
sat_hu,
sat_area,
imat_hu,
imat_area,
]
return figure_text_key