content: #    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):
          if len(input_list) == 1:
              return True
          else:
              # compare all entries to the first
              return np.allclose(input_list[0], input_list[1:])
  
      @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