Source code for monai.auto3dseg.analyzer

# Copyright (c) MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#     http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import time
from abc import ABC, abstractmethod
from copy import deepcopy
from typing import Any, Dict, List, Optional

import numpy as np
import torch

from monai.apps.utils import get_logger
from monai.auto3dseg.operations import Operations, SampleOperations, SummaryOperations
from monai.auto3dseg.utils import (
    concat_multikeys_to_dict,
    concat_val_to_np,
    get_foreground_image,
    get_foreground_label,
    get_label_ccp,
    verify_report_format,
)
from monai.bundle.config_parser import ConfigParser
from monai.bundle.utils import ID_SEP_KEY
from monai.data.meta_tensor import MetaTensor
from monai.transforms.transform import MapTransform
from monai.transforms.utils_pytorch_numpy_unification import sum, unique
from monai.utils.enums import ImageStatsKeys, LabelStatsKeys
from monai.utils.misc import ImageMetaKey, label_union

logger = get_logger(module_name=__name__)

__all__ = [
    "Analyzer",
    "ImageStats",
    "FgImageStats",
    "LabelStats",
    "ImageStatsSumm",
    "FgImageStatsSumm",
    "LabelStatsSumm",
    "FilenameStats",
]


class Analyzer(MapTransform, ABC):
    """
    The Analyzer component is a base class. Other classes inherit this class will provide a callable
    with the same class name and produces one pre-formatted dictionary for the input data. The format
    is pre-defined by the init function of the class that inherit this base class. Function operations
    can also be registered before the runtime of the callable.

    Args:
        report_format: a dictionary that outlines the key structures of the report format.

    """

    def __init__(self, stats_name: str, report_format: dict) -> None:
        super().__init__(None)
        parser = ConfigParser(report_format, globals=False)  # ConfigParser.globals not picklable
        self.report_format = parser.get("")
        self.stats_name = stats_name
        self.ops = ConfigParser({}, globals=False)

    def update_ops(self, key: str, op):
        """
        Register a statistical operation to the Analyzer and update the report_format.

        Args:
            key: value key in the report.
            op: Operation sub-class object that represents statistical operations.

        """
        self.ops[key] = op
        parser = ConfigParser(self.report_format)

        if parser.get(key, "None") != "None":
            parser[key] = op

        self.report_format = parser.get("")

    def update_ops_nested_label(self, nested_key: str, op):
        """
        Update operations for nested label format. Operation value in report_format will be resolved
        to a dict with only keys.

        Args:
            nested_key: str that has format of 'key1#0#key2'.
            op: Operation sub-class object that represents statistical operations.
        """
        keys = nested_key.split(ID_SEP_KEY)
        if len(keys) != 3:
            raise ValueError("Nested_key input format is wrong. Please ensure it is like key1#0#key2")
        root: str
        child_key: str
        (root, _, child_key) = keys
        if root not in self.ops:
            self.ops[root] = [{}]
        self.ops[root][0].update({child_key: None})

        self.ops[nested_key] = op

        parser = ConfigParser(self.report_format)
        if parser.get(nested_key, "NA") != "NA":
            parser[nested_key] = op

    def get_report_format(self):
        """
        Get the report format by resolving the registered operations recursively.

        Returns:
            a dictionary with {keys: None} pairs.

        """
        self.resolve_format(self.report_format)
        return self.report_format

    @staticmethod
    def unwrap_ops(func):
        """
        Unwrap a function value and generates the same set keys in a dict when the function is actually
        called in runtime

        Args:
            func: Operation sub-class object that represents statistical operations. The func object
                should have a `data` dictionary which stores the statistical operation information.
                For some operations (ImageStats for example), it may also contain the data_addon
                property, which is part of the update process.

        Returns:
            a dict with a set of keys.

        """
        ret = dict.fromkeys(list(func.data))
        if hasattr(func, "data_addon"):
            for key in func.data_addon:
                ret.update({key: None})
        return ret

    def resolve_format(self, report: dict):
        """
        Resolve the format of the pre-defined report.

        Args:
            report: the dictionary to resolve. Values will be replaced in-place.

        """
        for k, v in report.items():
            if isinstance(v, Operations):
                report[k] = self.unwrap_ops(v)
            elif isinstance(v, list) and len(v) > 0:
                self.resolve_format(v[0])
            else:
                report[k] = v

    @abstractmethod
    def __call__(self, data: Any):
        """Analyze the dict format dataset, return the summary report"""
        raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")


