# Copyright 2021 HIP Applied Computer Vision Lab, Division of Medical Image Computing, German Cancer Research Center # (DKFZ), Heidelberg, Germany # # 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. from abc import ABC, abstractmethod from typing import Tuple, Union, List import numpy as np class BaseReaderWriter(ABC): @staticmethod def _check_all_same(input_list): # compare all entries to the first for i in input_list[1:]: if i != input_list[0]: return False return True @staticmethod def _check_all_same_array(input_list): # compare all entries to the first for i in input_list[1:]: if i.shape != input_list[0].shape or not np.allclose(i, input_list[0]): return False return True @abstractmethod def read_images(self, image_fnames: Union[List[str], Tuple[str, ...]]) -> Tuple[np.ndarray, dict]: """ Reads a sequence of images and returns a 4d (!) np.ndarray along with a dictionary. The 4d array must have the modalities (or color channels, or however you would like to call them) in its first axis, followed by the spatial dimensions (so shape must be c,x,y,z where c is the number of modalities (can be 1)). Use the dictionary to store necessary meta information that is lost when converting to numpy arrays, for example the Spacing, Orientation and Direction of the image. This dictionary will be handed over to write_seg for exporting the predicted segmentations, so make sure you have everything you need in there! IMPORTANT: dict MUST have a 'spacing' key with a tuple/list of length 3 with the voxel spacing of the np.ndarray. Example: my_dict = {'spacing': (3, 0.5, 0.5), ...}. This is needed for planning and preprocessing. The ordering of the numbers must correspond to the axis ordering in the returned numpy array. So if the array has shape c,x,y,z and the spacing is (a,b,c) then a must be the spacing of x, b the spacing of y and c the spacing of z. In the case of 2D images, the returned array should have shape (c, 1, x, y) and the spacing should be (999, sp_x, sp_y). Make sure 999 is larger than sp_x and sp_y! Example: shape=(3, 1, 224, 224), spacing=(999, 1, 1) For images that don't have a spacing, set the spacing to 1 (2d exception with 999 for the first axis still applies!) :param image_fnames: :return: 1) a np.ndarray of shape (c, x, y, z) where c is the number of image channels (can be 1) and x, y, z are the spatial dimensions (set x=1 for 2D! Example: (3, 1, 224, 224) for RGB image). 2) a dictionary with metadata. This can be anything. BUT it HAS to include a {'spacing': (a, b, c)} where a is the spacing of x, b of y and c of z! If an image doesn't have spacing, just set this to 1. For 2D, set a=999 (largest spacing value! Make it larger than b and c) """ pass @abstractmethod def read_seg(self, seg_fname: str) -> Tuple[np.ndarray, dict]: """ Same requirements as BaseReaderWriter.read_image. Returned segmentations must have shape 1,x,y,z. Multiple segmentations are not (yet?) allowed If images and segmentations can be read the same way you can just `return self.read_image((image_fname,))` :param seg_fname: :return: 1) a np.ndarray of shape (1, x, y, z) where x, y, z are the spatial dimensions (set x=1 for 2D! Example: (1, 1, 224, 224) for 2D segmentation). 2) a dictionary with metadata. This can be anything. BUT it HAS to include a {'spacing': (a, b, c)} where a is the spacing of x, b of y and c of z! If an image doesn't have spacing, just set this to 1. For 2D, set a=999 (largest spacing value! Make it larger than b and c) """ pass @abstractmethod def write_seg(self, seg: np.ndarray, output_fname: str, properties: dict) -> None: """ Export the predicted segmentation to the desired file format. The given seg array will have the same shape and orientation as the corresponding image data, so you don't need to do any resampling or whatever. Just save :-) properties is the same dictionary you created during read_images/read_seg so you can use the information here to restore metadata IMPORTANT: Segmentations are always 3D! If your input images were 2d then the segmentation will have shape 1,x,y. You need to catch that and export accordingly (for 2d images you need to convert the 3d segmentation to 2d via seg = seg[0])! :param seg: A segmentation (np.ndarray, integer) of shape (x, y, z). For 2D segmentations this will be (1, y, z)! :param output_fname: :param properties: the dictionary that you created in read_images (the ones this segmentation is based on). Use this to restore metadata :return: """ pass