[docs]class ImageStats(Analyzer): """ Analyzer to extract image stats properties for each case(image). Args: image_key: the key to find image data in the callable function input (data) meta_key_postfix: the postfix to append for meta_dict ("image_meta_dict"). Examples: .. code-block:: python import numpy as np from monai.auto3dseg.analyzer import ImageStats input = {} input['image'] = np.random.rand(1,30,30,30) input['image_meta_dict'] = {'affine': np.eye(4)} analyzer = ImageStats(image_key="image") print(analyzer(input)) """ def __init__( self, image_key: str, stats_name: str = "image_stats", meta_key_postfix: Optional[str] = "meta_dict" ) -> None: if not isinstance(image_key, str): raise ValueError("image_key input must be str") self.image_key = image_key self.image_meta_key = f"{self.image_key}_{meta_key_postfix}" report_format = { ImageStatsKeys.SHAPE: None, ImageStatsKeys.CHANNELS: None, ImageStatsKeys.CROPPED_SHAPE: None, ImageStatsKeys.SPACING: None, ImageStatsKeys.INTENSITY: None, } super().__init__(stats_name, report_format) self.update_ops(ImageStatsKeys.INTENSITY, SampleOperations()) def __call__(self, data): """ Callable to execute the pre-defined functions Returns: A dictionary. The dict has the key in self.report_format. The value of ImageStatsKeys.INTENSITY is in a list format. Each element of the value list has stats pre-defined by SampleOperations (max, min, ....). Raises: RuntimeError if the stats report generated is not consistent with the pre- defined report_format. Note: The stats operation uses numpy and torch to compute max, min, and other functions. If the input has nan/inf, the stats results will be nan/inf. """ d = dict(data) start = time.time() ndas = data[self.image_key] ndas = [ndas[i] for i in range(ndas.shape[0])] if "nda_croppeds" not in d: nda_croppeds = [get_foreground_image(nda) for nda in ndas] # perform calculation report = deepcopy(self.get_report_format()) report[ImageStatsKeys.SHAPE] = [list(nda.shape) for nda in ndas] report[ImageStatsKeys.CHANNELS] = len(ndas) report[ImageStatsKeys.CROPPED_SHAPE] = [list(nda_c.shape) for nda_c in nda_croppeds] report[ImageStatsKeys.SPACING] = np.tile( np.diag(data[self.image_meta_key]["affine"])[:3], [len(ndas), 1] ).tolist() report[ImageStatsKeys.INTENSITY] = [ self.ops[ImageStatsKeys.INTENSITY].evaluate(nda_c) for nda_c in nda_croppeds ] if not verify_report_format(report, self.get_report_format()): raise RuntimeError(f"report generated by {self.__class__} differs from the report format.") logger.debug(f"Get image stats spent {time.time()-start}") d[self.stats_name] = report return d
[docs]class FgImageStats(Analyzer): """ Analyzer to extract foreground label properties for each case(image and label). Args: image_key: the key to find image data in the callable function input (data) label_key: the key to find label data in the callable function input (data) Examples: .. code-block:: python import numpy as np from monai.auto3dseg.analyzer import FgImageStats input = {} input['image'] = np.random.rand(1,30,30,30) input['label'] = np.ones([30,30,30]) analyzer = FgImageStats(image_key='image', label_key='label') print(analyzer(input)) """ def __init__(self, image_key: str, label_key: str, stats_name: str = "image_foreground_stats"): self.image_key = image_key self.label_key = label_key report_format = {ImageStatsKeys.INTENSITY: None} super().__init__(stats_name, report_format) self.update_ops(ImageStatsKeys.INTENSITY, SampleOperations()) def __call__(self, data) -> dict: """ Callable to execute the pre-defined functions Returns: A dictionary. The dict has the key in self.report_format and value in a list format. Each element of the value list has stats pre-defined by SampleOperations (max, min, ....). Raises: RuntimeError if the stats report generated is not consistent with the pre- defined report_format. Note: The stats operation uses numpy and torch to compute max, min, and other functions. If the input has nan/inf, the stats results will be nan/inf. """ d = dict(data) ndas = d[self.image_key] # (1,H,W,D) or (C,H,W,D) ndas = [ndas[i] for i in range(ndas.shape[0])] ndas_label = d[self.label_key] # (H,W,D) nda_foregrounds = [get_foreground_label(nda, ndas_label) for nda in ndas] # perform calculation report = deepcopy(self.get_report_format()) report[ImageStatsKeys.INTENSITY] = [ self.ops[ImageStatsKeys.INTENSITY].evaluate(nda_f) for nda_f in nda_foregrounds ] if not verify_report_format(report, self.get_report_format()): raise RuntimeError(f"report generated by {self.__class__} differs from the report format.") d[self.stats_name] = report return d
[docs]class LabelStats(Analyzer): """ Analyzer to extract label stats properties for each case(image and label). Args: image_key: the key to find image data in the callable function input (data) label_key: the key to find label data in the callable function input (data) do_ccp: performs connected component analysis. Default is True. Examples: .. code-block:: python import numpy as np from monai.auto3dseg.analyzer import LabelStats input = {} input['image'] = np.random.rand(1,30,30,30) input['label'] = np.ones([30,30,30]) analyzer = LabelStats(image_key='image', label_key='label') print(analyzer(input)) """ def __init__(self, image_key: str, label_key: str, stats_name: str = "label_stats", do_ccp: Optional[bool] = True): self.image_key = image_key self.label_key = label_key self.do_ccp = do_ccp report_format: Dict[str, Any] = { LabelStatsKeys.LABEL_UID: None, LabelStatsKeys.IMAGE_INTST: None, LabelStatsKeys.LABEL: [{LabelStatsKeys.PIXEL_PCT: None, LabelStatsKeys.IMAGE_INTST: None}], } if self.do_ccp: report_format[LabelStatsKeys.LABEL][0].update( {LabelStatsKeys.LABEL_SHAPE: None, LabelStatsKeys.LABEL_NCOMP: None} ) super().__init__(stats_name, report_format) self.update_ops(LabelStatsKeys.IMAGE_INTST, SampleOperations()) id_seq = ID_SEP_KEY.join([LabelStatsKeys.LABEL, "0", LabelStatsKeys.IMAGE_INTST]) self.update_ops_nested_label(id_seq, SampleOperations()) def __call__(self, data): """ Callable to execute the pre-defined functions. Returns: A dictionary. The dict has the key in self.report_format and value in a list format. Each element of the value list has stats pre-defined by SampleOperations (max, min, ....). Examples: output dict contains { LabelStatsKeys.LABEL_UID:[0,1,3], LabelStatsKeys.IMAGE_INTST: {...}, LabelStatsKeys.LABEL:[ { LabelStatsKeys.PIXEL_PCT: 0.8, LabelStatsKeys.IMAGE_INTST: {...}, LabelStatsKeys.LABEL_SHAPE: [...], LabelStatsKeys.LABEL_NCOMP: 1 } { LabelStatsKeys.PIXEL_PCT: 0.1, LabelStatsKeys.IMAGE_INTST: {...}, LabelStatsKeys.LABEL_SHAPE: [...], LabelStatsKeys.LABEL_NCOMP: 1 } { LabelStatsKeys.PIXEL_PCT: 0.1, LabelStatsKeys.IMAGE_INTST: {...}, LabelStatsKeys.LABEL_SHAPE: [...], LabelStatsKeys.LABEL_NCOMP: 1 } ] } Raises: RuntimeError if the stats report generated is not consistent with the pre- defined report_format. Notes: The label class_ID of the dictionary in LabelStatsKeys.LABEL IS NOT the index. Instead, the class_ID is the LabelStatsKeys.LABEL_UID with the same index. For instance, the last dict in LabelStatsKeys.LABEL in the Examples is 3, which is the last element under LabelStatsKeys.LABEL_UID. The stats operation uses numpy and torch to compute max, min, and other functions. If the input has nan/inf, the stats results will be nan/inf. """ d = dict(data) ndas = d[self.image_key] # (1,H,W,D) or (C,H,W,D) ndas = [ndas[i] for i in range(ndas.shape[0])] ndas_label = d[self.label_key] # (H,W,D) nda_foregrounds = [get_foreground_label(nda, ndas_label) for nda in ndas] unique_label = unique(ndas_label) if isinstance(ndas_label, (MetaTensor, torch.Tensor)): unique_label = unique_label.data.cpu().numpy() unique_label = unique_label.astype(np.int8).tolist() start = time.time() label_substats = [] # each element is one label pixel_sum = 0 pixel_arr = [] for index in unique_label: start_label = time.time() label_dict: Dict[str, Any] = {} mask_index = ndas_label == index label_dict[LabelStatsKeys.IMAGE_INTST] = [ self.ops[LabelStatsKeys.IMAGE_INTST].evaluate(nda[mask_index]) for nda in ndas ] pixel_count = sum(mask_index) pixel_arr.append(pixel_count) pixel_sum += pixel_count if self.do_ccp: # apply connected component shape_list, ncomponents = get_label_ccp(mask_index) label_dict[LabelStatsKeys.LABEL_SHAPE] = shape_list label_dict[LabelStatsKeys.LABEL_NCOMP] = ncomponents label_substats.append(label_dict) logger.debug(f" label {index} stats takes {time.time() - start_label}") for i, _ in enumerate(unique_label): label_substats[i].update({LabelStatsKeys.PIXEL_PCT: float(pixel_arr[i] / pixel_sum)}) report = deepcopy(self.get_report_format()) report[LabelStatsKeys.LABEL_UID] = unique_label report[LabelStatsKeys.IMAGE_INTST] = [ self.ops[LabelStatsKeys.IMAGE_INTST].evaluate(nda_f) for nda_f in nda_foregrounds ] report[LabelStatsKeys.LABEL] = label_substats if not verify_report_format(report, self.get_report_format()): raise RuntimeError(f"report generated by {self.__class__} differs from the report format.") d[self.stats_name] = report logger.debug(f"Get label stats spent {time.time()-start}") return d
[docs]class ImageStatsSumm(Analyzer): """ This summary analyzer processes the values of specific key `stats_name` in a list of dict. Typically, the list of dict is the output of case analyzer under the same prefix (ImageStats). Args: stats_name: the key of the to-process value in the dict. average: whether to average the statistical value across different image modalities. """ def __init__(self, stats_name: str = "image_stats", average: Optional[bool] = True): self.summary_average = average report_format = { ImageStatsKeys.SHAPE: None, ImageStatsKeys.CHANNELS: None, ImageStatsKeys.CROPPED_SHAPE: None, ImageStatsKeys.SPACING: None, ImageStatsKeys.INTENSITY: None, } super().__init__(stats_name, report_format) self.update_ops(ImageStatsKeys.SHAPE, SampleOperations()) self.update_ops(ImageStatsKeys.CHANNELS, SampleOperations()) self.update_ops(ImageStatsKeys.CROPPED_SHAPE, SampleOperations()) self.update_ops(ImageStatsKeys.SPACING, SampleOperations()) self.update_ops(ImageStatsKeys.INTENSITY, SummaryOperations()) def __call__(self, data: List[Dict]): """ Callable to execute the pre-defined functions Returns: A dictionary. The dict has the key in self.report_format and value in a list format. Each element of the value list has stats pre-defined by SampleOperations (max, min, ....). Raises: RuntimeError if the stats report generated is not consistent with the pre- defined report_format. Examples: output dict contains a dictionary for all of the following keys{ ImageStatsKeys.SHAPE:{...} ImageStatsKeys.CHANNELS: {...}, ImageStatsKeys.CROPPED_SHAPE: {...}, ImageStatsKeys.SPACING: {...}, ImageStatsKeys.INTENSITY: {...}, } Notes: The stats operation uses numpy and torch to compute max, min, and other functions. If the input has nan/inf, the stats results will be nan/inf. """ if not isinstance(data, list): return ValueError(f"Callable {self.__class__} requires list inputs") if len(data) == 0: return ValueError(f"Callable {self.__class__} input list is empty") if self.stats_name not in data[0]: return KeyError(f"{self.stats_name} is not in input data") report = deepcopy(self.get_report_format()) for k in [ImageStatsKeys.SHAPE, ImageStatsKeys.CHANNELS, ImageStatsKeys.CROPPED_SHAPE, ImageStatsKeys.SPACING]: v_np = concat_val_to_np(data, [self.stats_name, k]) report[k] = self.ops[k].evaluate(v_np, dim=(0, 1) if v_np.ndim > 2 and self.summary_average else 0) intst_str = ImageStatsKeys.INTENSITY op_keys = report[intst_str].keys() # template, max/min/... intst_dict = concat_multikeys_to_dict(data, [self.stats_name, intst_str], op_keys) report[intst_str] = self.ops[intst_str].evaluate(intst_dict, dim=None if self.summary_average else 0) if not verify_report_format(report, self.get_report_format()): raise RuntimeError(f"report generated by {self.__class__} differs from the report format.") return report
[docs]class FgImageStatsSumm(Analyzer): """ This summary analyzer processes the values of specific key `stats_name` in a list of dict. Typically, the list of dict is the output of case analyzer under the similar name (FgImageStats). Args: stats_name: the key of the to-process value in the dict. average: whether to average the statistical value across different image modalities. """ def __init__(self, stats_name: str = "image_foreground_stats", average: Optional[bool] = True): self.summary_average = average report_format = {ImageStatsKeys.INTENSITY: None} super().__init__(stats_name, report_format) self.update_ops(ImageStatsKeys.INTENSITY, SummaryOperations()) def __call__(self, data: List[Dict]): """ Callable to execute the pre-defined functions. Returns: A dictionary. The dict has the key in self.report_format and value in a list format. Each element of the value list has stats pre-defined by SampleOperations (max, min, ....) and SummaryOperation (max of the max, mean of the mean, etc). Raises: RuntimeError if the stats report generated is not consistent with the pre- defined report_format. Examples: output dict contains a dictionary for all of the following keys{ ImageStatsKeys.INTENSITY: {...}, } Notes: The stats operation uses numpy and torch to compute max, min, and other functions. If the input has nan/inf, the stats results will be nan/inf. """ if not isinstance(data, list): return ValueError(f"Callable {self.__class__} requires list inputs") if len(data) == 0: return ValueError(f"Callable {self.__class__} input list is empty") if self.stats_name not in data[0]: return KeyError(f"{self.stats_name} is not in input data.") report = deepcopy(self.get_report_format()) intst_str = ImageStatsKeys.INTENSITY op_keys = report[intst_str].keys() # template, max/min/... intst_dict = concat_multikeys_to_dict(data, [self.stats_name, intst_str], op_keys) report[intst_str] = self.ops[intst_str].evaluate(intst_dict, dim=None if self.summary_average else 0) if not verify_report_format(report, self.get_report_format()): raise RuntimeError(f"report generated by {self.__class__} differs from the report format.") return report
[docs]class LabelStatsSumm(Analyzer): """ This summary analyzer processes the values of specific key `stats_name` in a list of dict. Typically, the list of dict is the output of case analyzer under the similar name (LabelStats). Args: stats_name: the key of the to-process value in the dict. average: whether to average the statistical value across different image modalities. """ def __init__(self, stats_name: str = "label_stats", average: Optional[bool] = True, do_ccp: Optional[bool] = True): self.summary_average = average self.do_ccp = do_ccp report_format: Dict[str, Any] = { LabelStatsKeys.LABEL_UID: None, LabelStatsKeys.IMAGE_INTST: None, LabelStatsKeys.LABEL: [{LabelStatsKeys.PIXEL_PCT: None, LabelStatsKeys.IMAGE_INTST: None}], } if self.do_ccp: report_format[LabelStatsKeys.LABEL][0].update( {LabelStatsKeys.LABEL_SHAPE: None, LabelStatsKeys.LABEL_NCOMP: None} ) super().__init__(stats_name, report_format) self.update_ops(LabelStatsKeys.IMAGE_INTST, SummaryOperations()) # label-0-'pixel percentage' id_seq = ID_SEP_KEY.join([LabelStatsKeys.LABEL, "0", LabelStatsKeys.PIXEL_PCT]) self.update_ops_nested_label(id_seq, SampleOperations()) # label-0-'image intensity' id_seq = ID_SEP_KEY.join([LabelStatsKeys.LABEL, "0", LabelStatsKeys.IMAGE_INTST]) self.update_ops_nested_label(id_seq, SummaryOperations()) # label-0-shape id_seq = ID_SEP_KEY.join([LabelStatsKeys.LABEL, "0", LabelStatsKeys.LABEL_SHAPE]) self.update_ops_nested_label(id_seq, SampleOperations()) # label-0-ncomponents id_seq = ID_SEP_KEY.join([LabelStatsKeys.LABEL, "0", LabelStatsKeys.LABEL_NCOMP]) self.update_ops_nested_label(id_seq, SampleOperations()) def __call__(self, data: List[Dict]): """ Callable to execute the pre-defined functions Returns: A dictionary. The dict has the key in self.report_format and value in a list format. Each element of the value list has stats pre-defined by SampleOperations (max, min, ....) and SummaryOperation (max of the max, mean of the mean, etc). Raises: RuntimeError if the stats report generated is not consistent with the pre- defined report_format. Notes: The stats operation uses numpy and torch to compute max, min, and other functions. If the input has nan/inf, the stats results will be nan/inf. """ if not isinstance(data, list): return ValueError(f"Callable {self.__class__} requires list inputs") if len(data) == 0: return ValueError(f"Callable {self.__class__} input list is empty") if self.stats_name not in data[0]: return KeyError(f"{self.stats_name} is not in input data") report = deepcopy(self.get_report_format()) # unique class ID uid_np = concat_val_to_np(data, [self.stats_name, LabelStatsKeys.LABEL_UID], axis=None, ragged=True) unique_label = label_union(uid_np) report[LabelStatsKeys.LABEL_UID] = unique_label # image intensity intst_str = LabelStatsKeys.IMAGE_INTST op_keys = report[intst_str].keys() # template, max/min/... intst_dict = concat_multikeys_to_dict(data, [self.stats_name, intst_str], op_keys) report[intst_str] = self.ops[intst_str].evaluate(intst_dict, dim=None if self.summary_average else 0) detailed_label_list = [] # iterate through each label label_str = LabelStatsKeys.LABEL for label_id in unique_label: stats = {} pct_str = LabelStatsKeys.PIXEL_PCT pct_fixed_keys = [self.stats_name, label_str, label_id, pct_str] pct_np = concat_val_to_np(data, pct_fixed_keys, allow_missing=True) stats[pct_str] = self.ops[label_str][0][pct_str].evaluate( pct_np, dim=(0, 1) if pct_np.ndim > 2 and self.summary_average else 0 ) if self.do_ccp: ncomp_str = LabelStatsKeys.LABEL_NCOMP ncomp_fixed_keys = [self.stats_name, LabelStatsKeys.LABEL, label_id, ncomp_str] ncomp_np = concat_val_to_np(data, ncomp_fixed_keys, allow_missing=True) stats[ncomp_str] = self.ops[label_str][0][ncomp_str].evaluate( ncomp_np, dim=(0, 1) if ncomp_np.ndim > 2 and self.summary_average else 0 ) shape_str = LabelStatsKeys.LABEL_SHAPE shape_fixed_keys = [self.stats_name, label_str, label_id, LabelStatsKeys.LABEL_SHAPE] shape_np = concat_val_to_np(data, shape_fixed_keys, ragged=True, allow_missing=True) stats[shape_str] = self.ops[label_str][0][shape_str].evaluate( shape_np, dim=(0, 1) if shape_np.ndim > 2 and self.summary_average else 0 ) # label shape is a 3-element value, but the number of labels in each image # can vary from 0 to N. So the value in a list format is "ragged" intst_str = LabelStatsKeys.IMAGE_INTST intst_fixed_keys = [self.stats_name, label_str, label_id, intst_str] op_keys = report[label_str][0][intst_str].keys() intst_dict = concat_multikeys_to_dict(data, intst_fixed_keys, op_keys, allow_missing=True) stats[intst_str] = self.ops[label_str][0][intst_str].evaluate( intst_dict, dim=None if self.summary_average else 0 ) detailed_label_list.append(stats) report[LabelStatsKeys.LABEL] = detailed_label_list if not verify_report_format(report, self.get_report_format()): raise RuntimeError(f"report generated by {self.__class__} differs from the report format.") return report
[docs]class FilenameStats(Analyzer): """ This class finds the file path for the loaded image/label and writes the info into the data pipeline as a monai transforms. Args: key: the key to fetch the filename (for example, "image", "label"). stats_name: the key to store the filename in the output stats report. """ def __init__(self, key: str, stats_name: str, meta_key_postfix: Optional[str] = "meta_dict") -> None: self.key = key self.meta_key = None if key is None else f"{key}_{meta_key_postfix}" super().__init__(stats_name, {}) def __call__(self, data): d = dict(data) if self.meta_key: if self.key not in d: # check whether image/label is in the data raise ValueError(f"Data with key {self.key} is missing ") if self.meta_key not in d: raise ValueError(f"Meta data with key {self.meta_key} is missing") d[self.stats_name] = d[self.meta_key][ImageMetaKey.FILENAME_OR_OBJ] else: d[self.stats_name] = "None" return d