Repository: imi-bigpicture/wsidicom (language: python)

Issue:
Save support
There is a need to be able to save the wsi object to file for various reasons:

- When adding annotations to a slide, saving the annotation instance to file.
- When converting from another format, saving the full wsi object to a new set of DICOM files.
- Other needs?

For conversion/saving, it is likely best to be able to this on a tile-by-tile basis whenever possible. We could then use another library, that can open and provide tiles (without transcoding) from the wsi, and use this to build the DICOM compatible pyramid. As we don't need to do any decompression/compression, the conversion will be fast and lossless. 

For formats that are not native tiled, we to also need to tile the image data. We might be able to also do this lossless, but it will be more complicated.

Converting image data and adding the associated technical metadata is likely the easier part of converting to DICOM. We also need to add required DICOM data such as:

- [Specimen description](https://dicom.innolitics.com/ciods/vl-whole-slide-microscopy-image/specimen/00400560) includes preparatin/staining etc.
- [Optical paths](https://dicom.innolitics.com/ciods/vl-whole-slide-microscopy-image/optical-path) includes illumination type, etc.

Although we will likely support opening of DICOM WSI files of different types (e.g. sparse of full tiled, concatenated or single file) per level it is probably best to limit the write support to what we think is "best practice" (full tiled, single file per level?). 


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 3565c196..8b075651 100644
--- a/README.md
+++ b/README.md
@@ -65,8 +65,10 @@ slide.close()
 
 ## Data structure
 A WSI DICOM pyramid is in *wsidicom* represented by a hierarchy of objects of different classes, starting from bottom:
-- *WsiDicomFile*, represents a WSI DICOM file. A file can contain one or several focal planes and/or optical paths.
-- *WsiDicomInstance*, reperesents a single or a concatenation of WSI DICOM files.
+- *WsiDicomFile*, represents a WSI DICOM file, used for accessing DicomImageData and WsiDataset.
+- *DicomImageData*, represents the image data in one or several WSI DICOM files.
+- *WsiDataset*, represents the image metadata in one or several WSI DICOM files.
+- *WsiInstance*, represents image data and image metadata.
 - *WsiDicomLevel*, represents a group of instances with the same image size, i.e. of the same level.
 - *WsiDicomLevels*, represents a group of levels, i.e. the pyrimidal structure.
 - *WsiDicom*, represents a collection of levels, labels and overviews.
@@ -81,12 +83,21 @@ slide = WsiDicom.open(path_to_folder)
 But the structure can also be created manually from the bottom:
 ```python
 file = WsiDicomFile(path_to_file)
-instance = WsiDicomInstance([file])
+instance = WsiInstance(file.dataset, DicomImageData(files))
 level = WsiDicomLevel([instance])
 levels = WsiDicomLevels([level])
 slide = WsiDicom([levels])
 ```
 
+## Adding support for other file formats.
+By subclassing *ImageData* and implementing the required properties (transfer_syntax, image_size, tile_size, and pixel_spacing) and methods (get_tile() and close()) *wsidicom* can be used to access wsi images in other file formats than DICOM. In addition to a ImageData-object, image data, specified in a DICOM dataset, must also be created. For example, assuming a implementation of MyImageData exists that takes a path to a image file as argument and create_dataset() produces a DICOM dataset (see is_wsi_dicom() of WsiDataset for required attributes), WsiInstancees could be created for each pyramidal level, label, or overview:
+```python
+image_data = MyImageData('path_to_image_file')
+dataset = create_dataset()
+instance = WsiInstance(dataset, image_data)
+```
+The created instances can then be arranged into levels etc, and opened as a WsiDicom-object as described in 'Data structure'.
+
 ## Annotation usage
 Annotations are structured in a hierarchy:
 - AnnotationInstance
diff --git a/wsidicom/conceptcode.py b/wsidicom/conceptcode.py
new file mode 100644
index 00000000..8efa5022
--- /dev/null
+++ b/wsidicom/conceptcode.py
@@ -0,0 +1,425 @@
+from dataclasses import dataclass
+from typing import List, Dict, Optional
+from abc import ABCMeta, abstractmethod
+
+from pydicom.dataset import Dataset
+from pydicom.sequence import Sequence as DicomSequence
+from pydicom.sr.codedict import codes
+from pydicom.sr.coding import Code
+
+
+@dataclass
+class ConceptCode(metaclass=ABCMeta):
+    """Help functions for handling SR codes.
+    Provides functions for converting between Code and dicom dataset.
+    For CIDs that are not-yet standardized, functions for creating Code from
+    code meaning is provided using the CID definitions in the sup 222 draft.
+    For standardized CIDs one can use pydicom.sr.codedict to create codes."""
+    sequence_name: str
+
+    def __init__(
+        self,
+        meaning: str,
+        value: str,
+        scheme_designator: str,
+        scheme_version: str = None
+    ):
+        self.meaning = meaning
+        self.value = value
+        self.scheme_designator = scheme_designator
+        self.scheme_version = scheme_version
+
+    def __hash__(self):
+        return hash((
+            self.value,
+            self.scheme_designator,
+            self.meaning,
+            self.scheme_version
+        ))
+
+    @classmethod
+    @abstractmethod
+    def from_ds(cls, ds: Dataset):
+        raise NotImplementedError
+
+    @classmethod
+    @abstractmethod
+    def list(cls) -> List[str]:
+        raise NotImplementedError
+
+    @property
+    def code(self) -> Code:
+        return Code(
+            value=self.value,
+            scheme_designator=self.scheme_designator,
+            meaning=self.meaning,
+            scheme_version=self.scheme_version
+        )
+
+    def to_ds(self) -> Dataset:
+        """Codes code into DICOM dataset.
+
+        Returns
+        ----------
+        Dataset
+            Dataset of code.
+
+        """
+        ds = Dataset()
+        ds.CodeValue = self.value
+        ds.CodingSchemeDesignator = self.scheme_designator
+        ds.CodeMeaning = self.meaning
+        if self.scheme_version is not None:
+            ds.CodeSchemeVersion = self.scheme_version
+        return ds
+
+    def insert_into_ds(self, ds: Dataset) -> Dataset:
+        """Codes and insert object into sequence in dataset.
+
+        Parameters
+        ----------
+        ds: Dataset
+           Dataset to insert into.
+
+        Returns
+        ----------
+        Dataset
+            Dataset with object inserted.
+
+        """
+        # Append if sequence already set otherwise create
+        try:
+            sequence = getattr(ds, self.sequence_name)
+            sequence.append(self.to_ds())
+        except AttributeError:
+            setattr(ds, self.sequence_name, DicomSequence([self.to_ds()]))
+        return ds
+
+    @classmethod
+    def _from_ds(
+        cls,
+        ds: Dataset,
+    ) -> Optional[List[Code]]:
+        """Return list of ConceptCode from sequence in dataset.
+
+        Parameters
+        ----------
+        ds: Dataset
+            Datasete containing code sequence.
+
+        Returns
+        ----------
+        List[ConceptCode]
+            Codes created from sequence in dataset.
+
+        """
+        if cls.sequence_name not in ds:
+            return None
+        return [
+            cls(
+                value=code_ds.CodeValue,
+                scheme_designator=code_ds.CodingSchemeDesignator,
+                meaning=code_ds.CodeMeaning,
+                scheme_version=getattr(code_ds, 'CodeSchemeVersion', None)
+            )
+            for code_ds in getattr(ds, cls.sequence_name)
+        ]
+
+
+class SingleConceptCode(ConceptCode):
+    """Code for concepts that only allow a single item"""
+    @classmethod
+    def from_ds(cls, ds: Dataset) -> Optional['ConceptCode']:
+        """Return measurement code for value. Value can be a code meaning (str)
+        or a DICOM dataset containing the code.
+
+        Parameters
+        ----------
+        value: Union[str, Dataset]
+            The value for creating the code.
+
+        Returns
+        ----------
+        ConceptCode
+            Measurement code created from value.
+
+        """
+        codes = cls._from_ds(ds)
+        if codes is None:
+            return None
+        return codes[0]
+
+
+class MultipleConceptCode(ConceptCode):
+    """Code for concepts that allow multiple items"""
+    @classmethod
+    def from_ds(cls, ds: Dataset) -> Optional[List['ConceptCode']]:
+        """Return measurement code for value. Value can be a code meaning (str)
+        or a DICOM dataset containing the code.
+
+        Parameters
+        ----------
+        value: Union[str, Dataset]
+            The value for creating the code.
+
+        Returns
+        ----------
+        ConceptCode
+            Measurement code created from value.
+
+        """
+        return cls._from_ds(ds)
+
+
+class CidConceptCode(ConceptCode, metaclass=ABCMeta):
+    """Code for concepts defined in Context groups"""
+    cid: Dict[str, Code]
+
+    def __init__(
+        self,
+        meaning: str,
+        value: str = None,
+        scheme_designator: str = None,
+        scheme_version: str = None
+    ):
+        if value is None or scheme_designator is None:
+            code = self._from_cid(meaning)
+        else:
+            code = Code(value, scheme_designator, meaning, scheme_version)
+        super().__init__(
+            meaning=code.meaning,
+            value=code.value,
+            scheme_designator=code.scheme_designator,
+            scheme_version=code.scheme_version
+        )
+
+    @classmethod
+    def _from_cid(cls, meaning: str) -> Code:
+        """Return ConceptCode from CID and meaning. For a list of CIDs, see
+        http://dicom.nema.org/medical/dicom/current/output/chtml/part16/chapter_B.html # noqa
+
+        Parameters
+        ----------
+        meaning: str
+            Code meaning to get.
+
+        Returns
+        ----------
+        ConceptCode
+            Code created from CID and meaning.
+
+        """
+        # The keys are camelcase meanings, dont use.
+        for code in cls.cid.values():
+            if code.meaning == meaning:
+                return code
+        raise ValueError("Unsupported code")
+
+    @classmethod
+    def list(cls) -> List[str]:
+        """Return possible meanings for concept.
+
+        Returns
+        ----------
+        List[str]
+            Possible meanings for concept.
+
+        """
+        return [code.meaning for code in cls.cid.values()]
+
+
+class UnitCode(SingleConceptCode):
+    """Code for concepts representing units according to UCUM scheme"""
+    sequence_name = 'MeasurementUnitsCodeSequence'
+
+    def __init__(
+        self,
+        meaning: str,
+        value: str = None,
+        scheme_designator: str = None,
+        scheme_version: str = None
+    ):
+        if value is None or scheme_designator is None:
+            code = self._from_ucum(meaning)
+        else:
+            code = Code(value, scheme_designator, meaning, scheme_version)
+        super().__init__(
+            meaning=code.meaning,
+            value=code.value,
+            scheme_designator=code.scheme_designator,
+            scheme_version=code.scheme_version
+        )
+
+    @classmethod
+    def _from_ucum(cls, unit: str) -> Code:
+        """Return UCUM scheme ConceptCode.
+
+        Parameters
+        ----------
+        meaning: str
+            Code meaning.
+
+        Returns
+        ----------
+        ConceptCode
+            Code created from meaning.
+
+        """
+        return Code(
+            value=unit,
+            scheme_designator='UCUM',
+            meaning=unit
+        )
+
+    @classmethod
+    def measnings(cls) -> List[str]:
+        """Return possible meanings for concept.
+
+        Returns
+        ----------
+        List[str]
+            Possible meanings for concept.
+
+        """
+        return []
+
+
+class MeasurementCode(CidConceptCode, SingleConceptCode):
+    """
+    Concept code for measurement type.
+    Microscopy Measurement Types
+    """
+    sequence_name = 'ConceptNameCodeSequence'
+    cid = {'Area': Code('42798000', 'SCT', 'Area')}
+
+
+class AnnotationTypeCode(CidConceptCode, SingleConceptCode):
+    """
+    Concept code for annotation type.
+    Microscopy Annotation Property Types
+    """
+    sequence_name = 'AnnotationPropertyTypeCodeSequence'
+    cid = {
+        'Nucleus': Code('84640000', 'SCT', 'Nucleus'),
+        'EntireCell': Code('362837007', 'SCT', 'Entire cell')
+    }
+
+
+class AnnotationCategoryCode(CidConceptCode, SingleConceptCode):
+    """
+    Concept code for annotation category.
+    From CID 7150 Segmentation Property Categories
+    """
+    sequence_name = 'AnnotationPropertyCategoryCodeSequence'
+    cid = codes.cid7150.concepts  # Segmentation Property Categories
+
+
+class IlluminationCode(CidConceptCode, MultipleConceptCode):
+    """
+    Concept code for illumination type.
+    From CID 8123 Microscopy Illumination Method
+    """
+    sequence_name = 'IlluminationTypeCodeSequence'
+    cid = codes.cid8123.concepts  # Microscopy Illumination Method
+
+
+class LenseCode(CidConceptCode, MultipleConceptCode):
+    """
+    Concept code for lense.
+    From CID 8121 Microscopy Lens Type
+    """
+    sequence_name = 'LensesCodeSequence'
+    cid = codes.cid8121.concepts  # Microscopy Lens Type
+
+
+class LightPathFilterCode(CidConceptCode, MultipleConceptCode):
+    """
+    Concept code for light path filter.
+    From CID 8124 Microscopy Filter
+    """
+    sequence_name = 'LightPathFilterTypeStackCodeSequence'
+    cid = codes.cid8124.concepts  # Microscopy Filter
+
+
+class ImagePathFilterCode(CidConceptCode, MultipleConceptCode):
+    """
+    Concept code for image path filter.
+    From CID 8124 Microscopy Filter
+    """
+    sequence_name = 'ImagePathFilterTypeStackCodeSequence'
+    cid = codes.cid8124.concepts  # Microscopy Filter
+
+
+class IlluminationColorCode(CidConceptCode, SingleConceptCode):
+    """
+    Concept code for illumination color.
+    From CID 8122 Microscopy Illuminator and Sensor Color
+    """
+    sequence_name = 'IlluminationColorCodeSequence'
+    cid = codes.cid8122.concepts  # Microscopy Illuminator and Sensor Color
+
+
+class IlluminatorCode(CidConceptCode, SingleConceptCode):
+    """
+    Concept code for illuminator type.
+    From CID 8125 Microscopy Illuminator Type
+    """
+    sequence_name = 'IlluminatorTypeCodeSequence'
+    cid = codes.cid8125.concepts  # Microscopy Illuminator Type
+
+
+class ChannelDescriptionCode(CidConceptCode, SingleConceptCode):
+    """
+    Concept code for channel descriptor.
+    From CID 8122 Microscopy Illuminator and Sensor Color
+    """
+    sequence_name = 'ChannelDescriptionCodeSequence'
+    cid = codes.cid8122.concepts  # Microscopy Illuminator and Sensor Color
+
+
+class SpecimenCollectionProcedureCode(CidConceptCode, SingleConceptCode):
+    sequence_name = 'ConceptCodeSequence'
+    cid = codes.cid8109.concepts
+
+
+class SpecimenSamplingProcedureCode(CidConceptCode, SingleConceptCode):
+    sequence_name = 'ConceptCodeSequence'
+    cid = codes.cid8110.concepts
+
+
+class SpecimenPreparationProcedureCode(CidConceptCode, SingleConceptCode):
+    sequence_name = 'ConceptCodeSequence'
+    cid = codes.cid8111.concepts
+
+
+class SpecimenStainsCode(CidConceptCode, SingleConceptCode):
+    sequence_name = 'ConceptCodeSequence'
+    cid = codes.cid8112.concepts
+
+
+class SpecimenPreparationStepsCode(CidConceptCode, SingleConceptCode):
+    sequence_name = 'ConceptCodeSequence'
+    cid = codes.cid8113.concepts
+
+
+class SpecimenFixativesCode(CidConceptCode, SingleConceptCode):
+    sequence_name = 'ConceptCodeSequence'
+    cid = codes.cid8114.concepts
+
+
+class SpecimenEmbeddingMediaCode(CidConceptCode, SingleConceptCode):
+    sequence_name = 'ConceptCodeSequence'
+    cid = codes.cid8115.concepts
+
+
+class AnatomicPathologySpecimenTypesCode(CidConceptCode, SingleConceptCode):
+    sequence_name = 'ConceptCodeSequence'
+    cid = codes.cid8103.concepts
+
+
+class ConceptNameCode(SingleConceptCode):
+    sequence_name = 'ConceptNameCodeSequence'
+
+    @classmethod
+    def list(cls) -> List[str]:
+        return []
diff --git a/wsidicom/errors.py b/wsidicom/errors.py
index f08eaebe..0c4f5235 100644
--- a/wsidicom/errors.py
+++ b/wsidicom/errors.py
@@ -2,6 +2,16 @@
 from .geometry import Point
 
 
+class WsiDicomError(Exception):
+    """Raised for general error."""
+
+    def __init__(self, error: str):
+        self.error = error
+
+    def __str__(self):
+        return self.error
+
+
 class WsiDicomFileError(Exception):
     """Raised if read file data is incorrect."""
 
@@ -58,7 +68,7 @@ def __str__(self):
         return f"{self.item} not found in {self.not_found_in}"
 
 
-class WsiDicomOutOfBondsError(Exception):
+class WsiDicomOutOfBoundsError(Exception):
     """Raised if requested item is out of bonds"""
 
     def __init__(self, error: str, bonds: str):
diff --git a/wsidicom/geometry.py b/wsidicom/geometry.py
index deb47efb..b914f313 100644
--- a/wsidicom/geometry.py
+++ b/wsidicom/geometry.py
@@ -1,6 +1,6 @@
 import math
 from dataclasses import dataclass
-from typing import Tuple
+from typing import Tuple, Generator
 
 
 @dataclass
@@ -20,9 +20,19 @@ def __mul__(self, factor):
             return SizeMm(factor.x*self.width, factor.y*self.height)
         return NotImplemented
 
+    def __truediv__(self, divider):
+        if isinstance(divider, (int, float)):
+            return SizeMm(self.width/divider, self.height/divider)
+        if isinstance(divider, SizeMm):
+            return SizeMm(
+                self.width/divider.width,
+                self.height/divider.height
+            )
+        return NotImplemented
+
     def __floordiv__(self, divider):
         if isinstance(divider, (int, float)):
-            return SizeMm(int(self.width/divider), int(self.height/divider))
+            return Size(int(self.width/divider), int(self.height/divider))
         if isinstance(divider, SizeMm):
             return Size(
                 int(self.width/divider.width),
@@ -33,6 +43,9 @@ def __floordiv__(self, divider):
     def to_int_tuple(self) -> Tuple[int, int]:
         return int(self.width), int(self.height)
 
+    def to_tuple(self) -> Tuple[float, float]:
+        return self.width, self.height
+
     @staticmethod
     def from_tuple(tuple: Tuple[float, float]) -> 'SizeMm':
         return SizeMm(width=tuple[0], height=tuple[1])
@@ -142,6 +155,23 @@ def to_tuple(self) -> Tuple[int, int]:
     def from_tuple(tuple: Tuple[int, int]) -> 'Size':
         return Size(width=tuple[0], height=tuple[1])
 
+    @staticmethod
+    def max(size_1: 'Size', size_2: 'Size'):
+        return Size(
+            width=max(size_1.width, size_2.width),
+            height=max(size_1.height, size_2.height)
+        )
+
+    def ceil(self) -> 'Size':
+        return Size(
+            width=int(math.ceil(self.width)),
+            height=int(math.ceil(self.height))
+        )
+
+    @property
+    def area(self) -> int:
+        return self.width * self.height
+
 
 @dataclass
 class Point:
@@ -151,6 +181,9 @@ class Point:
     def __str__(self):
         return f'{self.x},{self.y}'
 
+    def __hash__(self) -> int:
+        return hash((self.x, self.y))
+
     def __mul__(self, factor):
         if isinstance(factor, (int, float)):
             return Point(int(factor*self.x), int(factor*self.y))
@@ -163,6 +196,8 @@ def __mul__(self, factor):
     def __floordiv__(self, divider):
         if isinstance(divider, (int, float)):
             return Point(int(self.x/divider), int(self.y/divider))
+        if isinstance(divider, Point):
+            return Point(int(self.x/divider.x), int(self.y/divider.y))
         if isinstance(divider, (Size, SizeMm)):
             return Point(int(self.x/divider.width), int(self.y/divider.height))
         return NotImplemented
@@ -254,10 +289,10 @@ def box(self) -> Tuple[int, int, int, int]:
     def box_from_origin(self) -> Tuple[int, int, int, int]:
         return 0, 0, self.size.width, self.size.height
 
-    def iterate_all(self, include_end=False):
+    def iterate_all(self, include_end=False) -> Generator[Point, None, None]:
         offset = 1 if include_end else 0
         return (
-            (x, y)
+            Point(x, y)
             for y in range(self.start.y, self.end.y + offset)
             for x in range(self.start.x, self.end.x + offset)
         )
diff --git a/wsidicom/interface.py b/wsidicom/interface.py
index 7c5a87b1..68834f88 100644
--- a/wsidicom/interface.py
+++ b/wsidicom/interface.py
@@ -2,8 +2,9 @@
 import math
 import warnings
 from abc import ABCMeta, abstractmethod
+from functools import cached_property
 from pathlib import Path
-from typing import Dict, List, Optional, OrderedDict, Tuple, Union
+from typing import Dict, List, Optional, OrderedDict, Set, Tuple, Union
 
 import numpy as np
 import pydicom
@@ -12,275 +13,31 @@
 from pydicom.sequence import Sequence as DicomSequence
 from pydicom.uid import UID as Uid
 
-from .errors import (WsiDicomFileError, WsiDicomMatchError,
-                     WsiDicomNotFoundError, WsiDicomOutOfBondsError,
-                     WsiDicomSparse, WsiDicomUidDuplicateError)
-from .geometry import Point, PointMm, Region, RegionMm, Size, SizeMm
-from .stringprinting import dict_pretty_str, list_pretty_str, str_indent
-from .uid import BaseUids, FileUids, WSI_SOP_CLASS_UID, ANN_SOP_CLASS_UID
-from .optical import OpticalManager
-from .graphical_annotations import AnnotationInstance
-
-
-class WsiDicomFile:
-    def __init__(self, filepath: Path):
-        """Open dicom file in filepath. If valid wsi type read required
-        parameters. Parses frames in pixel data but does not read the frames.
-
-        Parameters
-        ----------
-        filepath: Path
-            Path to file to open
-        """
-        self._filepath = filepath
-
-        if not pydicom.misc.is_dicom(self.filepath):
-            raise WsiDicomFileError(self.filepath, "is not a DICOM file")
-
-        file_meta = pydicom.filereader.read_file_meta_info(self.filepath)
-        transfer_syntax_uid = Uid(file_meta.TransferSyntaxUID)
-
-        self._fp = pydicom.filebase.DicomFile(self.filepath, mode='rb')
-        self._fp.is_little_endian = transfer_syntax_uid.is_little_endian
-        self._fp.is_implicit_VR = transfer_syntax_uid.is_implicit_VR
-        ds = pydicom.dcmread(self._fp, stop_before_pixels=True)
-        self._pixel_data_position = self._fp.tell()
-
-        if self.is_wsi_dicom(self.filepath, ds):
-            instance_uid = Uid(ds.SOPInstanceUID)
-            concatenation_uid: Uid = getattr(
-                ds,
-                'SOPInstanceUIDOfConcatenationSource',
-                None
-            )
-            base_uids = BaseUids(
-                ds.StudyInstanceUID,
-                ds.SeriesInstanceUID,
-                ds.FrameOfReferenceUID,
-            )
-            self._uids = FileUids(instance_uid, concatenation_uid, base_uids)
-
-            if concatenation_uid is not None:
-                try:
-                    self._frame_offset = int(ds.ConcatenationFrameOffsetNumber)
-                except AttributeError:
-                    raise WsiDicomFileError(
-                        self.filepath,
-                        'Concatenated file missing concatenation frame offset'
-                        'number'
-                    )
-            else:
-                self._frame_offset = 0
-            self._wsi_type = self.get_supported_wsi_dicom_type(
-                ds,
-                transfer_syntax_uid
-            )
-            self._pixel_spacing = self._get_pixel_spacing(ds)
-            self._image_size = self._get_image_size(ds)
-            self._mm_size = self._get_mm_size(ds)
-            self._frame_count = int(getattr(ds, 'NumberOfFrames', 1))
-            self._tile_size = self._get_tile_size(ds)
-            self._tile_type = self._get_tile_type(ds)
-            self._samples_per_pixel = int(ds.SamplesPerPixel)
-            self._transfer_syntax = Uid(ds.file_meta.TransferSyntaxUID)
-            self._photometric_interpretation = str(
-                ds.PhotometricInterpretation
-                )
-            self._optical_path_sequence = ds.OpticalPathSequence
-            self._shared_frame_sequence = ds.SharedFunctionalGroupsSequence
-            self._per_frame_sequence = getattr(
-                ds,
-                'PerFrameFunctionalGroupsSequence',
-                None
-            )
-            if self.tile_type == 'TILED_FULL':
-                self._focal_planes = int(
-                    getattr(ds, 'TotalPixelMatrixFocalPlanes', 1)
-                )
-
-            self._frame_positions = self._parse_pixel_data()
-
-        else:
-            self._wsi_type = "None"
-
-    def __repr__(self) -> str:
-        return f"WsiDicomFile('{self.filepath}')"
-
-    def __str__(self) -> str:
-        return self.pretty_str()
-
-    @property
-    def filepath(self) -> Path:
-        """Return filepath"""
-        return self._filepath
-
-    @property
-    def wsi_type(self) -> str:
-        """Return wsi flavour"""
-        return self._wsi_type
-
-    @property
-    def mpp(self) -> SizeMm:
-        """Return pixel spacing in um/pixel"""
-        return self.pixel_spacing*1000.0
-
-    @property
-    def pixel_spacing(self) -> SizeMm:
-        """Return pixel spacing in mm/pixel"""
-        return self._pixel_spacing
-
-    @property
-    def image_size(self) -> Size:
-        """Return image size in pixels"""
-        return self._image_size
-
-    @property
-    def mm_size(self) -> SizeMm:
-        """Return image size in mm"""
-        return self._mm_size
-
-    @property
-    def tile_size(self) -> Size:
-        """Return tile size in pixels"""
-        return self._tile_size
-
-    @property
-    def tile_type(self) -> str:
-        """Return tiling type (TILED_FULL or TILED_SPARSE)"""
-        return self._tile_type
-
-    @property
-    def uids(self) -> FileUids:
-        """Return uids"""
-        return self._uids
-
-    @property
-    def samples_per_pixel(self) -> int:
-        """Return samples per pixel (1 or 3)"""
-        return self._samples_per_pixel
-
-    @property
-    def transfer_syntax(self) -> Uid:
-        """Return transfer syntax uid"""
-        return self._transfer_syntax
-
-    @property
-    def photometric_interpretation(self) -> str:
-        """Return photometric interpretation"""
-        return self._photometric_interpretation
-
-    @property
-    def frame_offset(self) -> int:
-        """Return frame offset (for concatenated file, 0 otherwise)"""
-        return self._frame_offset
-
-    @property
-    def frame_positions(self) -> List[Tuple[int, int]]:
-        """Return frame positions and lengths"""
-        return self._frame_positions
-
-    @property
-    def frame_count(self) -> int:
-        """Return number of frames"""
-        return self._frame_count
-
-    @property
-    def optical_path_sequence(self) -> DicomSequence:
-        """Return DICOM Optical path sequence"""
-        return self._optical_path_sequence
-
-    @property
-    def shared_frame_sequence(self) -> DicomSequence:
-        """Return DICOM Shared functional group sequence.
-        """
-        return self._shared_frame_sequence
-
-    @property
-    def per_frame_sequence(self) -> Optional[DicomSequence]:
-        """Return DICOM Per frame functional groups sequence.
-        """
-        return self._per_frame_sequence
-
-    @property
-    def focal_planes(self) -> int:
-        """Return number of focal planes"""
-        return self._focal_planes
-
-    def pretty_str(
-        self,
-        indent: int = 0,
-        depth: int = None
-    ) -> str:
-        return f"File with path: {self.filepath}"
-
-    def matches_series(self, uids: BaseUids, tile_size: Size = None) -> bool:
-        """Check if instance is valid (Uids and tile size match).
-        Base uids should match for instances in all types of series,
-        tile size should only match for level series.
-        """
-        if tile_size is not None and tile_size != self.tile_size:
-            return False
-        return uids == self.uids.base
-
-    def matches_instance(self, other_file: 'WsiDicomFile') -> bool:
-        """Return true if other file is of the same instance as self.
-
-        Parameters
-        ----------
-        other_file: WsiDicomFile
-            File to check.
-
-        Returns
-        ----------
-        bool
-            True if same instance.
-        """
-        return (
-            self.uids.match(other_file.uids) and
-            self.image_size == other_file.image_size and
-            self.tile_size == other_file.tile_size and
-            self.tile_type == other_file.tile_type and
-            self.wsi_type == other_file.wsi_type
-        )
-
-    @staticmethod
-    def check_duplicate_file(
-        files: List['WsiDicomFile'],
-        caller: object
-    ) -> None:
-        """Check for duplicates in list of files. Files are duplicate if file
-        instance uids match. Stops at first found duplicate and raises
-        WsiDicomUidDuplicateError.
-
-        Parameters
-        ----------
-        files: List[WsiDicomFile]
-            List of files to check.
-        caller: Object
-            Object that the files belongs to.
-        """
-        instance_uids: List[str] = []
-        for file in files:
-            instance_uid = file.uids.instance
-            if instance_uid not in instance_uids:
-                instance_uids.append(file.uids.identifier)
-            else:
-                raise WsiDicomUidDuplicateError(str(file), str(caller))
+from wsidicom.errors import (WsiDicomError, WsiDicomFileError,
+                             WsiDicomMatchError, WsiDicomNotFoundError,
+                             WsiDicomOutOfBoundsError, WsiDicomSparse,
+                             WsiDicomUidDuplicateError)
+from wsidicom.geometry import Point, PointMm, Region, RegionMm, Size, SizeMm
+from wsidicom.graphical_annotations import AnnotationInstance
+from wsidicom.optical import OpticalManager
+from wsidicom.stringprinting import (dict_pretty_str, list_pretty_str,
+                                     str_indent)
+from wsidicom.uid import (ANN_SOP_CLASS_UID, WSI_SOP_CLASS_UID, BaseUids,
+                          FileUids)
+
+
+class WsiDataset(Dataset):
+    """Extend pydicom.dataset.Dataset (containing WSI metadata) with simple
+    parsers for attributes specific for WSI. Use snake case to avoid name
+    collision with dicom fields (that are handled by pydicom.dataset.Dataset).
+    """
 
-    @staticmethod
-    def is_wsi_dicom(filepath: Path, ds: Dataset) -> bool:
-        """Check if file is dicom wsi type and that required attributes
+    def is_wsi_dicom(self) -> bool:
+        """Check if dataset is dicom wsi type and that required attributes
         (for the function of the library) is available.
         Warn if attribute listed as requierd in the library or required in the
         standard is missing.
 
-        Parameters
-        ----------
-        filepath: Path
-            Path to file
-        ds: Dataset
-            Dataset of file
-
         Returns
         ----------
         bool
@@ -320,11 +77,8 @@ def is_wsi_dicom(filepath: Path, ds: Dataset) -> bool:
         ]
         REQURED_WHOLE_SLIDE_MICROSCOPY_MODULE_ATTRIBUTES = [
             "ImageType",
-            "ImagedVolumeWidth",
-            "ImagedVolumeHeight",
-            "ImagedVolumeDepth",
             "TotalPixelMatrixColumns",
-            "TotalPixelMatrixRows",
+            "TotalPixelMatrixRows"
         ]
         STANDARD_WHOLE_SLIDE_MICROSCOPY_MODULE_ATTRIBUTES = [
             "TotalPixelMatrixOriginSequence",
@@ -335,7 +89,7 @@ def is_wsi_dicom(filepath: Path, ds: Dataset) -> bool:
             "LossyImageCompression",
             "VolumetricProperties",
             "SpecimenLabelInImage",
-            "BurnedInAnnotation",
+            "BurnedInAnnotation"
         ]
         REQURED_MULTI_FRAME_FUNCTIONAL_GROUPS_MODULE_ATTRIBUTES = [
             "NumberOfFrames",
@@ -389,33 +143,82 @@ def is_wsi_dicom(filepath: Path, ds: Dataset) -> bool:
             'required': True,
             'standard': True
         }
-
         for key, module_attributes in TO_TEST.items():
             for module in module_attributes:
                 for attribute in module:
-                    if attribute not in ds:
+                    if attribute not in self:
                         warnings.warn(
-                            f'File {filepath} is missing {key}'
-                            f' attribute {attribute}'
+                            f' is missing {key} attribute {attribute}'
                         )
                         passed[key] = False
 
-        sop_class_uid = getattr(ds, "SOPClassUID", "")
+        sop_class_uid = getattr(self, "SOPClassUID", "")
         sop_class_uid_check = (sop_class_uid == WSI_SOP_CLASS_UID)
         return passed['required'] and sop_class_uid_check
 
     @staticmethod
+    def check_duplicate_dataset(
+        datasets: List[Dataset],
+        caller: object
+    ) -> None:
+        """Check for duplicates in a list of datasets. Datasets are duplicate
+        if instance uids match. Stops at first found duplicate and raises
+        WsiDicomUidDuplicateError.
+
+        Parameters
+        ----------
+        datasets: List[Dataset]
+            List of datasets to check.
+        caller: Object
+            Object that the files belongs to.
+        """
+        instance_uids: List[Uid] = []
+
+        for dataset in datasets:
+            instance_uid = Uid(dataset.SOPInstanceUID)
+            if instance_uid not in instance_uids:
+                instance_uids.append(instance_uid)
+            else:
+                raise WsiDicomUidDuplicateError(str(dataset), str(caller))
+
+    def matches_instance(self, other_dataset: 'WsiDataset') -> bool:
+        """Return true if other file is of the same instance as self.
+
+        Parameters
+        ----------
+        other_dataset: 'WsiDataset
+            Dataset to check.
+
+        Returns
+        ----------
+        bool
+            True if same instance.
+        """
+        return (
+            self.uids.match(other_dataset.uids) and
+            self.image_size == other_dataset.image_size and
+            self.tile_size == other_dataset.tile_size and
+            self.tile_type == other_dataset.tile_type
+        )
+
+    def matches_series(self, uids: BaseUids, tile_size: Size = None) -> bool:
+        """Check if instance is valid (Uids and tile size match).
+        Base uids should match for instances in all types of series,
+        tile size should only match for level series.
+        """
+        if tile_size is not None and tile_size != self.tile_size:
+            return False
+        return uids == self.base_uids
+
     def get_supported_wsi_dicom_type(
-        ds: Dataset,
+        self,
         transfer_syntax_uid: Uid
     ) -> str:
-        """Check image flavor and transfer syntax of dicom file.
+        """Check image flavor and transfer syntax of dicom dataset.
         Return image flavor if file valid.
 
         Parameters
         ----------
-        ds: Dataset
-            Pydicom dataset to check.
         transfer_syntax_uid: Uid'
             Transfer syntax uid for file.
 
@@ -425,8 +228,8 @@ def get_supported_wsi_dicom_type(
             WSI image flavor
         """
         SUPPORTED_IMAGE_TYPES = ['VOLUME', 'LABEL', 'OVERVIEW']
-        IMAGE_FLAVOR = 2
-        image_type: str = ds.ImageType[IMAGE_FLAVOR]
+        IMAGE_FLAVOR_INDEX_IN_IMAGE_TYPE = 2
+        image_type: str = self.ImageType[IMAGE_FLAVOR_INDEX_IN_IMAGE_TYPE]
         image_type_supported = image_type in SUPPORTED_IMAGE_TYPES
         if not image_type_supported:
             warnings.warn(f"Non-supported image type {image_type}")
@@ -444,33 +247,92 @@ def get_supported_wsi_dicom_type(
             return image_type
         return ""
 
-    def get_filepointer(
-        self,
-        frame_index: int
-    ) -> Tuple[pydicom.filebase.DicomFileLike, int, int]:
-        """Return file pointer, frame position, and frame lenght for frame
-        number.
+    def read_optical_path_identifier(self, frame: Dataset) -> str:
+        """Return optical path identifier from frame, or from self if not
+        found."""
+        optical_sequence = getattr(
+            frame,
+            'OpticalPathIdentificationSequence',
+            self.OpticalPathSequence
+        )
+        return getattr(optical_sequence[0], 'OpticalPathIdentifier', '0')
+
+    @cached_property
+    def instance_uid(self) -> Uid:
+        return Uid(self.SOPInstanceUID)
+
+    @cached_property
+    def concatenation_uid(self) -> Optional[Uid]:
+        return getattr(
+            self,
+            'SOPInstanceUIDOfConcatenationSource',
+            None
+        )
+
+    @cached_property
+    def base_uids(self) -> BaseUids:
+        return BaseUids(
+            self.StudyInstanceUID,
+            self.SeriesInstanceUID,
+            self.FrameOfReferenceUID,
+        )
+
+    @cached_property
+    def uids(self) -> FileUids:
+        return FileUids(
+            self.instance_uid,
+            self.concatenation_uid,
+            self.base_uids
+        )
+
+    @cached_property
+    def frame_offset(self) -> int:
+        if self.concatenation_uid is None:
+            return 0
+        try:
+            return int(self.ConcatenationFrameOffsetNumber)
+        except AttributeError:
+            raise WsiDicomError(
+                'Concatenated file missing concatenation frame offset'
+                'number'
+            )
+
+    @cached_property
+    def frame_count(self) -> int:
+        return int(getattr(self, 'NumberOfFrames', 1))
+
+    @cached_property
+    def tile_type(self) -> str:
+        """Return tiling type from dataset. Raises WsiDicomError if type
+        is undetermined.
 
         Parameters
         ----------
-        frame_index: int
-            Frame, including concatenation offset, to get.
+        ds: Dataset
+            Pydicom dataset.
 
         Returns
         ----------
-        Tuple[pydicom.filebase.DicomFileLike, int, int]:
-            File pointer, frame offset and frame lenght in number of bytes
+        str
+            Tiling type
         """
-        frame_index -= self.frame_offset
-        frame_position, frame_length = self.frame_positions[frame_index]
-        return self._fp, frame_position, frame_length
+        if(getattr(self, 'DimensionOrganizationType', '') == 'TILED_FULL'):
+            return 'TILED_FULL'
+        elif 'PerFrameFunctionalGroupsSequence' in self:
+            return 'TILED_SPARSE'
+        raise WsiDicomError("undetermined tile type")
 
-    def close(self) -> None:
-        """Close the file."""
-        self._fp.close()
+    @cached_property
+    def frame_count(self) -> int:
+        return int(getattr(self, 'NumberOfFrames', 1))
+
+    @cached_property
+    def pixel_measure(self) -> Dataset:
+        return self.SharedFunctionalGroupsSequence[0].PixelMeasuresSequence[0]
 
-    def _get_mm_size(self, ds: Dataset) -> SizeMm:
-        """Read mm size from dicom file.
+    @cached_property
+    def pixel_spacing(self) -> SizeMm:
+        """Read pixel spacing from dicom dataset.
 
         Parameters
         ----------
@@ -480,92 +342,248 @@ def _get_mm_size(self, ds: Dataset) -> SizeMm:
         Returns
         ----------
         SizeMm
-            The size of the image in mm
+            The pixel spacing in mm/pixel.
         """
-        width = float(ds.ImagedVolumeWidth)
-        height = float(ds.ImagedVolumeHeight)
-        if width == 0 or height == 0:
-            raise WsiDicomFileError(self.filepath, "Image mm size is zero")
-        return SizeMm(width=width, height=height)
+        pixel_measure = self.pixel_measure
+        pixel_spacing: Tuple[float, float] = pixel_measure.PixelSpacing
+        if any([spacing == 0 for spacing in pixel_spacing]):
+            raise WsiDicomError("Pixel spacing is zero")
+        return SizeMm(width=pixel_spacing[0], height=pixel_spacing[1])
 
-    def _get_pixel_spacing(self, ds: Dataset) -> SizeMm:
-        """Read pixel spacing from dicom file.
+    @cached_property
+    def spacing_between_slices(self) -> float:
+        pixel_measure = self.pixel_measure
+        return getattr(pixel_measure, 'SpacingBetweenSlices', 0.0)
 
-        Parameters
+    @cached_property
+    def number_of_focal_planes(self) -> int:
+        return getattr(self, 'TotalPixelMatrixFocalPlanes', 1)
+
+    @cached_property
+    def file_offset(self) -> int:
+        return int(getattr(self, 'ConcatenationFrameOffsetNumber', 0))
+
+    @cached_property
+    def frame_sequence(self) -> DicomSequence:
+        if (
+            'PerFrameFunctionalGroupsSequence' in self and
+            (
+                'PlanePositionSlideSequence' in
+                self.PerFrameFunctionalGroupsSequence[0]
+            )
+        ):
+            return self.PerFrameFunctionalGroupsSequence
+
+        return self.SharedFunctionalGroupsSequence
+
+    @cached_property
+    def ext_depth_of_field(self) -> bool:
+        return self.ExtendedDepthOfField == 'YES'
+
+    @cached_property
+    def ext_depth_of_field_planes(self) -> Optional[int]:
+        return getattr(self, 'NumberOfFocalPlanes', None)
+
+    @cached_property
+    def ext_depth_of_field_plane_distance(self) -> Optional[int]:
+        return getattr(self, 'DistanceBetweenFocalPlanes', None)
+
+    @cached_property
+    def focus_method(self) -> str:
+        return str(self.FocusMethod)
+
+    @cached_property
+    def image_size(self) -> Size:
+        """Read total pixel size from dataset.
+
+        Returns
         ----------
-        ds: Dataset
-            Pydicom dataset
+        Size
+            The image size
+        """
+        width = int(self.TotalPixelMatrixColumns)
+        height = int(self.TotalPixelMatrixRows)
+        if width == 0 or height == 0:
+            raise WsiDicomFileError(self.filepath, "Image size is zero")
+        return Size(width=width, height=height)
+
+    @cached_property
+    def mm_size(self) -> SizeMm:
+        """Read mm size from dataset.
 
         Returns
         ----------
         SizeMm
-            The pixel spacing
+            The size of the image in mm
         """
-        pixel_spacing: Tuple[float, float] = (
-            ds.SharedFunctionalGroupsSequence[0].
-            PixelMeasuresSequence[0].PixelSpacing
-        )
-        if any([spacing == 0 for spacing in pixel_spacing]):
-            raise WsiDicomFileError(self.filepath, "Pixel spacing is zero")
-        return SizeMm(width=pixel_spacing[0], height=pixel_spacing[1])
+        width = float(self.ImagedVolumeWidth)
+        height = float(self.ImagedVolumeHeight)
+        return SizeMm(width=width, height=height)
 
-    def _get_tile_size(self, ds: Dataset) -> Size:
-        """Read tile size from from dicom file.
+    @cached_property
+    def mm_depth(self) -> float:
+        return self.ImagedVolumeDepth
 
-        Parameters
-        ----------
-        ds: Dataset
-            Pydicom dataset
+    @cached_property
+    def tile_size(self) -> Size:
+        """Read tile size from from dataset.
 
         Returns
         ----------
         Size
             The tile size
         """
-        width = int(ds.Columns)
-        height = int(ds.Rows)
-        if width == 0 or height == 0:
-            raise WsiDicomFileError(self.filepath, "Tile size is zero")
+        width = int(self.Columns)
+        height = int(self.Rows)
         return Size(width=width, height=height)
 
-    def _get_image_size(self, ds: Dataset) -> Size:
-        """Read total pixel size from dicom file.
+    @cached_property
+    def samples_per_pixel(self) -> int:
+        return int(self.SamplesPerPixel)
 
-        Parameters
-        ----------
-        ds: Dataset
-            Pydicom dataset
+    @cached_property
+    def photophotometric_interpretation(self) -> str:
+        return self.PhotometricInterpretation
 
-        Returns
+    @cached_property
+    def instance_number(self) -> str:
+        return self.InstanceNumber
+
+    @cached_property
+    def optical_path_sequence(self) -> DicomSequence:
+        return self.OpticalPathSequence
+
+    @cached_property
+    def slice_thickness(self) -> float:
+        try:
+            pixel_measure = self.pixel_measure
+            return float(pixel_measure.SliceThickness)
+        except AttributeError:
+            # This might not be correct if multiple focal planes
+            return self.mm_depth
+
+
+class WsiDicomFile:
+    """Represents a DICOM file (potentially) containing WSI image and metadata.
+    """
+    def __init__(self, filepath: Path):
+        """Open dicom file in filepath. If valid wsi type read required
+        parameters. Parses frames in pixel data but does not read the frames.
+
+        Parameters
         ----------
-        Size
-            The image size
+        filepath: Path
+            Path to file to open
         """
-        width = int(ds.TotalPixelMatrixColumns)
-        height = int(ds.TotalPixelMatrixRows)
-        if width == 0 or height == 0:
-            raise WsiDicomFileError(self.filepath, "Image size is zero")
-        return Size(width=width, height=height)
+        self._filepath = filepath
 
-    def _get_tile_type(self, ds: Dataset) -> str:
-        """Return tiling type from dataset. Raises WsiDicomFileError if type
-        is undetermined.
+        if not pydicom.misc.is_dicom(self.filepath):
+            raise WsiDicomFileError(self.filepath, "is not a DICOM file")
+
+        file_meta = pydicom.filereader.read_file_meta_info(self.filepath)
+        self._transfer_syntax_uid = Uid(file_meta.TransferSyntaxUID)
+
+        self._fp = pydicom.filebase.DicomFile(self.filepath, mode='rb')
+        self._fp.is_little_endian = self._transfer_syntax_uid.is_little_endian
+        self._fp.is_implicit_VR = self._transfer_syntax_uid.is_implicit_VR
+
+        dataset = pydicom.dcmread(self._fp, stop_before_pixels=True)
+        self._pixel_data_position = self._fp.tell()
+
+        self._dataset = WsiDataset(dataset)
+        if self.dataset.is_wsi_dicom():
+            self._wsi_type = self.dataset.get_supported_wsi_dicom_type(
+                self.transfer_syntax
+            )
+            instance_uid = self.dataset.instance_uid
+            concatenation_uid = self.dataset.concatenation_uid
+            base_uids = self.dataset.base_uids
+            self._uids = FileUids(instance_uid, concatenation_uid, base_uids)
+            # If supported wsi type and transfer syntax, parse pixel data.
+            if self._wsi_type != '':
+                self._frame_offset = self.dataset.frame_offset
+                self._frame_count = self.dataset.frame_count
+                self._frame_positions = self._parse_pixel_data()
+        else:
+            self._wsi_type = ''
+            warnings.warn(f"Non-supported file {filepath}")
+
+    def __repr__(self) -> str:
+        return f"WsiDicomFile('{self.filepath}')"
+
+    def __str__(self) -> str:
+        return self.pretty_str()
+
+    @property
+    def dataset(self) -> WsiDataset:
+        """Return pydicom dataset of file."""
+        return self._dataset
+
+    @property
+    def filepath(self) -> Path:
+        """Return filepath"""
+        return self._filepath
+
+    @property
+    def wsi_type(self) -> str:
+        return self._wsi_type
+
+    @property
+    def uids(self) -> FileUids:
+        """Return uids"""
+        return self._uids
+
+    @property
+    def transfer_syntax(self) -> Uid:
+        """Return transfer syntax uid"""
+        return self._transfer_syntax_uid
+
+    @property
+    def frame_offset(self) -> int:
+        """Return frame offset (for concatenated file, 0 otherwise)"""
+        return self._frame_offset
+
+    @property
+    def frame_positions(self) -> List[Tuple[int, int]]:
+        """Return frame positions and lengths"""
+        return self._frame_positions
+
+    @property
+    def frame_count(self) -> int:
+        """Return number of frames"""
+        return self._frame_count
+
+    def pretty_str(
+        self,
+        indent: int = 0,
+        depth: int = None
+    ) -> str:
+        return f"File with path: {self.filepath}"
+
+    def get_filepointer(
+        self,
+        frame_index: int
+    ) -> Tuple[pydicom.filebase.DicomFileLike, int, int]:
+        """Return file pointer, frame position, and frame lenght for frame
+        number.
 
         Parameters
         ----------
-        ds: Dataset
-            Pydicom dataset.
+        frame_index: int
+            Frame, including concatenation offset, to get.
 
         Returns
         ----------
-        str
-            Tiling type
+        Tuple[pydicom.filebase.DicomFileLike, int, int]:
+            File pointer, frame offset and frame lenght in number of bytes
         """
-        if(getattr(ds, 'DimensionOrganizationType', '') == 'TILED_FULL'):
-            return 'TILED_FULL'
-        elif 'PerFrameFunctionalGroupsSequence' in ds:
-            return 'TILED_SPARSE'
-        raise WsiDicomFileError(self.filepath, "undetermined tile type")
+        frame_index -= self.frame_offset
+        frame_position, frame_length = self.frame_positions[frame_index]
+        return self._fp, frame_position, frame_length
+
+    def close(self) -> None:
+        """Close the file."""
+        self._fp.close()
 
     def _read_bot(self) -> None:
         """Read (skips over) basic table offset (BOT). The BOT can contain
@@ -683,149 +701,166 @@ def _parse_pixel_data(self) -> List[Tuple[int, int]]:
             )
         return frame_positions
 
-
-class SparseTilePlane:
-    def __init__(self, plane_size: Size):
-        """Hold frame indices for the tiles in a sparse tiled file.
-        Empty (sparse) frames are represented by -1.
+    @staticmethod
+    def _filter_files(
+        files: List['WsiDicomFile'],
+        series_uids: BaseUids,
+        series_tile_size: Size = None
+    ) -> List['WsiDicomFile']:
+        """Filter list of wsi dicom files to only include matching uids and
+        tile size if defined.
 
         Parameters
         ----------
-        plane_size: Size
-            Size of the tiling
+        files: List['WsiDicomFile']
+            Wsi files to filter.
+        series_uids: Uids
+            Uids to check against.
+        series_tile_size: Size
+            Tile size to check against.
+
+        Returns
+        ----------
+        List['WsiDicomFile']
+            List of matching wsi dicom files.
         """
-        self.plane = np.full(plane_size.to_tuple(), -1, dtype=int)
+        valid_files: List[WsiDicomFile] = []
 
-    def __str__(self) -> str:
-        return self.pretty_str()
+        for file in files:
+            if file.dataset.matches_series(series_uids, series_tile_size):
+                valid_files.append(file)
+            else:
+                warnings.warn(
+                    f'{file.filepath} with uids {file.uids.base} '
+                    f'did not match series with {series_uids} '
+                    f'and tile size {series_tile_size}'
+                )
+                file.close()
 
-    def __getitem__(self, position: Point) -> int:
-        """Get frame index from tile index at plane_position.
+        return valid_files
+
+    @classmethod
+    def _group_files(
+        cls,
+        files: List['WsiDicomFile']
+    ) -> Dict[str, List['WsiDicomFile']]:
+        """Return files grouped by instance identifier (instances).
 
         Parameters
         ----------
-        plane_position: Point
-            Position in plane to get the frame index from
+        files: List[WsiDicomFile]
+            Files to group into instances
 
         Returns
         ----------
-        int
-            Frame index
-        """
-        frame_index = int(self.plane[position.x, position.y])
-        if frame_index == -1:
-            raise WsiDicomSparse(position)
-        return frame_index
-
-    def __setitem__(self, position: Point, frame_index: int):
-        """Add frame index to tile index.
-
-        Parameters
-        ----------
-        plane_position: Point
-            Position in plane to add the frame index
-        frame_index: int
-            Frame index to add to the index
+        Dict[str, List[WsiDicomFile]]
+            Files grouped by instance, with instance identifier as key.
         """
-        self.plane[position.x, position.y] = frame_index
-
-    def pretty_str(
-        self,
-        indent: int = 0,
-        depth: int = None
-    ) -> str:
-        return ("Sparse tile plane")
+        grouped_files: Dict[str, List[WsiDicomFile]] = {}
+        for file in files:
+            try:
+                grouped_files[file.uids.identifier].append(file)
+            except KeyError:
+                grouped_files[file.uids.identifier] = [file]
+        return grouped_files
 
 
-class TileIndex(metaclass=ABCMeta):
-    def __init__(
-        self,
-        files: Dict[int, WsiDicomFile],
-        optical: OpticalManager
-    ):
-        """Index for the tiling of pixel data.
-        Requires same tile size for all tile planes
+class ImageData(metaclass=ABCMeta):
+    """Generic class for image data that can be inherited to implement support
+    for other image/file formats. Subclasses should implement properties to get
+    transfer_syntax, image_size, tile_size, and pixel_spacing and methods
+    get_tile() and close(). Additionally properties focal_planes and/or
+    optical_paths should be overridden if multiple focal planes or optical
+    paths are implemented."""
 
-        Parameters
-        ----------
-        files: Dict[int, WsiDicomFile]
-            Dict of file frame offset and wsi file.
-        Optical: OpticalManager
-            Optical manager to add optical paths to.
+    @property
+    @abstractmethod
+    def transfer_syntax(self) -> Uid:
+        """Should return the uid of the transfer syntax of the image."""
+        raise NotImplementedError
 
-        """
-        self._image_size = files[0].image_size
-        self._tile_size = files[0].tile_size
-        self._plane_size: Size = self.image_size / self.tile_size
-        self._frame_count = self._get_frame_count_from_files(files)
-        self._focal_planes = self._get_focal_planes_from_files(files)
-        self._optical_paths = self._get_optical_paths_from_files(
-            files,
-            optical
-        )
-        self._default_z: float = self._select_default_z(self.focal_planes)
-        self._default_path = self.optical_paths[0]
+    @property
+    @abstractmethod
+    def image_size(self) -> Size:
+        """Should return the pixel size of the image."""
+        raise NotImplementedError
 
     @property
+    @abstractmethod
     def tile_size(self) -> Size:
-        """Return tile size in pixels"""
-        return self._tile_size
+        """Should return the pixel tile size of the image, or pixel size of
+        the image if not tiled."""
+        raise NotImplementedError
 
     @property
-    def plane_size(self) -> Size:
-        """Return size of tiling (columns x rows)"""
-        return self._plane_size
+    @abstractmethod
+    def pixel_spacing(self) -> SizeMm:
+        """Should return the size of the pixels in mm/pixel."""
+        raise NotImplementedError
+
+    @abstractmethod
+    def get_tile(
+        self,
+        tile: Point,
+        z: float,
+        path: str
+    ) -> bytes:
+        """Should return image bytes for tile defined by tile (x, y), z,
+        and optical path."""
+        raise NotImplementedError
+
+    @abstractmethod
+    def close(self) -> None:
+        """Should close any open files."""
+        raise NotImplementedError
 
     @property
-    def default_z(self) -> float:
-        """Return default focal plane in um"""
-        return self._default_z
+    def tiled_size(self) -> Size:
+        """The size of the image when divided into tiles, e.g. number of
+        columns and rows of tiles. Equals (1, 1) if image is not tiled."""
+        return self.image_size / self.tile_size
 
     @property
-    def default_path(self) -> str:
-        """Return default optical path identifier"""
-        return self._default_path
-
-    @property
-    def frame_count(self) -> int:
-        """Return total number of frames"""
-        return self._frame_count
-
-    @property
-    def focal_planes(self) -> List[float]:
-        """Return total number of focal planes"""
-        return self._focal_planes
+    def focal_planes(self) -> List[float]:
+        """Focal planes avaiable in the image defined in um."""
+        return [0.0]
 
     @property
     def optical_paths(self) -> List[str]:
-        """Return total number of optical paths"""
-        return self._optical_paths
-
-    @property
-    def image_size(self) -> Size:
-        """Return image size in pixels"""
-        return self._image_size
+        """Optical paths avaiable in the image."""
+        raise ['0']
 
-    @abstractmethod
     def pretty_str(
         self,
         indent: int = 0,
         depth: int = None
     ) -> str:
-        raise NotImplementedError()
+        return str(self)
 
-    @abstractmethod
-    def get_frame_index(self, tile: Point, z: float, path: str) -> int:
-        """Abstract method for getting the frame index for a tile"""
-        raise NotImplementedError
+    @cached_property
+    def default_z(self) -> float:
+        """Return single defined focal plane (in um) if only one focal plane
+        defined. Return the middle focal plane if several focal planes are
+        defined."""
+        default = 0
+        if(len(self.focal_planes) > 1):
+            smallest = min(self.focal_planes)
+            largest = max(self.focal_planes)
+            middle = (largest - smallest)/2
+            default = min(range(len(self.focal_planes)),
+                          key=lambda i: abs(self.focal_planes[i]-middle))
 
-    @abstractmethod
-    def _get_focal_planes_from_files(
-        self,
-        files: Dict[int, WsiDicomFile]
-    ) -> List[float]:
-        """Abstract method for getting focal planes from file"""
-        raise NotImplementedError
+        return self.focal_planes[default]
+
+    @property
+    def default_path(self) -> str:
+        """Return the first defined optical path as default optical path
+        identifier."""
+        return self.optical_paths[0]
+
+    @cached_property
+    def plane_region(self) -> Region:
+        return Region(position=Point(0, 0), size=self.tiled_size - 1)
 
     def valid_tiles(self, region: Region, z: float, path: str) -> bool:
         """Check if tile region is inside tile geometry and z coordinate and
@@ -834,127 +869,379 @@ def valid_tiles(self, region: Region, z: float, path: str) -> bool:
         Parameters
         ----------
         region: Region
-            Tile region
+            Tile region.
         z: float
-            z coordiante
+            Z coordiante.
         path: str
-            optical path
+            Optical path.
         """
-        plane_region = Region(
-            position=Point(0, 0),
-            size=self.plane_size - 1
-        )
         return (
-            region.is_inside(plane_region) and
+            region.is_inside(self.plane_region) and
             (z in self.focal_planes) and
             (path in self.optical_paths)
         )
 
-    def get_range(self, pixel_region: Region, z: float, path: str) -> Region:
-        """Return range of tiles to cover pixel region.
+
+class DicomImageData(ImageData):
+    """Represents image data read from dicom file(s). Image data can
+    be sparsly or fully tiled and/or concatenated."""
+    def __init__(self, files: Union[WsiDicomFile, List[WsiDicomFile]]) -> None:
+        """Create DicomImageData from frame data in files.
 
         Parameters
         ----------
-        pixel_region: Region
-            Pixel region of tiles to get
+        files: Union[WsiDicomFile, List[WsiDicomFile]]
+            Single or list of WsiDicomFiles containing frame data.
+        """
+        if not isinstance(files, list):
+            files = [files]
+
+        # Key is frame offset
+        self._files = OrderedDict(
+            (file.frame_offset, file) for file
+            in sorted(files, key=lambda file: file.frame_offset)
+        )
+
+        base_file = files[0]
+        datasets = [file.dataset for file in self._files.values()]
+        if base_file.dataset.tile_type == 'TILED_FULL':
+            self.tiles = FullTileIndex(datasets)
+        else:
+            self.tiles = SparseTileIndex(datasets)
+
+        self._pixel_spacing = base_file.dataset.pixel_spacing
+        self._transfer_syntax = base_file.transfer_syntax
+
+    @property
+    def transfer_syntax(self) -> Uid:
+        """The uid of the transfer syntax of the image."""
+        return self._transfer_syntax
+
+    @property
+    def image_size(self) -> Size:
+        """The pixel size of the image."""
+        return self.tiles.image_size
+
+    @property
+    def tile_size(self) -> Size:
+        """The pixel tile size of the image."""
+        return self.tiles.tile_size
+
+    @property
+    def focal_planes(self) -> List[float]:
+        """Focal planes avaiable in the image defined in um."""
+        return self.tiles.focal_planes
+
+    @property
+    def optical_paths(self) -> List[str]:
+        """Optical paths avaiable in the image."""
+        return self.tiles.optical_paths
+
+    @property
+    def pixel_spacing(self) -> SizeMm:
+        """Size of the pixels in mm/pixel."""
+        return self._pixel_spacing
+
+    def get_tile(self, tile: Point, z: float, path: str) -> bytes:
+        frame_index = self._get_frame_index(tile, z, path)
+        return self._get_tile_frame(frame_index)
+
+    def get_filepointer(
+        self,
+        tile: Point,
+        z: float,
+        path: str
+    ) -> Tuple[pydicom.filebase.DicomFileLike, int, int]:
+        """Return file pointer, frame position, and frame lenght for tile with
+        z and path. If frame is inside tile geometry but no tile exists in
+        frame data (sparse) WsiDicomSparse is raised.
+
+        Parameters
+        ----------
+        tile: Point
+            Tile coordinate to get.
         z: float
-            Z coordinate of tiles to get
+            z coordinate to get tile for.
         path: str
-            Optical path identifier of tiles to get
+            Optical path to get tile for.
 
         Returns
         ----------
-        Region
-            Region of tiles for stitching image
+        Tuple[pydicom.filebase.DicomFileLike, int, int]:
+            File pointer, frame offset and frame lenght in number of bytes.
         """
-        start = pixel_region.start // self.tile_size
-        end = pixel_region.end / self.tile_size - 1
-        tile_region = Region.from_points(start, end)
+        frame_index = self._get_frame_index(tile, z, path)
+        file = self._get_file(frame_index)
+        return file.get_filepointer(frame_index)
+
+    def _get_file(self, frame_index: int) -> WsiDicomFile:
+        """Return file contaning frame index. Raises WsiDicomNotFoundError if
+        frame is not found.
+
+        Parameters
+        ----------
+        frame_index: int
+             Frame index to get
+
+        Returns
+        ----------
+        WsiDicomFile
+            File containing the frame
+        """
+        for frame_offset, file in self._files.items():
+            if (frame_index < frame_offset + file.frame_count and
+                    frame_index >= frame_offset):
+                return file
+
+        raise WsiDicomNotFoundError(f"Frame index {frame_index}", "instance")
+
+    def _get_tile_frame(self, frame_index: int) -> bytes:
+        """Return tile frame for frame index.
+
+        Parameters
+        ----------
+        frame_index: int
+             Frame index to get
+
+        Returns
+        ----------
+        bytes
+            The frame in bytes
+        """
+        file = self._get_file(frame_index)
+        tile_frame = file._read_frame(frame_index)
+        return tile_frame
+
+    def _get_frame_index(self, tile: Point, z: float, path: str) -> int:
+        """Return frame index for tile. Raises WsiDicomOutOfBoundsError if
+        tile, z, or path is not valid. Raises WsiDicomSparse if index is sparse
+        and tile is not in frame data.
+
+        Parameters
+        ----------
+        tile: Point
+             Tile coordiante
+        z: float
+            Z coordiante
+        path: str
+            Optical identifier
+
+        Returns
+        ----------
+        int
+            Tile frame index
+        """
+        tile_region = Region(position=tile, size=Size(0, 0))
         if not self.valid_tiles(tile_region, z, path):
-            raise WsiDicomOutOfBondsError(
-                f"Tile region {tile_region}", f"plane {self.plane_size}"
+            raise WsiDicomOutOfBoundsError(
+                f"Tile region {tile_region}",
+                f"plane {self.tiles.tiled_size}"
             )
-        return tile_region
+        frame_index = self.tiles.get_frame_index(tile, z, path)
+        return frame_index
 
-    @staticmethod
-    def _select_default_z(focal_planes: List[float]) -> float:
-        """Select default z coordinate to use if specific plane not set.
-        If more than one focal plane available the middle one is selected.
+    def is_sparse(self, tile: Point, z: float, path: str) -> bool:
+        try:
+            self.tiles.get_frame_index(tile, z, path)
+            return False
+        except WsiDicomSparse:
+            return True
+
+    def close(self) -> None:
+        for file in self._files.values():
+            file.close()
+
+
+class SparseTilePlane:
+    """Hold frame indices for the tiles in a sparse tiled file. Empty (sparse)
+    frames are represented by -1."""
+    def __init__(self, tiled_size: Size):
+        """Create a SparseTilePlane of specified size.
+
+        Parameters
+        ----------
+        tiled_size: Size
+            Size of the tiling
+        """
+        self.plane = np.full(tiled_size.to_tuple(), -1, dtype=int)
+
+    def __str__(self) -> str:
+        return self.pretty_str()
+
+    def __getitem__(self, position: Point) -> int:
+        """Get frame index from tile index at plane_position.
 
         Parameters
         ----------
-        focal_planes: List[float]
-           List of focal planes to select from
+        plane_position: Point
+            Position in plane to get the frame index from
 
         Returns
         ----------
-        float
-            Default z coordinate
+        int
+            Frame index
+        """
+        frame_index = int(self.plane[position.x, position.y])
+        if frame_index == -1:
+            raise WsiDicomSparse(position)
+        return frame_index
 
+    def __setitem__(self, position: Point, frame_index: int):
+        """Add frame index to tile index.
+
+        Parameters
+        ----------
+        plane_position: Point
+            Position in plane to add the frame index
+        frame_index: int
+            Frame index to add to the index
         """
-        default = 0
-        if(len(focal_planes) > 1):
-            smallest = min(focal_planes)
-            largest = max(focal_planes)
-            middle = (largest - smallest)/2
-            default = min(range(len(focal_planes)),
-                          key=lambda i: abs(focal_planes[i]-middle))
-        return focal_planes[default]
+        self.plane[position.x, position.y] = frame_index
+
+    def pretty_str(
+        self,
+        indent: int = 0,
+        depth: int = None
+    ) -> str:
+        return ("Sparse tile plane")
+
+
+class TileIndex(metaclass=ABCMeta):
+    """Index for mapping tile position to frame number. Is subclassed into
+    FullTileIndex and SparseTileIndex."""
+    def __init__(
+        self,
+        datasets: List[WsiDataset]
+    ):
+        """Create tile index for frames in datasets. Requires equal tile
+        size for all tile planes.
+
+        Parameters
+        ----------
+        datasets: List[WsiDataset]
+            List of datasets containing tiled image data.
+
+        """
+        base_dataset = datasets[0]
+        self._image_size = base_dataset.image_size
+        self._tile_size = base_dataset.tile_size
+        self._frame_count = self._read_frame_count_from_datasets(datasets)
+        self._optical_paths = self._read_optical_paths_from_datasets(datasets)
+
+    @property
+    @abstractmethod
+    def focal_planes(self) -> List[float]:
+        """Return list of focal planes in index."""
+        raise NotImplementedError
+
+    @property
+    def image_size(self) -> Size:
+        """Return image size in pixels."""
+        return self._image_size
+
+    @property
+    def tile_size(self) -> Size:
+        """Return tile size in pixels."""
+        return self._tile_size
+
+    @cached_property
+    def tiled_size(self) -> Size:
+        """Return size of tiling (columns x rows)."""
+        return self.image_size / self.tile_size
+
+    @property
+    def frame_count(self) -> int:
+        """Return total number of frames in index."""
+        return self._frame_count
+
+    @property
+    def optical_paths(self) -> List[str]:
+        """Return list of optical paths in index."""
+        return self._optical_paths
+
+    @abstractmethod
+    def pretty_str(
+        self,
+        indent: int = 0,
+        depth: int = None
+    ) -> str:
+        raise NotImplementedError()
+
+    @abstractmethod
+    def get_frame_index(self, tile: Point, z: float, path: str) -> int:
+        """Abstract method for getting the frame index for a tile"""
+        raise NotImplementedError
 
     @staticmethod
-    def _get_frame_count_from_files(
-        files: Dict[int, WsiDicomFile]
+    def _read_frame_count_from_datasets(
+        datasets: List[WsiDataset]
     ) -> int:
         """Return total frame count from files.
 
         Parameters
         ----------
-        files: Dict[int, WsiDicomFile]
-           Dict of wsi dicom files
+        datasets: List[WsiDataset]
+           List of datasets.
 
         Returns
         ----------
         int
-            Total frame count
+            Total frame count.
 
         """
         count = 0
-        for file in files.values():
-            count += file.frame_count
+        for dataset in datasets:
+            count += dataset.frame_count
         return count
 
     @staticmethod
-    def _get_optical_paths_from_files(
-        files: Dict[int, WsiDicomFile],
-        optical: OpticalManager
+    def _read_optical_paths_from_datasets(
+        datasets: List[WsiDataset]
     ) -> List[str]:
         """Return list of optical path identifiers from files.
 
         Parameters
         ----------
-        files: Dict[int, WsiDicomFile]
-           Dict of wsi dicom files
-        optical: OpticalManager
-            Optical manager to add optical paths to.
+        datasets: List[WsiDataset]
+           List of datasets.
 
         Returns
         ----------
         List[str]
-            Optical identifiers
+            Optical identifiers.
 
         """
-        paths: List[str] = []
-        for file in files.values():
-            paths += optical.add(file.optical_path_sequence)
-        return paths
+        paths: Set[str] = set()
+        for dataset in datasets:
+            paths.update(OpticalManager.get_path_identifers(
+                dataset.optical_path_sequence
+            ))
+        return list(paths)
 
 
 class FullTileIndex(TileIndex):
-    """Index for full tiled pixel data.
-    Requires same tile size for all tile planes.
-    Pixel data tiles are ordered by colum, row, z and path, thus
-    the frame index for a tile can directly be calculated.
-    """
+    """Index for mapping tile position to frame number for datasets containing
+    full tiles. Pixel data tiles are ordered by colum, row, z and path, thus
+    the frame index for a tile can directly be calculated."""
+    def __init__(
+        self,
+        datasets: List[WsiDataset]
+    ):
+        """Create full tile index for frames in datasets. Requires equal tile
+        size for all tile planes.
+
+        Parameters
+        ----------
+        datasets: List[WsiDataset]
+            List of datasets containing full tiled image data.
+        """
+        super().__init__(datasets)
+        self._focal_planes = self._read_focal_planes_from_datasets(datasets)
+
+    @property
+    def focal_planes(self) -> List[float]:
+        return self._focal_planes
+
     def __str__(self) -> str:
         return self.pretty_str()
 
@@ -965,7 +1252,7 @@ def pretty_str(
     ) -> str:
         string = (
             f"Full tile index tile size: {self.tile_size}"
-            f", plane size: {self.plane_size}"
+            f", plane size: {self.tiled_size}"
         )
         if depth is not None:
             depth -= 1
@@ -978,74 +1265,63 @@ def pretty_str(
         return string
 
     def get_frame_index(self, tile: Point, z: float, path: str) -> int:
-        """Return frame index for a Point tile, z coordinate,
-        and optical path from full tile index. Assumes that tile, z, and path
-        are valid.
+        """Return frame index for a Point tile, z coordinate, and optical path
+        from full tile index. Assumes that tile, z, and path are valid.
 
         Parameters
         ----------
         tile: Point
-            tile xy to get
+            Tile xy to get.
         z: float
-            z coordinate to get
+            Z coordinate to get.
         path: str
-            ID of optical path to get
+            ID of optical path to get.
 
         Returns
         ----------
         int
-            Frame index
+            Frame index.
         """
-        z_index = self._focal_plane_index(z)
-        plane_offset = tile.x + self.plane_size.width*tile.y
-        tiles_in_plane = self.plane_size.width * self.plane_size.height
-        z_offset = z_index * tiles_in_plane
-        path_index = self._optical_path_index(path)
+        plane_offset = tile.x + self.tiled_size.width * tile.y
+        z_offset = self._get_focal_plane_index(z) * self.tiled_size.area
         path_offset = (
-            path_index * len(self._focal_planes) * tiles_in_plane
+            self._get_optical_path_index(path)
+            * len(self._focal_planes) * self.tiled_size.area
         )
         return plane_offset + z_offset + path_offset
 
-    def _get_focal_planes_from_files(
+    def _read_focal_planes_from_datasets(
         self,
-        files: Dict[int, WsiDicomFile]
+        datasets: List[WsiDataset]
     ) -> List[float]:
-        """Return list of focal planes from files.
-        Values in Pixel Measures Sequene are in mm.
+        """Return list of focal planes in datasets. Values in Pixel Measures
+        Sequene are in mm.
 
         Parameters
         ----------
-        files: Dict[int, WsiDicomFile]
-           Dict of wsi dicom files
+        datasets: List[WsiDataset]
+           List of datasets to read focal planes from.
 
         Returns
         ----------
         List[float]
-            Focal planes
+            Focal planes, specified in um.
 
         """
         MM_TO_MICRON = 1000.0
         DECIMALS = 3
-        focal_planes: List[float] = []
-        for file in files.values():
-            sequence = file.shared_frame_sequence[0]
-            spacing = getattr(
-                sequence.PixelMeasuresSequence[0],
-                'SpacingBetweenSlices',
-                0
-            )
-            if spacing == 0 and file.focal_planes != 1:
-                raise WsiDicomFileError(
-                    file.filepath,
-                    "Multipe focal planes and zero plane spacing"
-                )
-            for plane in range(file.focal_planes):
-                z = round(plane * spacing * MM_TO_MICRON, DECIMALS)
-                if z not in focal_planes:
-                    focal_planes.append(z)
-        return focal_planes
-
-    def _optical_path_index(self, path: str) -> int:
+        focal_planes: Set[float] = set()
+        for dataset in datasets:
+            slice_spacing = dataset.spacing_between_slices
+            number_of_focal_planes = dataset.number_of_focal_planes
+            if slice_spacing == 0 and number_of_focal_planes != 1:
+                raise ValueError
+            for plane in range(number_of_focal_planes):
+                z = round(plane * slice_spacing * MM_TO_MICRON, DECIMALS)
+                focal_planes.add(z)
+        return list(focal_planes)
+
+    def _get_optical_path_index(self, path: str) -> int:
         """Return index of the optical path in instance.
         This assumes that all files in a concatenated set contains all the
         optical path identifiers of the set.
@@ -1053,13 +1329,13 @@ def _optical_path_index(self, path: str) -> int:
         Parameters
         ----------
         path: str
-            Optical path identifier
+            Optical path identifier to search for.
 
         Returns
         ----------
         int
             The index of the optical path identifier in the optical path
-            sequence
+            sequence.
         """
         try:
             return next(
@@ -1067,59 +1343,56 @@ def _optical_path_index(self, path: str) -> int:
                  if plane_path == path)
             )
         except StopIteration:
-            raise WsiDicomNotFoundError(
-                f"Optical path {path}",
-                "full tile index"
-            )
+            raise WsiDicomNotFoundError(f"Optical path {path}", self)
 
-    def _focal_plane_index(self, z: float) -> int:
+    def _get_focal_plane_index(self, z: float) -> int:
         """Return index of the focal plane of z.
 
         Parameters
         ----------
         z: float
-            The z coordinate to search for
+            The z coordinate (in um) to search for.
 
         Returns
         ----------
         int
-            Focal plane index
+            Focal plane index for z coordiante.
         """
         try:
             return next(index for index, plane in enumerate(self.focal_planes)
                         if plane == z)
         except StopIteration:
-            raise WsiDicomNotFoundError(
-                f"Z {z} in instance", "full tile index"
-            )
+            raise WsiDicomNotFoundError(f"Z {z} in instance", self)
 
 
 class SparseTileIndex(TileIndex):
+    """Index for mapping tile position to frame number for datasets containing
+    sparse tiles. Frame indices are retrieved from tile position, z, and path
+    by finding the corresponding matching SparseTilePlane (z and path) and
+    returning the frame index at tile position. If the tile is missing (due to
+    the sparseness), -1 is returned."""
     def __init__(
         self,
-        files: Dict[int, WsiDicomFile],
-        optical: OpticalManager
+        datasets: List[WsiDataset]
     ):
-        """Index for sparse tiled pixel data.
-        Requires same tile size for all tile planes.
-        Pixel data tiles are identified by the Per Frame Functional Groups
-        Sequence that contains tile colum, row, z, path, and frame index. These
-        are stored in a SparseTilePlane (one plane for every combination of z
-        and path). Frame indices are retrieved from tile position, z, and path
-        by finding the corresponding matching SparseTilePlane (z and path) and
-        returning the frame index at tile position. If the tile is missing (due
-        to the sparseness), -1 is returned.
+        """Create sparse tile index for frames in datasets. Requires equal tile
+        size for all tile planes. Pixel data tiles are identified by the Per
+        Frame Functional Groups Sequence that contains tile colum, row, z,
+        path, and frame index. These are stored in a SparseTilePlane
+        (one plane for every combination of z and path).
 
         Parameters
         ----------
-        files: Dict[int, WsiDicomFile]
-            Dict of file frame offset and wsi file.
-        Optical: OpticalManager
-            Optical manager to add optical paths to.
-
+        datasets: List[WsiDataset]
+            List of datasets containing sparse tiled image data.
         """
-        super().__init__(files, optical)
-        self._planes = self._get_planes_from_files(files)
+        super().__init__(datasets)
+        self._planes = self._read_planes_from_datasets(datasets)
+        self._focal_planes = self._get_focal_planes()
+
+    @property
+    def focal_planes(self) -> List[float]:
+        return self._focal_planes
 
     def __str__(self) -> str:
         return self.pretty_str()
@@ -1131,7 +1404,7 @@ def pretty_str(
     ) -> str:
         string = (
             f"Sparse tile index tile size: {self.tile_size}"
-            f", plane size: {self.plane_size}"
+            f", plane size: {self.tiled_size}"
         )
         return string
 
@@ -1142,66 +1415,49 @@ def get_frame_index(self, tile: Point, z: float, path: str) -> int:
         Parameters
         ----------
         tile: Point
-            tile xy to get
+            Tile xy to get.
         z: float
-            z coordinate to get
+            Z coordinate to get.
         path: str
-            ID of optical path to get
+            ID of optical path to get.
 
         Returns
         ----------
         int
-            Frame index
+            Frame index.
         """
-        plane = self._get_plane(z, path)
+        try:
+            plane = self._planes[(z, path)]
+        except KeyError:
+            raise WsiDicomNotFoundError(
+                f"Plane with z {z}, path {path}", self
+            )
         frame_index = plane[tile]
         return frame_index
 
-    def _get_focal_planes_from_files(
-        self,
-        files: Dict[int, WsiDicomFile]
-    ) -> List[float]:
-        """Return list of focal planes from files.
-
-        Parameters
-        ----------
-        files: Dict[int, WsiDicomFile]
-           Dict of wsi dicom files
+    def _get_focal_planes(self) -> List[float]:
+        """Return list of focal planes defiend in planes.
 
         Returns
         ----------
         List[float]
-            Focal planes
+            Focal planes, specified in um.
         """
-        focal_planes: List[float] = []
-        for file in files.values():
-            if 'PlanePositionSlideSequence' in file.shared_frame_sequence[0]:
-                frame_sequence = file.shared_frame_sequence
-            else:
-                frame_sequence = file.per_frame_sequence
-            for frame in frame_sequence:
-                try:
-                    (tile, z) = self._get_frame_coordinates(frame)
-                except AttributeError:
-                    print(frame)
-                    raise WsiDicomFileError(
-                        file.filepath,
-                        "Invalid plane position slide sequence"
-                    )
-                if z not in focal_planes:
-                    focal_planes.append(z)
-        return focal_planes
+        focal_planes: Set[float] = set()
+        for z, path in self._planes.keys():
+            focal_planes.add(z)
+        return list(focal_planes)
 
-    def _get_planes_from_files(
+    def _read_planes_from_datasets(
         self,
-        files: Dict[int, WsiDicomFile]
+        datasets: List[WsiDataset]
     ) -> Dict[Tuple[float, str], SparseTilePlane]:
-        """Return SparseTilePlane from planes read from files.
+        """Return SparseTilePlane from planes in datasets.
 
         Parameters
         ----------
-        files: Dict[int, WsiDicomFile]
-           Dict of wsi dicom files
+        datasets: List[WsiDataset]
+           List of datasets to read planes from.
 
         Returns
         ----------
@@ -1210,56 +1466,26 @@ def _get_planes_from_files(
         """
         planes: Dict[Tuple[float, str], SparseTilePlane] = {}
 
-        for file_offset, file in files.items():
-            if 'PlanePositionSlideSequence' in file.shared_frame_sequence[0]:
-                frame_sequence = file.shared_frame_sequence
-            else:
-                frame_sequence = file.per_frame_sequence
+        for dataset in datasets:
+            file_offset = dataset.file_offset
+            frame_sequence = dataset.frame_sequence
             for i, frame in enumerate(frame_sequence):
-                (tile, z) = self._get_frame_coordinates(frame)
-                optical_sequence = getattr(
-                    frame,
-                    'OpticalPathIdentificationSequence',
-                    file.optical_path_sequence
-                )
-                identifier = getattr(
-                    optical_sequence[0],
-                    'OpticalPathIdentifier',
-                    '0'
-                )
+                (tile, z) = self._read_frame_coordinates(frame)
+                identifier = dataset.read_optical_path_identifier(frame)
+
                 try:
                     plane = planes[(z, identifier)]
                 except KeyError:
-                    plane = SparseTilePlane(self.plane_size)
+                    plane = SparseTilePlane(self.tiled_size)
                     planes[(z, identifier)] = plane
                 plane[tile] = i + file_offset
-        return planes
 
-    def _get_plane(self, z: float, path: str) -> SparseTilePlane:
-        """Return plane with z coordinate and optical path.
-
-        Parameters
-        ----------
-        z: float
-            Z coordinate to search for
-        path: str
-            Optical path identifer to search for
+        return planes
 
-        Returns
-        ----------
-        SparseTilePlane
-            The plane for z coordinate and path
-        """
-        try:
-            return self._planes[(z, path)]
-        except KeyError:
-            raise WsiDicomNotFoundError(
-                f"Plane with z {z}, path {path}", "sparse tile index"
-            )
+    def _read_frame_coordinates(
+            self,
+            frame: DicomSequence
 
-    def _get_frame_coordinates(
-        self,
-        frame: DicomSequence
     ) -> Tuple[Point, float]:
         """Return frame coordinate (Point(x, y) and float z) of the frame.
         In the Plane Position Slide Sequence x and y are defined in mm and z in
@@ -1268,7 +1494,7 @@ def _get_frame_coordinates(
         Parameters
         ----------
         frame: DicomSequence
-            Pydicom frame sequence
+            Pydicom frame sequence.
 
         Returns
         ----------
@@ -1284,50 +1510,38 @@ def _get_frame_coordinates(
         return tile, z
 
 
-class WsiDicomInstance:
-    def __init__(self, files: List[WsiDicomFile], optical: OpticalManager):
-        """Represents a single SOP instance or a concatenated SOP instance.
-        The instance can contain multiple focal planes and optical paths.
-
-        Files needs to match in UIDs and have the same image and tile size.
+class WsiInstance:
+    """Represents a level, label, or overview wsi image, containing image data
+    and datasets with metadata."""
+    def __init__(
+        self,
+        datasets: Union[WsiDataset, List[WsiDataset]],
+        image_data: ImageData
+    ):
+        """Create a WsiInstance from datasets with metadata and image data.
 
         Parameters
         ----------
-        files: List[WsiDicomFile]
-            Files to include in the instance.
-        optical: OpticalManager
-            Optical manager to add optical paths to.
+        datasets: Union[WsiDataset, List[WsiDataset]]
+            Single dataset or list of datasets.
+        image_data: ImageData
+            Image data.
         """
-        # Key is frame offset
-        self._files = OrderedDict(
-            (file.frame_offset, file) for file
-            in sorted(files, key=lambda file: file.frame_offset)
-        )
-        (
-            self._identifier,
-            self._uids,
-            self._wsi_type
-        ) = self._validate_instance(optical)
+        if not isinstance(datasets, list):
+            datasets = [datasets]
+        self._datasets = datasets
 
-        self.tiles = self._create_tileindex(self._files, optical)
+        self._image_data = image_data
 
-        base_file = files[0]
-        self._size = base_file.image_size
-        self._pixel_spacing = base_file.pixel_spacing
-        self._tile_size = base_file.tile_size
-        self._samples_per_pixel = base_file.samples_per_pixel
-        self._transfer_syntax = base_file.transfer_syntax
-        self._photometric_interpretation = base_file.photometric_interpretation
+        self._identifier, self._uids = self._validate_instance(self.datasets)
 
-        self._blank_color = self._get_blank_color(
-            self._photometric_interpretation
-        )
-        if(self._samples_per_pixel == 1):
-            self._image_mode = "L"
-        else:
-            self._image_mode = "RGB"
-        self._blank_tile = self._create_blank_tile()
-        self._blank_encoded_tile = self._encode_tile(self._blank_tile)
+        if self.ext_depth_of_field:
+            if self.ext_depth_of_field_planes is None:
+                raise WsiDicomError("Instance Missing NumberOfFocalPlanes")
+            if self.ext_depth_of_field_plane_distance is None:
+                raise WsiDicomError(
+                    "Instance Missing DistanceBetweenFocalPlanes"
+                )
 
     def __str__(self) -> str:
         return self.pretty_str()
@@ -1338,201 +1552,188 @@ def pretty_str(
         depth: int = None
     ) -> str:
         string = (
-            f"Instance default z: {self.tiles.default_z}"
-            f" default path: { self.tiles.default_path}"
+            f"default z: {self.default_z} "
+            f"default path: { self.default_path}"
+
         )
         if depth is not None:
             depth -= 1
             if(depth < 0):
                 return string
         string += (
-            '\n' + str_indent(indent) + 'Files'
-            + dict_pretty_str(self._files, indent+1, depth, 1, 1) + '\n'
-            + str_indent(indent) + self.tiles.pretty_str(indent+1, depth)
+            ' ImageData ' + self._image_data.pretty_str(indent+1, depth)
         )
         return string
 
-    @property
-    def default_z(self) -> float:
-        return self.tiles.default_z
-
-    @property
-    def default_path(self) -> str:
-        return self.tiles.default_path
-
-    @property
-    def files(self) -> List[WsiDicomFile]:
-        """Return list of files"""
-        return list(self._files.values())
-
-    @property
+    @cached_property
     def wsi_type(self) -> str:
-        """Return wsi flavour"""
-        return self._wsi_type
+        """Return wsi type."""
+        return self.dataset.get_supported_wsi_dicom_type(
+            self._image_data.transfer_syntax
+        )
 
     @property
     def blank_color(self) -> Tuple[int, int, int]:
-        """Return RGB background color"""
-        return self._blank_color
+        """Return RGB background color."""
+        return self._get_blank_color(self.photometric_interpretation)
 
-    @property
-    def blank_tile(self) -> Image:
-        """Return background tile"""
-        return self._blank_tile
+    @cached_property
+    def image_mode(self) -> str:
+        if(self.samples_per_pixel == 1):
+            return "L"
+        else:
+            return "RGB"
 
     @property
-    def blank_encoded_tile(self) -> bytes:
-        """Return encoded background tile"""
-        return self._blank_encoded_tile
+    def datasets(self) -> List[WsiDataset]:
+        return self._datasets
 
     @property
-    def uids(self) -> BaseUids:
-        """Return base uids"""
-        return self._uids
+    def dataset(self) -> WsiDataset:
+        return self.datasets[0]
 
     @property
     def size(self) -> Size:
-        """Return image size in pixels"""
-        return self._size
+        """Return image size in pixels."""
+        return self._image_data.image_size
 
     @property
     def tile_size(self) -> Size:
-        """Return tile size in pixels"""
-        return self._tile_size
+        """Return tile size in pixels."""
+        return self._image_data.tile_size
 
     @property
     def mpp(self) -> SizeMm:
-        """Return pixel spacing in um/pixel"""
+        """Return pixel spacing in um/pixel."""
         return self.pixel_spacing*1000.0
 
     @property
     def pixel_spacing(self) -> SizeMm:
-        """Return pixel spacing in mm/pixel"""
-        return self._pixel_spacing
+        """Return pixel spacing in mm/pixel."""
+        return self._image_data.pixel_spacing
 
     @property
-    def identifier(self) -> Uid:
-        """Return identifier (instance uid for single file instance or
-        concatenation uid for multiple file instance)"""
-        return self._identifier
+    def mm_size(self) -> SizeMm:
+        """Return slide size in mm."""
+        return self.dataset.mm_size
 
-    @staticmethod
-    def check_duplicate_instance(
-        instances: List['WsiDicomInstance'],
-        self: object
-    ) -> None:
-        """Check for duplicates in list of instances. Instances are duplicate
-        if instance identifier (file instance uid or concatenation uid) match.
-        Stops at first found duplicate and raises WsiDicomUidDuplicateError.
+    @property
+    def mm_depth(self) -> float:
+        """Return imaged depth in mm."""
+        return self.dataset.mm_depth
 
-        Parameters
-        ----------
-        instances: List['WsiDicomInstance']
-            List of instances to check.
-        caller: Object
-            Object that the instances belongs to.
-        """
-        instance_identifiers: List[str] = []
-        for instance in instances:
-            instance_identifier = instance.identifier
-            if instance_identifier not in instance_identifiers:
-                instance_identifiers.append(instance_identifier)
-            else:
-                raise WsiDicomUidDuplicateError(str(instance), str(self))
+    @property
+    def slice_thickness(self) -> float:
+        """Return slice thickness."""
+        return self.dataset.slice_thickness
 
-    @classmethod
-    def open(
-        cls,
-        files: List[WsiDicomFile],
-        optical: OpticalManager
-    ) -> List['WsiDicomInstance']:
-        """Return list of instances created from files.
+    @property
+    def slice_spacing(self) -> float:
+        """Return slice spacing."""
+        return self.dataset.spacing_between_slices
 
-        Parameters
-        ----------
-        files: List[WsiDicomFile]
-            Files to create instances from.
-        optical: OpticalManager
-            Optical manager to add optical paths to.
+    @property
+    def focus_method(self) -> str:
+        return self.dataset.focus_method
 
-        Returns
-        ----------
-        List[WsiDicomInstance]
-            List of created instances.
-        """
-        instances: List[WsiDicomInstance] = []
-        files_grouped_by_instance = cls._group_files(files)
+    @property
+    def ext_depth_of_field(self) -> bool:
+        return self.dataset.ext_depth_of_field
 
-        for instance_files in files_grouped_by_instance.values():
-            new_instance = WsiDicomInstance(instance_files, optical)
-            instances.append(new_instance)
+    @property
+    def ext_depth_of_field_planes(self) -> Optional[int]:
+        return self.dataset.ext_depth_of_field_planes
 
-        return instances
+    @property
+    def ext_depth_of_field_plane_distance(self) -> Optional[float]:
+        return self.dataset.ext_depth_of_field_plane_distance
 
-    def matches(self, other_instance: 'WsiDicomInstance') -> bool:
-        """Return true if other instance is of the same stack as self.
+    @property
+    def photometric_interpretation(self) -> str:
+        """Return photometric interpretation."""
+        return self.dataset.photophotometric_interpretation
 
-        Parameters
-        ----------
-        other_instance: WsiDicomInstance
-            Instance to check
+    @property
+    def samples_per_pixel(self) -> int:
+        """Return samples per pixel (1 or 3)."""
+        return self.dataset.samples_per_pixel
 
-        Returns
-        ----------
-        bool
-            True if instanes are of same stack.
+    @property
+    def identifier(self) -> Uid:
+        """Return identifier (instance uid for single file instance or
+        concatenation uid for multiple file instance)."""
+        return self._identifier
 
-        """
-        return (
-            self.uids == other_instance.uids and
-            self.size == other_instance.size and
-            self.tile_size == other_instance.tile_size and
-            self.wsi_type == other_instance.wsi_type
-        )
+    @property
+    def instance_number(self) -> int:
+        return self.dataset.instance_number
 
-    def create_ds(self) -> Dataset:
-        """Create a base pydicom dataset based on first file in instance.
+    @property
+    def default_z(self) -> float:
+        return self._image_data.default_z
 
-        Returns
-        ----------
-        pydicom.dataset
-            Pydicom dataset with common attributes for the levels.
-        """
-        INCLUDE = [0x0002, 0x0008, 0x0010, 0x0018, 0x0020, 0x0040]
-        DELETE = ['ImageType', 'SOPInstanceUID', 'ContentTime',
-                  'InstanceNumber', 'DimensionOrganizationSequence']
+    @property
+    def default_path(self) -> str:
+        return self._image_data.default_path
 
-        base_file_path = self._files[0].filepath
-        base_ds = pydicom.dcmread(base_file_path, stop_before_pixels=True)
-        ds = pydicom.Dataset()
-        for group in INCLUDE:
-            group_ds = base_ds.group_dataset(group)
-            for element in group_ds.iterall():
-                ds.add(element)
-        for delete in DELETE:
-            ds.pop(delete)
-        return ds
+    @property
+    def focal_planes(self) -> List[float]:
+        return self._image_data.focal_planes
 
-    def encode(self, image: Image) -> bytes:
-        """Encode image using transfer syntax.
+    @property
+    def optical_paths(self) -> List[str]:
+        return self._image_data.optical_paths
+
+    @property
+    def tiled_size(self) -> Size:
+        return self._image_data.tiled_size
+
+    @property
+    def datasets(self) -> List[WsiDataset]:
+        return self._datasets
+
+    @property
+    def uids(self) -> Optional[BaseUids]:
+        """Return base uids"""
+        return self._uids
+
+    @classmethod
+    def open(
+        cls,
+        files: List[WsiDicomFile],
+        series_uids: BaseUids,
+        series_tile_size: Size = None
+    ) -> List['WsiInstance']:
+        """Create instances from Dicom files. Only files with matching series
+        uid and tile size, if defined, are used. Other files are closed.
 
         Parameters
         ----------
-        image: Image
-            Image to encode
+        files: List[WsiDicomFile]
+            Files to create instances from.
+        series_uids: BaseUids
+            Uid to match against.
+        series_tile_size: Size
+            Tile size to match against (for level instances).
 
         Returns
         ----------
-        bytes
-            Encoded image as bytes
-
+        List[WsiInstancece]
+            List of created instances.
         """
-        (image_format, image_options) = self._image_settings(
-            self._transfer_syntax
+        filtered_files = WsiDicomFile._filter_files(
+            files,
+            series_uids,
+            series_tile_size
         )
-        with io.BytesIO() as buffer:
-            image.save(buffer, format=image_format, **image_options)
-            return buffer.getvalue()
+        files_grouped_by_instance = WsiDicomFile._group_files(filtered_files)
+        return [
+            WsiInstance(
+                [file.dataset for file in instance_files],
+                DicomImageData(instance_files)
+            )
+            for instance_files in files_grouped_by_instance.values()
+        ]
 
     def stitch_tiles(
         self,
@@ -1554,105 +1755,66 @@ def stitch_tiles(
         Returns
         ----------
         Image
-            Stitched image
-        """
-        if z is None:
-            z = self.default_z
-        if path is None:
-            path = self.default_path
-        stitching_tiles = self.tiles.get_range(region, z, path)
-        image = Image.new(mode=self._image_mode, size=region.size.to_tuple())
-        write_index = Point(x=0, y=0)
-        tile = stitching_tiles.position
-        for x, y in stitching_tiles.iterate_all(include_end=True):
-            tile = Point(x=x, y=y)
-            tile_image = self.get_tile(tile, z, path)
-            tile_crop = self.crop_tile(tile, region)
-            tile_image = tile_image.crop(box=tile_crop.box)
-            image.paste(tile_image, write_index.to_tuple())
-            write_index = self._write_indexer(
-                write_index,
-                tile_crop.size,
-                region.size
-            )
-        return image
-
-    def get_tile(
-        self,
-        tile: Point,
-        z: float = None,
-        path: str = None
-    ) -> Image:
-        """Get tile image at tile coordinate x, y.
-        If frame is inside tile geometry but no tile exists in
-        frame data (sparse) returns blank image
-
-        Parameters
-        ----------
-        tile: Point
-            Tile x, y coordinate
-        z: float
-            Z coordinate
-        path: str
-            Optical path
-
-        Returns
-        ----------
-        Image
-            Tile image
+            Stitched image
         """
         if z is None:
             z = self.default_z
         if path is None:
             path = self.default_path
-        try:
-            frame_index = self._get_frame_index(tile, z, path)
-            tile_frame = self._get_tile_frame(frame_index)
-            image = Image.open(io.BytesIO(tile_frame))
-        except WsiDicomSparse:
-            image = self.blank_tile
+        stitching_tiles = self.get_tile_range(region, z, path)
+        image = Image.new(mode=self.image_mode, size=region.size.to_tuple())
 
-        # Check if tile is an edge tile that should be croped
-        cropped_tile_region = self.crop_to_level_size(tile)
-        if cropped_tile_region.size != self.tile_size:
-            image = image.crop(box=cropped_tile_region.box_from_origin)
+        write_index = Point(x=0, y=0)
+        tile = stitching_tiles.position
+        for tile in stitching_tiles.iterate_all(include_end=True):
+            tile_image = self.get_tile(tile, z, path)
+            tile_crop = self.crop_tile(tile, region)
+            tile_image = tile_image.crop(box=tile_crop.box)
+            image.paste(tile_image, write_index.to_tuple())
+            write_index = self._write_indexer(
+                write_index,
+                tile_crop.size,
+                region.size
+            )
         return image
 
-    def get_encoded_tile(
+    def get_tile_range(
         self,
-        tile: Point,
-        z: float = None,
-        path: str = None
-    ) -> bytes:
-        """Get tile bytes at tile coordinate x, y
-        If frame is inside tile geometry but no tile exists in
-        frame data (sparse) returns encoded blank image.
+        pixel_region: Region,
+        z: float,
+        path: str
+    ) -> Region:
+        """Return range of tiles to cover pixel region.
 
         Parameters
         ----------
-        tile: Point
-            Tile x, y coordinate
+        pixel_region: Region
+            Pixel region of tiles to get
         z: float
-            Z coordinate
+            Z coordinate of tiles to get
         path: str
-            Optical path
+            Optical path identifier of tiles to get
 
         Returns
         ----------
-        bytes
-            Tile image as bytes
+        Region
+            Region of tiles for stitching image
         """
-        if z is None:
-            z = self.default_z
-        if path is None:
-            path = self.default_path
-        try:
-            frame_index = self._get_frame_index(tile, z, path)
-            tile_frame = self._get_tile_frame(frame_index)
-        except WsiDicomSparse:
-            tile_frame = self.blank_encoded_tile
+        start = pixel_region.start // self._image_data.tile_size
+        end = pixel_region.end / self._image_data.tile_size - 1
+        tile_region = Region.from_points(start, end)
+        if not self._image_data.valid_tiles(tile_region, z, path):
+            raise WsiDicomOutOfBoundsError(
+                f"Tile region {tile_region}",
+                f"tiled size {self._image_data.tiled_size}"
+            )
+        return tile_region
 
-        # Check if tile is an edge tile that should be croped
+    def crop_encoded_tile_to_level(
+        self,
+        tile: Point,
+        tile_frame: bytes
+    ) -> bytes:
         cropped_tile_region = self.crop_to_level_size(tile)
         if cropped_tile_region.size != self.tile_size:
             image = Image.open(io.BytesIO(tile_frame))
@@ -1660,33 +1822,38 @@ def get_encoded_tile(
             tile_frame = self.encode(image)
         return tile_frame
 
-    def get_filepointer(
+    def crop_tile_to_level(
         self,
         tile: Point,
-        z: float,
-        path: str
-    ) -> Tuple[pydicom.filebase.DicomFileLike, int, int]:
-        """Return file pointer, frame position, and frame lenght for tile with
-        z and path. If frame is inside tile geometry but no tile exists in
-        frame data (sparse) WsiDicomSparse is raised.
+        tile_image: Image
+    ) -> Image:
+        cropped_tile_region = self.crop_to_level_size(tile)
+        if cropped_tile_region.size != self.tile_size:
+            tile_image = tile_image.crop(
+                box=cropped_tile_region.box_from_origin
+            )
+        return tile_image
+
+    def encode(self, image: Image) -> bytes:
+        """Encode image using transfer syntax.
 
         Parameters
         ----------
-        tile: Point
-            Tile coordinate to get.
-        z: float
-            z coordinate to get tile for.
-        path: str
-            Optical path to get tile for.
+        image: Image
+            Image to encode
 
         Returns
         ----------
-        Tuple[pydicom.filebase.DicomFileLike, int, int]:
-            File pointer, frame offset and frame lenght in number of bytes
+        bytes
+            Encoded image as bytes
+
         """
-        frame_index = self._get_frame_index(tile, z, path)
-        file = self._get_file(frame_index)
-        return file.get_filepointer(frame_index)
+        (image_format, image_options) = self._image_settings(
+            self._image_data.transfer_syntax
+        )
+        with io.BytesIO() as buffer:
+            image.save(buffer, format=image_format, **image_options)
+            return buffer.getvalue()
 
     def crop_tile(self, tile: Point, stitching: Region) -> Region:
         """Crop tile at edge of stitching region so that the tile after croping
@@ -1736,137 +1903,35 @@ def crop_to_level_size(self, item: Union[Point, Region]) -> Region:
             return self.crop_tile(item, level_region)
         return level_region.crop(item)
 
-    def close(self) -> None:
-        """Close all files in the instance."""
-        for file in self._files.values():
-            file.close()
-
-    def _validate_instance(
-        self,
-        optical: OpticalManager
-    ) -> Tuple[str, BaseUids, str]:
-        """Check that no files in instance are duplicate, that all files in
-        instance matches (uid, type and size) and that the optical manager
-        matches by base uid. Raises WsiDicomMatchError otherwise.
-        Returns the matching file uid.
-
-        Parameters
-        ----------
-        optical: OpticalManager
-            Optical manager to check
-
-        Returns
-        ----------
-        Tuple[str, BaseUids, str]
-            Instance identifier uid, base uids and wsi type
-        """
-        WsiDicomFile.check_duplicate_file(self.files, self)
-        base_file = self.files[0]
-        if not base_file.uids.base == optical.uids:
-            raise WsiDicomMatchError(
-                str(base_file.filepath), str(self)
-            )
-        for file in self.files[1:]:
-            if not base_file.matches_instance(file):
-                raise WsiDicomMatchError(
-                    str(file.filepath), str(self)
-                )
-        return (
-            base_file.uids.identifier,
-            base_file.uids.base,
-            base_file.wsi_type
-        )
-
-    @classmethod
-    def _group_files(
-        cls,
-        files: List[WsiDicomFile]
-    ) -> Dict[str, List[WsiDicomFile]]:
-        """Return files grouped by instance identifier (instances).
-
-        Parameters
-        ----------
-        files: List[WsiDicomFile]
-            Files to group into instances
-
-        Returns
-        ----------
-        Dict[str, List[WsiDicomFile]]
-            Files grouped by instance, with instance identifier as key.
-        """
-        grouped_files: Dict[str, List[WsiDicomFile]] = {}
-        for file in files:
-            try:
-                grouped_files[file.uids.identifier].append(file)
-            except KeyError:
-                grouped_files[file.uids.identifier] = [file]
-        return grouped_files
-
-    def _create_blank_tile(self) -> Image:
-        """Create blank tile for instance.
-
-        Returns
-        ----------
-        Image
-            Blank tile image
-        """
-        if(self._samples_per_pixel == 1):
-            self._image_mode = "L"
-        else:
-            self._image_mode = "RGB"
-        return Image.new(
-            mode=self._image_mode,
-            size=self.tile_size.to_tuple(),
-            color=self.blank_color[:self._samples_per_pixel]
-        )
-
     @staticmethod
-    def _get_blank_color(
-        photometric_interpretation: str
-    ) -> Tuple[int, int, int]:
-        """Return color to use blank tiles.
+    def _write_indexer(
+        index: Point,
+        previous_size: Size,
+        image_size: Size
+    ) -> Point:
+        """Increment index in x by previous width until index x exceds image
+        size. Then resets index x to 0 and increments index y by previous
+        height. Requires that tiles are scanned row by row.
 
         Parameters
         ----------
-        photometric_interpretation: str
-            The photomoetric interpretation of the dataset
+        index: Point
+            The last write index position
+        previouis_size: Size
+            The size of the last written last tile
+        image_size: Size
+            The size of the image to be written
 
         Returns
         ----------
-        int, int, int
-            RGB color
-
-        """
-        BLACK = 0
-        WHITE = 255
-        if(photometric_interpretation == "MONOCHROME2"):
-            return (BLACK, BLACK, BLACK)  # Monocrhome2 is black
-        return (WHITE, WHITE, WHITE)
-
-    @staticmethod
-    def _create_tileindex(
-        files: Dict[int, WsiDicomFile],
-        optical: OpticalManager
-    ) -> TileIndex:
-        """Return a tile index created from files. Add optical paths to optical
-        manager.
-
-        Parameters
-        ----------
-        files: Dict[int, WsiDicomFile]
-            Files to add
-        optical: OpticalManager
-            Optical manager to add new optical paths to
-
-        Returns
-        ----------w
-        TileIndex
-            Created tile index
+        Point
+            The position (upper right) in image to insert the next tile into
         """
-        base_file = files[0]
-        if(base_file.tile_type == 'TILED_FULL'):
-            return FullTileIndex(files, optical)
-        return SparseTileIndex(files, optical)
+        index.x += previous_size.width
+        if(index.x >= image_size.width):
+            index.x = 0
+            index.y += previous_size.height
+        return index
 
     @staticmethod
     def _image_settings(
@@ -1901,152 +1966,271 @@ def _image_settings(
             )
         return (image_format, image_options)
 
-    def _encode_tile(
-        self,
-        tile: Image,
-    ) -> bytes:
-        """Encode tile using transfer syntax.
+    def get_tile(
+        self,
+        tile: Point,
+        z: float = None,
+        path: str = None
+    ) -> Image:
+        """Get tile image at tile coordinate x, y.
+        If frame is inside tile geometry but no tile exists in
+        frame data (sparse) returns blank image.
+
+        Parameters
+        ----------
+        tile: Point
+            Tile x, y coordinate.
+        z: float
+            Z coordinate.
+        path: str
+            Optical path.
+
+        Returns
+        ----------
+        Image
+            Tile image.
+        """
+        if z is None:
+            z = self.default_z
+        if path is None:
+            path = self.default_path
+        try:
+            tile_frame = self._image_data.get_tile(tile, z, path)
+            image = Image.open(io.BytesIO(tile_frame))
+        except WsiDicomSparse:
+            image = self.blank_tile
+        return self.crop_tile_to_level(tile, image)
+
+    def get_encoded_tile(
+        self,
+        tile: Point,
+        z: float,
+        path: str
+    ) -> bytes:
+        """Get tile bytes at tile coordinate x, y
+        If frame is inside tile geometry but no tile exists in
+        frame data (sparse) returns encoded blank image.
+
+        Parameters
+        ----------
+        tile: Point
+            Tile x, y coordinate.
+        z: float
+            Z coordinate.
+        path: str
+            Optical path.
+        crop: bool
+            If the tile should be croped to image region.
+
+        Returns
+        ----------
+        bytes
+            Tile image as bytes.
+        """
+        if z is None:
+            z = self.default_z
+        if path is None:
+            path = self.default_path
+        try:
+            tile_frame = self._image_data.get_tile(tile, z, path)
+        except WsiDicomSparse:
+            tile_frame = self.blank_encoded_tile
+
+        # Check if tile is an edge tile that should be croped
+        cropped_tile_region = self.crop_to_level_size(tile)
+        if cropped_tile_region.size != self.tile_size:
+            image = Image.open(io.BytesIO(tile_frame))
+            image.crop(box=cropped_tile_region.box_from_origin)
+            tile_frame = self.encode(image)
+        return tile_frame
+
+    @staticmethod
+    def check_duplicate_instance(
+        instances: List['WsiInstance'],
+        self: object
+    ) -> None:
+        """Check for duplicates in list of instances. Instances are duplicate
+        if instance identifier (file instance uid or concatenation uid) match.
+        Stops at first found duplicate and raises WsiDicomUidDuplicateError.
 
         Parameters
         ----------
-        tile: Image
-            Tile image to encode
+        instances: List['WsiInstance']
+            List of instances to check.
+        caller: Object
+            Object that the instances belongs to.
+        """
+        instance_identifiers: List[str] = []
+        for instance in instances:
+            instance_identifier = instance.identifier
+            if instance_identifier not in instance_identifiers:
+                instance_identifiers.append(instance_identifier)
+            else:
+                raise WsiDicomUidDuplicateError(str(instance), str(self))
+
+    def _validate_instance(
+        self,
+        datasets: List[WsiDataset]
+    ) -> Tuple[str, BaseUids, str]:
+        """Check that no files in instance are duplicate, that all files in
+        instance matches (uid, type and size).
+        Raises WsiDicomMatchError otherwise.
+        Returns the matching file uid.
+
 
         Returns
         ----------
-        bytes
-            Encoded tile as bytes
+        Tuple[str, BaseUids, str]
+            Instance identifier uid and base uids
         """
-        (image_format, image_options) = self._image_settings(
-            self._transfer_syntax
-        )
+        WsiDataset.check_duplicate_dataset(datasets, self)
 
-        with io.BytesIO() as buffer:
-            tile.save(buffer, format=image_format, **image_options)
-            return buffer.getvalue()
+        base_dataset = datasets[0]
+        for dataset in datasets[1:]:
+            if not base_dataset.matches_instance(dataset):
+                raise WsiDicomError()
+        return (
+            base_dataset.uids.identifier,
+            base_dataset.uids.base,
+        )
 
     @staticmethod
-    def _write_indexer(
-        index: Point,
-        previous_size: Size,
-        image_size: Size
-    ) -> Point:
-        """Increment index in x by previous width until index x exceds image
-        size. Then resets index x to 0 and increments index y by previous
-        height. Requires that tiles are scanned row by row.
+    def _get_blank_color(
+        photometric_interpretation: str
+    ) -> Tuple[int, int, int]:
+        """Return color to use blank tiles.
 
         Parameters
         ----------
-        index: Point
-            The last write index position
-        previouis_size: Size
-            The size of the last written last tile
-        image_size: Size
-            The size of the image to be written
+        photometric_interpretation: str
+            The photomoetric interpretation of the dataset
 
         Returns
         ----------
-        Point
-            The position (upper right) in image to insert the next tile into
+        int, int, int
+            RGB color
+
         """
-        index.x += previous_size.width
-        if(index.x >= image_size.width):
-            index.x = 0
-            index.y += previous_size.height
-        return index
+        BLACK = 0
+        WHITE = 255
+        if(photometric_interpretation == "MONOCHROME2"):
+            return (BLACK, BLACK, BLACK)  # Monocrhome2 is black
+        return (WHITE, WHITE, WHITE)
 
-    def _get_file(self, frame_index: int) -> WsiDicomFile:
-        """Return file contaning frame index. Raises WsiDicomNotFoundError if
-        frame is not found.
+    def get_tile_range(
+        self,
+        pixel_region: Region,
+        z: float,
+        path: str
+    ) -> Region:
+        start = pixel_region.start // self.tile_size
+        end = pixel_region.end / self.tile_size - 1
+        tile_region = Region.from_points(start, end)
+        return tile_region
+
+    @staticmethod
+    def _create_filepointer(path: Path) -> pydicom.filebase.DicomFile:
+        """Return a dicom filepointer.
 
         Parameters
         ----------
-        frame_index: int
-             Frame index to get
-
+        path: Path
+            Path to filepointer.
         Returns
         ----------
-        WsiDicomFile
-            File containing the frame
+        pydicom.filebase.DicomFile
+            Created filepointer.
         """
-        for frame_offset, file in self._files.items():
-            if (frame_index < frame_offset + file.frame_count and
-                    frame_index >= frame_offset):
-                return file
+        fp = pydicom.filebase.DicomFile(path, mode='wb')
+        fp.is_little_endian = True
+        fp.is_implicit_VR = False
+        return fp
 
-        raise WsiDicomNotFoundError(f"Frame index {frame_index}", "instance")
+    @cached_property
+    def blank_tile(self) -> Image:
+        """Return background tile."""
+        return self._create_blank_tile()
 
-    def _get_tile_frame(self, frame_index: int) -> bytes:
-        """Return tile frame for frame index.
+    @cached_property
+    def blank_encoded_tile(self) -> bytes:
+        """Return encoded background tile."""
+        return self.encode(self.blank_tile)
+
+    def matches(self, other_instance: 'WsiInstance') -> bool:
+        """Return true if other instance is of the same group as self.
 
         Parameters
         ----------
-        frame_index: int
-             Frame index to get
+        other_instance: WsiInstance
+            Instance to check.
 
         Returns
         ----------
-        bytes
-            The frame in bytes
+        bool
+            True if instanes are of same group.
+
         """
-        file = self._get_file(frame_index)
-        tile_frame = file._read_frame(frame_index)
-        return tile_frame
+        return (
+            self.uids == other_instance.uids and
+            self.size == other_instance.size and
+            self.tile_size == other_instance.tile_size and
+            self.wsi_type == other_instance.wsi_type
+        )
 
-    def _get_frame_index(self, tile: Point, z: float, path: str) -> int:
-        """Return frame index for tile. Raises WsiDicomOutOfBondsError if
-        tile, z, or path is not valid. Raises WsiDicomSparse if index is sparse
-        and tile is not in frame data.
+    def is_sparse(self, tile: Point, z: float, path: str) -> bool:
+        try:
+            self._image_data.get_tile(tile, z, path)
+            return False
+        except WsiDicomSparse:
+            return True
 
-        Parameters
-        ----------
-        tile: Point
-             Tile coordiante
-        z: float
-            Z coordiante
-        path: str
-            Optical identifier
+    def _create_blank_tile(self) -> Image:
+        """Create blank tile for instance.
 
         Returns
         ----------
-        int
-            Tile frame index
+        Image
+            Blank tile image
         """
-        tile_region = Region(position=tile, size=Size(0, 0))
-        if not self.tiles.valid_tiles(tile_region, z, path):
-            raise WsiDicomOutOfBondsError(
-                f"Tile region {tile_region}",
-                f"plane {self.tiles.plane_size}"
-            )
-        frame_index = self.tiles.get_frame_index(tile, z, path)
-        return frame_index
+        return Image.new(
+            mode=self.image_mode,
+            size=self.tile_size.to_tuple(),
+            color=self.blank_color[:self.samples_per_pixel]
+        )
+
+    def close(self) -> None:
+        self._image_data.close()
 
 
-class WsiDicomStack(metaclass=ABCMeta):
+class WsiDicomGroup:
+    """Represents a group of instances having the same size, but possibly
+    different z coordinate and/or optical path."""
     def __init__(
         self,
-        instances: List[WsiDicomInstance]
+        instances: List[WsiInstance]
     ):
-        """Represents a stack of instances having the same size,
-        but possibly different z coordinate and/or optical path.
-        Instances should match in the common uids, wsi type, and tile size.
+        """Create a group of WsiInstances. Instances should match in the common
+        uids, wsi type, and tile size.
 
         Parameters
         ----------
-        instances: List[WsiDicomInstance]
-            Instances to build the stack
+        instances: List[WsiInstance]
+            Instances to build the group.
         """
         self._instances = {  # key is identifier (str)
             instance.identifier: instance for instance in instances
         }
-        self._uids, self._wsi_type = self._validate_stack()
+        self._validate_group()
 
         base_instance = instances[0]
+        self._wsi_type = base_instance.wsi_type
+        self._uids = base_instance.uids
+
         self._size = base_instance.size
         self._pixel_spacing = base_instance.pixel_spacing
         self._default_instance_uid: str = base_instance.identifier
 
-    def __getitem__(self, index) -> WsiDicomInstance:
+    def __getitem__(self, index) -> WsiInstance:
         return self.instances[index]
 
     @property
@@ -2056,7 +2240,7 @@ def uids(self) -> BaseUids:
 
     @property
     def wsi_type(self) -> str:
-        """Return wsi flavour"""
+        """Return wsi type"""
         return self._wsi_type
 
     @property
@@ -2075,12 +2259,12 @@ def pixel_spacing(self) -> SizeMm:
         return self._pixel_spacing
 
     @property
-    def instances(self) -> Dict[str, WsiDicomInstance]:
+    def instances(self) -> Dict[str, WsiInstance]:
         """Return contained instances"""
         return self._instances
 
     @property
-    def default_instance(self) -> WsiDicomInstance:
+    def default_instance(self) -> WsiInstance:
         """Return default instance"""
         return self.instances[self._default_instance_uid]
 
@@ -2092,49 +2276,70 @@ def files(self) -> List[WsiDicomFile]:
         ]
         return [file for sublist in instance_files for file in sublist]
 
+    @property
+    def datasets(self) -> List[Dataset]:
+        """Return contained datasets."""
+        instance_datasets = [
+            instance.datasets for instance in self.instances.values()
+        ]
+        return [
+            dataset for sublist in instance_datasets for dataset in sublist
+        ]
+
+    @property
+    def optical_paths(self) -> List[str]:
+        return list({
+            path
+            for instance in self.instances.values()
+            for path in instance.optical_paths
+        })
+
+    @property
+    def focal_planes(self) -> List[float]:
+        return list({
+            focal_plane
+            for innstance in self.instances.values()
+            for focal_plane in innstance.focal_planes
+        })
+
     @classmethod
     def open(
         cls,
-        files: List[WsiDicomFile],
-        optical: OpticalManager
-    ) -> List['WsiDicomStack']:
-        """Return list of stacks created from wsi files.
+        instances: List[WsiInstance],
+    ) -> List['WsiDicomGroup']:
+        """Return list of groups created from wsi instances.
 
         Parameters
         ----------
-        files: List[WsiDicomFile]
-            Files to create stacks from.
-        optical: OpticalManager
-            Optical manager to add optical paths to.
+        files: List[WsiInstance]
+            Instances to create groups from.
 
         Returns
         ----------
-        List[WsiDicomStack]
-            List of created stacks.
+        List[WsiDicomGroup]
+            List of created groups.
 
         """
-        stacks: List[WsiDicomStack] = []
+        groups: List[WsiDicomGroup] = []
 
-        instances = WsiDicomInstance.open(files, optical)
-        instances_grouped_by_stack = cls._group_instances(instances)
+        grouped_instances = cls._group_instances(instances)
 
-        for instance_group in instances_grouped_by_stack.values():
-            new_stack = WsiDicomStack(instance_group)
-            stacks.append(new_stack)
+        for group in grouped_instances.values():
+            groups.append(WsiDicomGroup(group))
 
-        return stacks
+        return groups
 
-    def matches(self, other_stack: 'WsiDicomStack') -> bool:
-        """Check if stack is valid (Uids and tile size match).
+    def matches(self, other_group: 'WsiDicomGroup') -> bool:
+        """Check if group is valid (Uids and tile size match).
         The common Uids should match for all series.
         """
         return (
-            other_stack.uids == self.uids and
-            other_stack.wsi_type == self.wsi_type
+            other_group.uids == self.uids and
+            other_group.wsi_type == self.wsi_type
         )
 
     def valid_pixels(self, region: Region) -> bool:
-        """Check if pixel region is withing the size of the stack image size.
+        """Check if pixel region is withing the size of the group image size.
 
         Parameters
         ----------
@@ -2154,7 +2359,7 @@ def get_instance(
         self,
         z: float = None,
         path: str = None
-    ) -> WsiDicomInstance:
+    ) -> WsiInstance:
         """Search for instance fullfilling the parameters.
         The behavior when z and/or path is none could be made more
         clear.
@@ -2168,39 +2373,43 @@ def get_instance(
 
         Returns
         ----------
-        WsiDicomInstance
-            The instance containing selected path and z coordinate.
+        WsiInstance
+            The instance containing selected path and z coordinate
         """
         if z is None and path is None:
             instance = self.default_instance
-            z = instance.tiles.default_z
-            path = instance.tiles.default_path
+            z = instance.default_z
+            path = instance.default_path
+
             return self.default_instance
 
         # Sort instances by number of focal planes (prefer simplest instance)
         sorted_instances = sorted(
             list(self._instances.values()),
-            key=lambda i: len(i.tiles.focal_planes)
+            key=lambda i: len(i.focal_planes)
         )
         try:
             if z is None:
                 # Select the instance with selected optical path
                 instance = next(i for i in sorted_instances if
-                                path in i.tiles.optical_paths)
+                                path in i.optical_paths)
             elif path is None:
                 # Select the instance with selected z
                 instance = next(i for i in sorted_instances
-                                if z in i.tiles.focal_planes)
+                                if z in i.focal_planes)
             else:
                 # Select by both path and z
                 instance = next(i for i in sorted_instances
-                                if (z in i.tiles.focal_planes and
-                                    path in i.tiles.optical_paths))
+                                if (z in i.focal_planes and
+                                    path in i.optical_paths))
         except StopIteration:
             raise WsiDicomNotFoundError(
-                f"Instance for path: {path}, z: {z}",
-                "stack"
+                f"Instance for path: {path}, z: {z}", "group"
             )
+        if z is None:
+            z = instance.default_z
+        if path is None:
+            path = instance.default_path
         return instance
 
     def get_default_full(self) -> Image:
@@ -2209,11 +2418,11 @@ def get_default_full(self) -> Image:
         Returns
         ----------
         Image
-            Full image of the stack
+            Full image of the group.
         """
         instance = self.default_instance
-        z = instance.tiles.default_z
-        path = instance.tiles.default_path
+        z = instance.default_z
+        path = instance.default_path
         region = Region(position=Point(x=0, y=0), size=self.size)
         image = self.get_region(region, z, path)
         return image
@@ -2324,7 +2533,6 @@ def get_encoded_tile(
             The tile as bytes
         """
         instance = self.get_instance(z, path)
-
         return instance.get_encoded_tile(tile, z, path)
 
     def mm_to_pixel(self, region: RegionMm) -> Region:
@@ -2345,57 +2553,48 @@ def mm_to_pixel(self, region: RegionMm) -> Region:
             size=region.size // self.pixel_spacing
         )
         if not self.valid_pixels(pixel_region):
-            raise WsiDicomOutOfBondsError(
+            raise WsiDicomOutOfBoundsError(
                 f"Region {region}", f"level size {self.size}"
             )
         return pixel_region
 
     def close(self) -> None:
-        """Close all instances on the stack."""
+        """Close all instances on the group."""
         for instance in self._instances.values():
             instance.close()
 
-    def _validate_stack(
-        self,
-    ) -> Tuple[BaseUids, str]:
-        """Check that no file or instance in stack is duplicate, instances in
-        stack matches and that the optical manager matches by base uid.
+    def _validate_group(self):
+        """Check that no file or instance in group is duplicate, instances in
+        group matches and that the optical manager matches by base uid.
         Raises WsiDicomMatchError otherwise.
-        Returns the matching base uid.
-
-        Returns
-        ----------
-        Tuple[BaseUids, str]
-            Matching uids and wsi type
         """
+        WsiDataset.check_duplicate_dataset(self.datasets, self)
         instances = list(self.instances.values())
-        WsiDicomFile.check_duplicate_file(self.files, self)
-        WsiDicomInstance.check_duplicate_instance(instances, self)
         base_instance = instances[0]
         for instance in instances[1:]:
             if not base_instance.matches(instance):
                 raise WsiDicomMatchError(str(instance), str(self))
-        return base_instance.uids, base_instance.wsi_type
+        WsiInstance.check_duplicate_instance(instances, self)
 
     @classmethod
     def _group_instances(
         cls,
-        instances: List[WsiDicomInstance]
-    ) -> Dict[Size, List[WsiDicomInstance]]:
-        """Return instances grouped by image size (stacks).
+        instances: List[WsiInstance]
+    ) -> Dict[Size, List[WsiInstance]]:
+        """Return instances grouped by image size.
 
         Parameters
         ----------
-        instances: List[WsiDicomInstance]
-            Instances to group into stacks.
+        instances: List[WsiInstance]
+            Instances to group by image size.
 
         Returns
         ----------
-        Dict[Size, List[WsiDicomInstance]]:
+        Dict[Size, List[WsiInstance]]:
             Instances grouped by size, with size as key.
 
         """
-        grouped_instances: Dict[Size, List[WsiDicomInstance]] = {}
+        grouped_instances: Dict[Size, List[WsiInstance]] = {}
         for instance in instances:
             try:
                 grouped_instances[instance.size].append(instance)
@@ -2404,21 +2603,23 @@ def _group_instances(
         return grouped_instances
 
 
-class WsiDicomLevel(WsiDicomStack):
+class WsiDicomLevel(WsiDicomGroup):
+    """Represents a level in the pyramid and contains one or more instances
+    having the same pyramid level index, pixel spacing, and size but possibly
+    different focal planes and/or optical paths.
+    """
     def __init__(
         self,
-        instances: List[WsiDicomInstance],
+        instances: List[WsiInstance],
         base_pixel_spacing: SizeMm
     ):
-        """Represents a level in the pyramid and contains one or more
-        instances having the same level, pixel spacing, and size but possibly
-        different focal planes and/or optical paths and present in
-        different files.
+        """Create a level from list of WsiInstances. Asign the pyramid level
+        index from pixel spacing of base level.
 
         Parameters
         ----------
-        instances: List[WsiDicomInstance]
-            Instances to build the stack.
+        instances: List[WsiInstance]
+            Instances to build the level.
         base_pixel_spacing: SizeMm
             Pixel spacing of base level.
         """
@@ -2440,7 +2641,9 @@ def pretty_str(
             depth -= 1
             if(depth < 0):
                 return string
-        string += dict_pretty_str(self.instances, indent, depth)
+        string += (
+            ' Instances: ' + dict_pretty_str(self.instances, indent, depth)
+        )
         return string
 
     @property
@@ -2448,7 +2651,7 @@ def pyramid(self) -> str:
         """Return string representation of the level"""
         return (
             f'Level [{self.level}]'
-            f' tiles: {self.default_instance.tiles.plane_size},'
+            f' tiles: {self.default_instance.tiled_size},'
             f' size: {self.size}, mpp: {self.mpp} um/px'
         )
 
@@ -2464,46 +2667,41 @@ def level(self) -> int:
     @classmethod
     def open_levels(
         cls,
-        files: List[WsiDicomFile],
-        optical: OpticalManager
+        instances: List[WsiInstance],
     ) -> List['WsiDicomLevel']:
-        """Return list of level stacks created wsi files.
+        """Return list of levels created wsi files.
 
         Parameters
         ----------
-        files: List[WsiDicomFile]
-            Files to create stacks from
-        optical: OpticalManager
-            Optical manager to add optical paths to.
+        files: List[WsiInstance]
+            Instances to create levels from.
 
         Returns
         ----------
         List[WsiDicomLevel]
-            List of created level stacks.
+            List of created levels.
 
         """
         levels: List[WsiDicomLevel] = []
-        instances = WsiDicomInstance.open(files, optical)
-        instances_grouped_by_stack = cls._group_instances(instances)
-        largest_size = max(instances_grouped_by_stack.keys())
-        base_group = instances_grouped_by_stack[largest_size]
+        instances_grouped_by_level = cls._group_instances(instances)
+        largest_size = max(instances_grouped_by_level.keys())
+        base_group = instances_grouped_by_level[largest_size]
         base_pixel_spacing = base_group[0].pixel_spacing
-        for level_group in instances_grouped_by_stack.values():
-            new_level = WsiDicomLevel(level_group, base_pixel_spacing)
-            levels.append(new_level)
+        for level in instances_grouped_by_level.values():
+            levels.append(WsiDicomLevel(level, base_pixel_spacing))
 
         return levels
 
-    def matches(self, other_stack: 'WsiDicomStack') -> bool:
-        """Check if stack is valid (Uids and tile size match).
+    def matches(self, other_group: 'WsiDicomGroup') -> bool:
+        """Check if group is valid (Uids and tile size match).
         The common Uids should match for all series. For level series the tile
-        size should also match. It is assumed that the instances in the stacks
+        size should also match. It is assumed that the instances in the groups
         are matching each other.
         """
-        other_instance = other_stack.default_instance
+        other_instance = other_group.default_instance
         this_instance = self.default_instance
         return (
-            other_stack.uids == self.uids and
+            other_group.uids == self.uids and
             other_instance.tile_size == this_instance.tile_size
         )
 
@@ -2550,7 +2748,7 @@ def get_scaled_tile(
         scaled_region = Region.from_tile(tile, instance.tile_size) * scale
         cropped_region = instance.crop_to_level_size(scaled_region)
         if not self.valid_pixels(cropped_region):
-            raise WsiDicomOutOfBondsError(
+            raise WsiDicomOutOfBoundsError(
                 f"Region {cropped_region}", f"level size {self.size}"
             )
         image = self.get_region(cropped_region, z, path)
@@ -2630,21 +2828,28 @@ def _assign_level(self, base_pixel_spacing: SizeMm) -> int:
 
 
 class WsiDicomSeries(metaclass=ABCMeta):
+    """Represents a series of WsiDicomGroups with the same image flavor, e.g.
+    pyramidal levels, lables, or overviews.
+    """
     wsi_type: str
 
-    def __init__(self, stacks: List[WsiDicomStack]):
-        """Holds a series of stacks of same image flavor
+    def __init__(self, groups: List[WsiDicomGroup]):
+        """Create a WsiDicomSeries from list of WsiDicomGroups.
 
         Parameters
         ----------
-        stacks: List[WsiDicomStack]
-            List of stacks to include in the series
+        groups: List[WsiDicomGroup]
+            List of groups to include in the series.
         """
-        self._stacks: List[WsiDicomStack] = stacks
-        self._uids = self._validate_series(self.stacks)
+        self._groups: List[WsiDicomGroup] = groups
+
+        if len(self.groups) != 0 and self.groups[0].uids is not None:
+            self._uids = self._validate_series(self.groups)
+        else:
+            self._uids = None
 
-    def __getitem__(self, index: int) -> WsiDicomStack:
-        """Get stack by index.
+    def __getitem__(self, index: int) -> WsiDicomGroup:
+        """Get group by index.
 
         Parameters
         ----------
@@ -2653,40 +2858,51 @@ def __getitem__(self, index: int) -> WsiDicomStack:
 
         Returns
         ----------
-        WsiDicomStack
-            The stack at index in the series
+        WsiDicomGroup
+            The group at index in the series
         """
-        return self.stacks[index]
+        return self.groups[index]
 
     def __len__(self) -> int:
         return len(self.stacks)
 
     @property
-    def stacks(self) -> List[WsiDicomStack]:
-        """Return contained stacks"""
-        return self._stacks
+    def groups(self) -> List[WsiDicomGroup]:
+        """Return contained groups."""
+        return self._groups
 
     @property
     def uids(self) -> Optional[BaseUids]:
-        """Return uids"""
+        """Return uids."""
         return self._uids
 
     @property
     def mpps(self) -> List[SizeMm]:
-        """Return contained mpp (um/px)"""
-        return [stack.mpp for stack in self.stacks]
+        """Return contained mpp (um/px)."""
+        return [group.mpp for group in self.groups]
 
     @property
     def files(self) -> List[WsiDicomFile]:
-        """Return contained files"""
-        series_files = [series.files for series in self.stacks]
+        """Return contained files."""
+        series_files = [series.files for series in self.groups]
         return [file for sublist in series_files for file in sublist]
 
     @property
-    def instances(self) -> List[WsiDicomInstance]:
+    def datasets(self) -> List[Dataset]:
+        """Return contained datasets."""
+
+        series_datasets = [
+            series.datasets for series in self.groups
+        ]
+        return [
+            dataset for sublist in series_datasets for dataset in sublist
+        ]
+
+    @property
+    def instances(self) -> List[WsiInstance]:
         """Return contained instances"""
         series_instances = [
-            series.instances.values() for series in self.stacks
+            series.instances.values() for series in self.groups
         ]
         return [
             instance for sublist in series_instances for instance in sublist
@@ -2694,149 +2910,102 @@ def instances(self) -> List[WsiDicomInstance]:
 
     def _validate_series(
             self,
-            stacks: Union[List[WsiDicomStack], List[WsiDicomLevel]]
+            groups: Union[List[WsiDicomGroup], List[WsiDicomLevel]]
     ) -> Optional[BaseUids]:
-        """Check that no files or instances in series is duplicate, all stacks
-        in series matches and that the optical manager matches by base uid.
+        """Check that no files or instances in series is duplicate and that
+        all groups in series matches.
         Raises WsiDicomMatchError otherwise.
-        Returns the matching base uid. If list of stacks is empty, return None.
+        Returns the matching base uid. If list of groups is empty, return None.
 
         Parameters
         ----------
-        stacks: Union[List[WsiDicomStack], List[WsiDicomLevel]]
-            List of stacks to check
-        optical: OpticalManager
-            Optical manager to check
+        groups: Union[List[WsiDicomGroup], List[WsiDicomLevel]]
+            List of groups or levels to check
 
         Returns
         ----------
         Optional[BaseUids]:
             Matching uids
         """
-        WsiDicomFile.check_duplicate_file(self.files, self)
-        WsiDicomInstance.check_duplicate_instance(self.instances, self)
+        WsiDataset.check_duplicate_dataset(self.datasets, self)
+        WsiInstance.check_duplicate_instance(self.instances, self)
 
         try:
-            base_stack = stacks[0]
-            if base_stack.wsi_type != self.wsi_type:
+            base_group = groups[0]
+            # print(base_group.wsi_type)
+            # print(self.wsi_type)
+            if base_group.wsi_type != self.wsi_type:
                 raise WsiDicomMatchError(
-                    str(base_stack), str(self)
+                    str(base_group), str(self)
                 )
-            for stack in stacks[1:]:
-                if not stack.matches(base_stack):
+            for group in groups[1:]:
+                if not group.matches(base_group):
                     raise WsiDicomMatchError(
-                        str(stack), str(self)
+                        str(group), str(self)
                     )
-            return base_stack.uids
+            return base_group.uids
         except IndexError:
             return None
 
-    @staticmethod
-    def _filter_files(
-        files: List[WsiDicomFile],
-        series_uids: BaseUids,
-        series_tile_size: Size = None
-    ) -> List[WsiDicomFile]:
-        """Filter list of wsi dicom files to only include matching uids and
-        tile size if defined.
-
-        Parameters
-        ----------
-        files: List[WsiDicomFile]
-            Wsi files to filter
-        series_uids: Uids
-            Uids to check against
-        series_tile_size: Size
-            Tile size to check against
-
-        Returns
-        ----------
-        List[WsiDicomFile]
-            List of matching wsi dicom files
-        """
-        valid_files: List[WsiDicomFile] = []
-        for file in files:
-            if file.matches_series(series_uids, series_tile_size):
-                valid_files.append(file)
-            else:
-                file.close()
-        return valid_files
-
     def close(self) -> None:
-        """Close all stacks in the series."""
-        for stack in self.stacks:
-            stack.close()
+        """Close all groups in the series."""
+        for group in self.groups:
+            group.close()
 
 
 class WsiDicomLabels(WsiDicomSeries):
+    """Represents a series of WsiDicomGroups of the label wsi flavor."""
     wsi_type = 'LABEL'
 
     @classmethod
     def open(
         cls,
-        files: List[WsiDicomFile],
-        optical: OpticalManager,
-        base_file: WsiDicomFile
+        instances: List[WsiInstance]
     ) -> 'WsiDicomLabels':
         """Return label series created from wsi files.
 
         Parameters
         ----------
-        files: List[WsiDicomFile]
-            Files to create labels from.
-        optical: OpticalManager
-            Optical manager to add optical paths to.
-        base_file: WsiDicomFile
-            File that defines valid files to add.
+        instances: List[WsiInstance]
+            Instances to create levels from.
 
         Returns
         ----------
         WsiDicomLabels
             Created label series
         """
-        filtered_files = cls._filter_files(
-            files,
-            base_file.uids.base,
-        )
-        labels = WsiDicomStack.open(filtered_files, optical)
-        return cls(labels)
+        labels = WsiDicomGroup.open(instances)
+        return WsiDicomLabels(labels)
 
 
 class WsiDicomOverviews(WsiDicomSeries):
+    """Represents a series of WsiDicomGroups of the overview wsi flavor."""
     wsi_type = 'OVERVIEW'
 
     @classmethod
     def open(
         cls,
-        files: List[WsiDicomFile],
-        optical: OpticalManager,
-        base_file: WsiDicomFile
+        instances: List[WsiInstance]
     ) -> 'WsiDicomOverviews':
         """Return overview series created from wsi files.
 
         Parameters
         ----------
-        files: List[WsiDicomFile]
-            Files to create overviews from.
-        optical: OpticalManager
-            Optical manager to add optical paths to.
-        base_file: WsiDicomFile
-            File that defines valid files to add.
+        instances: List[WsiInstance]
+            Instances to create levels from.
 
         Returns
         ----------
         WsiDicomOverviews
             Created overview series
         """
-        filtered_files = cls._filter_files(
-            files,
-            base_file.uids.base,
-        )
-        overviews = WsiDicomStack.open(filtered_files, optical)
-        return cls(overviews)
+        overviews = WsiDicomGroup.open(instances)
+        return WsiDicomOverviews(overviews)
 
 
 class WsiDicomLevels(WsiDicomSeries):
+    """Represents a series of WsiDicomGroups of the volume (e.g. pyramidal
+    level) wsi flavor."""
     wsi_type = 'VOLUME'
 
     def __init__(self, levels: List[WsiDicomLevel]):
@@ -2844,14 +3013,17 @@ def __init__(self, levels: List[WsiDicomLevel]):
 
         Parameters
         ----------
-        stacks: List[WsiDicomLevel]
-            List of level stacks to include in series
+        levels: List[WsiDicomLevel]
+            List of levels to include in series
         """
         self._levels = OrderedDict(
             (level.level, level)
             for level in sorted(levels, key=lambda level: level.level)
         )
-        self._uids = self._validate_series(self.stacks)
+        if len(self.groups) != 0 and self.groups[0].uids is not None:
+            self._uids = self._validate_series(self.groups)
+        else:
+            self._uids = None
 
     @property
     def pyramid(self) -> str:
@@ -2865,8 +3037,8 @@ def pyramid(self) -> str:
         )
 
     @property
-    def stacks(self) -> List[WsiDicomStack]:
-        """Return contained stacks"""
+    def groups(self) -> List[WsiDicomGroup]:
+        """Return contained groups"""
         return list(self._levels.values())
 
     @property
@@ -2887,33 +3059,22 @@ def base_level(self) -> WsiDicomLevel:
     @classmethod
     def open(
         cls,
-        files: List[WsiDicomFile],
-        optical: OpticalManager,
-        base_file: WsiDicomFile
+        instances: List[WsiInstance]
     ) -> 'WsiDicomLevels':
-        """Return level series created from wsi files.
+        """Return level series created from wsi instances.
 
         Parameters
         ----------
-        files: List[WsiDicomFile]
-            Files to create levels from.
-        optical: OpticalManager
-            Optical manager to add optical paths to.
-        base_file: WsiDicomFile
-            File that defines valid files to add.
+        instances: List[WsiInstance]
+            Instances to create levels from.
 
         Returns
         ----------
         WsiDicomLevels
             Created level series
         """
-        filtered_files = cls._filter_files(
-            files,
-            base_file.uids.base,
-            base_file.tile_size
-        )
-        levels = WsiDicomLevel.open_levels(filtered_files, optical)
-        return cls(levels)
+        levels = WsiDicomLevel.open_levels(instances)
+        return WsiDicomLevels(levels)
 
     def valid_level(self, level: int) -> bool:
         """Check that given level is less or equal to the highest level
@@ -2966,7 +3127,7 @@ def get_closest_by_level(self, level: int) -> WsiDicomLevel:
             The level closest to searched level
         """
         if not self.valid_level(level):
-            raise WsiDicomOutOfBondsError(
+            raise WsiDicomOutOfBoundsError(
                 f"Level {level}", f"maximum level {self.highest_level}"
             )
         closest_level = 0
@@ -2996,7 +3157,7 @@ def get_closest_by_size(self, size: Size) -> WsiDicomLevel:
         WsiDicomLevel
             The level with size closest to searched size
         """
-        closest_size = self.stacks[0].size
+        closest_size = self.groups[0].size
         closest = None
         for wsi_level in self._levels.values():
             if((size.width <= wsi_level.size.width) and
@@ -3040,46 +3201,59 @@ def get_closest_by_pixel_spacing(
 
 
 class WsiDicom:
+    """Represent a wsi slide containing pyramidal levels and optionally
+    labels and/or overviews."""
     def __init__(
         self,
-        series: List[WsiDicomSeries],
-        optical: OpticalManager,
+        levels: WsiDicomLevels,
+        labels: WsiDicomLabels,
+        overviews: WsiDicomOverviews,
         annotations: AnnotationInstance = None
+
     ):
         """Holds wsi dicom levels, labels and overviews.
 
         Parameters
         ----------
-        series: List[WsiDicomSeries]
-            List of series (levels, labels, overviews).
-        optical: OpticalManager
-            Manager for optical paths.
+        levels: WsiDicomLevels
+            Series of pyramidal levels.
+        labels: WsiDicomLabels
+            Series of label images.
+        overviews: WsiDicomOverviews
+            Series of overview images
+        annotations: AnnotationInstance = None
+            Sup-222 annotation instance.
         """
 
-        self._levels: WsiDicomLevels = self._assign(series, 'levels')
-        self._labels: WsiDicomLabels = self._assign(series, 'labels')
-        self._overviews: WsiDicomOverviews = self._assign(series, 'overviews')
-        self.annotations: AnnotationInstance = annotations
+        self._levels = levels
+        self._labels = labels
+        self._overviews = overviews
+        self.annotations = annotations
 
-        self.uids = self._validate_collection(
-            [self.levels, self.labels, self.overviews],
-            optical
+        if self.levels.uids is not None:
+            self.uids = self._validate_collection(
+                [self.levels, self.labels, self.overviews],
+            )
+        else:
+            self.uids = None
+
+        self.optical = OpticalManager.open(
+            levels.instances + labels.instances + overviews.instances
         )
-        self.optical = optical
 
         if (
             self.annotations.frame_of_reference is not None and
             self.annotations.frame_of_reference != self.uids.frame_of_reference
         ):
             warnings.warn("Annotations frame of referance does not match")
-        base = self.levels.base_level.default_instance
-        self.ds = base.create_ds()
+
         self.__enter__()
 
     def __enter__(self):
         return self
 
-    def __exit__(self, type, value, traceback):
+    def __exit__(self, exc_type, exc_val, exc_tb):
+
         self.close()
 
     def __str__(self) -> str:
@@ -3126,7 +3300,16 @@ def files(self) -> List[WsiDicomFile]:
         return self.levels.files + self.labels.files + self.overviews.files
 
     @property
-    def instances(self) -> List[WsiDicomInstance]:
+    def datasets(self) -> List[Dataset]:
+        """Return contained datasets."""
+        return (
+            self.levels.datasets
+            + self.labels.datasets
+            + self.overviews.datasets
+        )
+
+    @property
+    def instances(self) -> List[WsiInstance]:
         """Return contained instances"""
         return (
             self.levels.instances
@@ -3150,7 +3333,7 @@ def pretty_str(
                 return string
         return (
             string + ' of levels:\n'
-            + list_pretty_str(self.levels.stacks, indent, depth, 0, 2)
+            + list_pretty_str(self.levels.groups, indent, depth, 0, 2)
         )
 
     @classmethod
@@ -3189,19 +3372,23 @@ def open(cls, path: Union[str, List[str]]) -> 'WsiDicom':
             elif sop_class_uid == ANN_SOP_CLASS_UID:
                 annotation_files.append(filepath)
 
-        base_file = cls._get_base_file(level_files)
-
-        optical = OpticalManager(base_file.uids.base)
+        base_dataset = cls._get_base_dataset(level_files)
+        base_uids = base_dataset.base_uids
+        base_tile_size = base_dataset.tile_size
+        level_instances = WsiInstance.open(
+            level_files,
+            base_uids,
+            base_tile_size
+        )
+        label_instances = WsiInstance.open(label_files, base_uids)
+        overview_instances = WsiInstance.open(overview_files, base_uids)
 
-        levels = WsiDicomLevels.open(level_files, optical, base_file)
-        labels = WsiDicomLabels.open(label_files, optical, base_file)
-        overviews = WsiDicomOverviews.open(overview_files, optical, base_file)
+        levels = WsiDicomLevels.open(level_instances)
+        labels = WsiDicomLabels.open(label_instances)
+        overviews = WsiDicomOverviews.open(overview_instances)
         annotations = AnnotationInstance.open(annotation_files)
-        return cls(
-            [levels, labels, overviews],
-            optical=optical,
-            annotations=annotations
-        )
+
+        return WsiDicom(levels, labels, overviews, annotations)
 
     @staticmethod
     def _get_sop_class_uid(path: Path) -> Uid:
@@ -3245,28 +3432,28 @@ def _get_filepaths(path: Union[str, List[str]]) -> List[Path]:
         raise WsiDicomNotFoundError("No files found", str(path))
 
     @staticmethod
-    def _get_base_file(
+    def _get_base_dataset(
         files: List[WsiDicomFile]
-    ) -> WsiDicomFile:
+    ) -> WsiDataset:
         """Return file with largest image (width) from list of files.
 
         Parameters
         ----------
         files: List[WsiDicomFile]
-            Wsi files to check.
+           List of files.
 
         Returns
         ----------
-        WsiDicomFile
-            Base layer file.
+        WsiDataset
+            Base layer dataset.
         """
         base_size = Size(0, 0)
-        base_file: WsiDicomFile
+        base_dataset: WsiDataset
         for file in files:
-            if file.image_size.width > base_size.width:
-                base_file = file
-                base_size = file.image_size
-        return base_file
+            if file.dataset.image_size.width > base_size.width:
+                base_dataset = file.dataset
+                base_size = file.dataset.image_size
+        return base_dataset
 
     @staticmethod
     def _filter_paths(filepaths: List[Path]) -> List[Path]:
@@ -3289,28 +3476,24 @@ def _filter_paths(filepaths: List[Path]) -> List[Path]:
 
     def _validate_collection(
         self,
-        series: List[WsiDicomSeries],
-        optical: OpticalManager
+        series: List[WsiDicomSeries]
     ) -> BaseUids:
         """Check that no files or instance in collection is duplicate, that all
-        series and optical manager all have the same base uids.
-        Raises WsiDicomMatchError otherwise.
-        Returns the matching base uid.
+        series have the same base uids. Raises WsiDicomMatchError otherwise.
+        Returns base uid for collection.
 
         Parameters
         ----------
         series: List[WsiDicomSeries]
             List of series to check
-        optical: OpticalManager
-            Optical manager to check
 
         Returns
         ----------
         BaseUids
             Matching uids
         """
-        WsiDicomFile.check_duplicate_file(self.files, self)
-        WsiDicomInstance.check_duplicate_instance(self.instances, self)
+        WsiDataset.check_duplicate_dataset(self.datasets, self)
+        WsiInstance.check_duplicate_instance(self.instances, self)
 
         try:
             base_uids = next(
@@ -3318,44 +3501,11 @@ def _validate_collection(
             )
         except StopIteration:
             raise WsiDicomNotFoundError("Valid series", "in collection")
-        for i, item in enumerate(series):
+        for item in series:
             if item.uids is not None and item.uids != base_uids:
                 raise WsiDicomMatchError(str(item), str(self))
-        if base_uids != optical.uids:
-            raise WsiDicomMatchError(str(optical), str(self))
         return base_uids
 
-    @staticmethod
-    def _assign(series: List[WsiDicomSeries], assign_as: str):
-        """Returns first series in list that matches assign as parameter.
-        Returns None if none found.
-
-        Parameters
-        ----------
-        series: List[WsiDicomSeries]
-            List of series to check
-        assign_as: str
-            Type of series to get
-
-        Returns
-        ----------
-        Optional[
-            WsiDicomLevels,
-            WsiDicomLabels,
-            WsiDicomOverviews
-        ]
-            Series of assign as type
-        """
-        series_types = {
-            'levels': WsiDicomLevels,
-            'labels': WsiDicomLabels,
-            'overviews': WsiDicomOverviews
-        }
-        return next(
-            (item for item in series
-             if type(item) == series_types[assign_as]), None
-        )
-
     def read_label(self, index: int = 0) -> Image:
         """Read label image of the whole slide. If several label
         images are present, index can be used to select a specific image.
@@ -3462,7 +3612,7 @@ def read_region(
         ) * scale_factor
 
         if not wsi_level.valid_pixels(scaled_region):
-            raise WsiDicomOutOfBondsError(
+            raise WsiDicomOutOfBoundsError(
                 f"Region {scaled_region}", f"level size {wsi_level.size}"
             )
         image = wsi_level.get_region(scaled_region, z, path)
@@ -3525,7 +3675,7 @@ def read_region_mpp(
         location: float, float
             Upper left corner of region in mm
         mpp: float
-            Requested pixel spacing (um/mm)
+            Requested pixel spacing (um/pixel)
         size: float
             Size of region in mm
         z: float
@@ -3548,7 +3698,7 @@ def read_region_mpp(
         )
         image = wsi_level.get_region_mm(region, z, path)
         image_size = SizeMm(width=size[0], height=size[1]) // pixel_spacing
-        return image.resize(image_size.to_int_tuple(), resample=Image.BILINEAR)
+        return image.resize(image_size.to_tuple(), resample=Image.BILINEAR)
 
     def read_tile(
         self,
@@ -3633,7 +3783,8 @@ def get_instance(
         level: int,
         z: float = None,
         path: str = None
-    ) -> WsiDicomInstance:
+    ) -> WsiInstance:
+
         """Return instance fullfilling level, z and/or path.
 
         Parameters
@@ -3647,7 +3798,7 @@ def get_instance(
 
         Returns
         ----------
-        WsiDicomInstance
+        WsiInstance:
             Instance
         """
         wsi_level = self.levels.get_level(level)
diff --git a/wsidicom/optical.py b/wsidicom/optical.py
index 858645dc..813e1bb1 100644
--- a/wsidicom/optical.py
+++ b/wsidicom/optical.py
@@ -1,28 +1,24 @@
-from dataclasses import dataclass
 import struct
-import numpy as np
-from typing import Dict, List, Optional
-from PIL import Image, ImageCms
-import io
-
+import warnings
+from abc import ABCMeta, abstractmethod
+from dataclasses import dataclass
+from typing import Dict, List, Optional, Union
 
+import numpy as np
+from PIL import Image
 from pydicom.dataset import Dataset
 from pydicom.sequence import Sequence as DicomSequence
 
+from .conceptcode import (ChannelDescriptionCode, IlluminationCode,
+                          IlluminationColorCode, IlluminatorCode,
+                          ImagePathFilterCode, LenseCode, LightPathFilterCode)
 from .errors import WsiDicomNotFoundError
-from .uid import BaseUids
-
-
-@dataclass
-class IccProfile:
-    name: str
-    description: str
-    profile: bytes
 
 
 class Lut:
-    def __init__(self, size: int, bits: int):
-        """Stores RGB lookup tables.
+    """Represents a LUT."""
+    def __init__(self, lut_sequence: DicomSequence):
+        """Read LUT from a DICOM LUT sequence.
 
         Parameters
         ----------
@@ -31,39 +27,100 @@ def __init__(self, size: int, bits: int):
         bits: int
             the bits for each entry (currently forced to 16)
         """
-        self.bits = bits
+        self._lut_item = lut_sequence[0]
+        length, first, bits = \
+            self._lut_item.RedPaletteColorLookupTableDescriptor
+        self._length = length
         self._type: type
+        self._bits = bits
         if bits == 8:
             self._type = np.uint8
         else:
             self._type = np.uint16
-        self.table = np.zeros((3, size), self._type)
-        self._position = [0, 0, 0]
-        self._last_value = [-1, -1, -1]
         self._byte_format = 'HHH'  # Do we need to set endianess?
+        self.table = self._parse_lut(self._lut_item)
 
-    def get(self) -> np.ndarray:
-        """Return 2D representation of the lookup table.
+    def array(self, mode: str) -> np.ndarray:
+        """Return flattened representation of the lookup table with order
+        suitable for use with Pillows point(). The lookup table is scaled to
+        either 8 or 16 bit depending on mode.
+
+        Parameters
+        ----------
+        mode: str
+            Image mode to produce lookup table for.
 
         Returns
         ----------
         np.ndarray
-            Lookup table ordered by color x entry
+            Lookup table ordered by rgb, rgb ...
         """
-        return self.table
+        if mode == 'L' or mode == 'I':
+            bits = 16
+        else:
+            bits = 8
+        return self.table.flatten()/(2**self._bits/2**bits)
 
-    def get_flat(self) -> np.ndarray:
-        """Return 1D representation of the lookup table.
-        Suitable for use with pillows point function.
+    def insert_into_ds(self, ds: Dataset) -> Dataset:
+        """Codes and insert object into sequence in dataset.
+
+        Parameters
+        ----------
+        ds: Dataset
+           Dataset to insert into.
+
+        Returns
+        ----------
+        Dataset
+            Dataset with object inserted.
+
+        """
+        ds.PaletteColorLookupTableSequence = DicomSequence[self._lut_item]
+
+    @classmethod
+    def from_ds(cls, ds: Dataset) -> Optional['Lut']:
+        if('PaletteColorLookupTableSequence' in ds):
+            return cls(ds.PaletteColorLookupTableSequence)
+        return None
+
+    def get(self) -> np.ndarray:
+        """Return 2D representation of the lookup table.
 
         Returns
         ----------
         np.ndarray
-            Lookup table ordered by rgb, rgb ...
+            Lookup table ordered by color x entry
         """
-        return self.table.flatten()
+        return self.table
+
+    def _parse_color(self, segmented_lut_data: bytes):
+        LENGTH = 6
+        parsed_table = np.ndarray((0, ), dtype=self._type)
+        for segment in range(int(len(segmented_lut_data)/LENGTH)):
+            segment_bytes = segmented_lut_data[
+                segment*LENGTH:segment*LENGTH+LENGTH
+            ]
+            lut_type, lut_length, lut_value = struct.unpack(
+                self._byte_format,
+                segment_bytes
+            )
+            if(lut_type == 0):
+                parsed_table = self._add_discret(
+                    parsed_table,
+                    lut_length,
+                    lut_value
+                )
+            elif(lut_type == 1):
+                parsed_table = self._add_linear(
+                    parsed_table,
+                    lut_length,
+                    lut_value
+                )
+            else:
+                raise NotImplementedError("Unkown lut segment type")
+        return parsed_table
 
-    def parse_lut(self, lut: DicomSequence):
+    def _parse_lut(self, lut: DicomSequence) -> np.ndarray:
         """Parse a dicom Palette Color Lookup Table Sequence item.
 
         Parameters
@@ -76,19 +133,14 @@ def parse_lut(self, lut: DicomSequence):
             lut.SegmentedGreenPaletteColorLookupTableData,
             lut.SegmentedBluePaletteColorLookupTableData,
         ]
+        parsed_tables = np.zeros((len(tables), self._length), dtype=self._type)
+
         for color, table in enumerate(tables):
-            for i in range(int(len(table)/6)):
-                segment_bytes = table[i*6:i*6+6]
-                segment_type, length, value = struct.unpack(
-                    self._byte_format,
-                    segment_bytes
-                )
-                if(segment_type == 0):
-                    self._add_discret(color, length, value)
-                elif(segment_type == 1):
-                    self._add_linear(color, length, value)
+            parsed_tables[color] = self._parse_color(table)
+        return parsed_tables
 
-    def _insert(self, channel: int, segment: np.ndarray):
+    @classmethod
+    def _insert(cls, table: np.ndarray, segment: np.ndarray):
         """Insert a segement into the lookup table of channel.
 
         Parameters
@@ -98,13 +150,11 @@ def _insert(self, channel: int, segment: np.ndarray):
         segment: np.ndarray
             The segment to insert
         """
-        length = len(segment)
-        position = self._position[channel]
-        self.table[channel, position:position+length] = segment
-        self._position[channel] += length
-        self._last_value[channel] = segment[length - 1]
+        table = np.append(table, segment)
+        return table
 
-    def _add_discret(self, channel: int, length: int, value: int):
+    @classmethod
+    def _add_discret(cls, table: np.ndarray, length: int, value: int):
         """Add a discret segement into the lookup table of channel.
 
         Parameters
@@ -112,14 +162,16 @@ def _add_discret(self, channel: int, length: int, value: int):
         channel: int
             The channel (r=0, g=1, b=2) to operate on
         length: int
-            The lenght of the discret segment
+            The length of the discret segment
         value: int
             The value of the deiscret segment
         """
-        segment = np.full(length, value, dtype=self._type)
-        self._insert(channel, segment)
+        segment = np.full(length, value, dtype=table.dtype)
+        table = cls._insert(table, segment)
+        return table
 
-    def _add_linear(self, channel: int, lenght: int, value: int):
+    @classmethod
+    def _add_linear(cls, table: np.ndarray, length: int, value: int):
         """Add a linear segement into the lookup table of channel.
 
         Parameters
@@ -127,7 +179,7 @@ def _add_linear(self, channel: int, lenght: int, value: int):
         channel: int
             The channel (r=0, g=1, b=2) to operate on
         length: int
-            The lenght of the discret segment
+            The length of the discret segment
         value: int
             The value of the deiscret segment
         """
@@ -136,279 +188,488 @@ def _add_linear(self, channel: int, lenght: int, value: int):
         start_position = 1
         # If no last value, set it to 0 and include
         # first value in segment
-        if self._last_value[channel] == -1:
-            self._last_value[channel] = 0
+        try:
+            last_value = table[-1]
+        except IndexError:
+            last_value = 0
             start_position = 0
         segment = np.linspace(
-            start=self._last_value[channel],
+            start=last_value,
             stop=value,
-            num=start_position+lenght,
-            dtype=self._type
+            num=start_position+length,
+            dtype=table.dtype
         )
-        self._insert(channel, segment[start_position:])
+        table = cls._insert(table, segment[start_position:])
+        return table
 
 
 @dataclass
-class OpticalPath:
-    identifier: str
-    description: str
-    icc_profile_name: str
-    lut: Optional[Lut]
+class OpticalFilter(metaclass=ABCMeta):
+    """Metaclass for filter conditions for optical path"""
+    filters: Optional[Union[LightPathFilterCode, ImagePathFilterCode]]
+    nominal: Optional[float]
+    low_pass: Optional[float]
+    high_pass: Optional[float]
 
-    def __str__(self):
-        return self.pretty_str()
-
-    def pretty_str(
-        self,
-        indent: int = 0,
-        depth: int = None
-    ) -> str:
-        if(self.description == ''):
-            return self.identifier
-        return self.identifier + ':' + self.description
+    @classmethod
+    @abstractmethod
+    def from_ds(cls, ds: Dataset):
+        raise NotImplementedError
 
+    def insert_into_ds(self, ds: Dataset) -> Dataset:
+        """Codes and insert object into dataset.
 
-class OpticalManager:
-    def __init__(self, uids: BaseUids):
-        """Store optical paths and icc profiles loaded from dicom files.
         Parameters
         ----------
-        uids: BaseUids
-            Base uids this manager is bound to
+        ds: Dataset
+           Optical path sequence item.
+
+        Returns
+        ----------
+        Dataset
+            Dataset with object inserted.
+
         """
+        if self.nominal is not None:
+            ds.LightPathFilterPassBand = self.nominal
+        if self.low_pass is not None and self.high_pass is not None:
+            ds.LightPathFilterPassThroughwavelength = [
+                self.low_pass,
+                self.high_pass
+            ]
+        if self.filters is not None:
+            for filter in self.filters:
+                ds = filter.insert_into_ds(ds)
+        return ds
 
-        self.uids = uids
-        self._optical_paths: Dict[str, OpticalPath] = {}
-        self._icc_profiles: Dict[str, IccProfile] = {}
 
-    def add(self, sequence: DicomSequence) -> List[str]:
-        """Add optical paths parsed from pydicom optical path sequence.
+@dataclass
+class LightPathFilter(OpticalFilter):
+    """Set of light path filter conditions for optical path"""
+    filters: Optional[List[LightPathFilterCode]]
+
+    @classmethod
+    def from_ds(cls, ds: Dataset) -> 'LightPathFilter':
+        """Returns LightPathFilter object read from dataset
+        (optical path sequence item).
 
         Parameters
         ----------
-        ds: DicomSequence
-            A pydicom Optical Path Sequence
+        ds: Dataset
+           Optical path sequence item.
 
         Returns
         ----------
-        List[str]
-            A list of found identifiers
+        LightPathFilter
+            Object containing light path filter conditions for optical path.
+
         """
-        found_identifiers: List[str] = []
-        for optical_path in sequence:
-            identifier = str(optical_path.OpticalPathIdentifier)
-            found_identifiers.append(identifier)
-            # Is optical path with identifier already known?
-            try:
-                self.get(identifier)
-            except WsiDicomNotFoundError:
-                path = self._new_optical_path(identifier, optical_path)
-                # self._optical_paths.append(path)
-                self._optical_paths[identifier] = path
-        return found_identifiers
+        filter_band = getattr(ds, 'LightPathFilterPassBand', [None, None])
+        return cls(
+            filters=LightPathFilterCode.from_ds(ds),
+            nominal=getattr(ds, 'LightPathFilterPassThroughwavelengthh', None),
+            low_pass=filter_band[0],
+            high_pass=filter_band[1]
+        )
 
-    def get(self, identifier: str) -> OpticalPath:
-        """Return the optical path item with identifier.
+
+@dataclass
+class ImagePathFilter(OpticalFilter):
+    """Set of image path filter conditions for optical path"""
+    filters: Optional[List[ImagePathFilterCode]]
+
+    @classmethod
+    def from_ds(cls, ds: Dataset) -> 'ImagePathFilter':
+        """Returns ImagePathFilter object read from dataset
+        (optical path sequence item).
 
         Parameters
         ----------
-        identifier: str
-            The unique optical identifier to get
+        ds: Dataset
+           Optical path sequence item.
 
         Returns
         ----------
-        OpticalPath
-            The OpticalPath item
+        ImagePathFilter
+            Object containing image path filter conditions for optical path.
+
         """
-        path = self._optical_paths.get(identifier)
-        if path is None:
-            raise WsiDicomNotFoundError(
-                f"identifier {identifier}",
-                "optical path manager"
-            )
-        return path
+        filter_band = getattr(ds, 'ImagePathFilterPassBand', [None, None])
+        return cls(
+            filters=ImagePathFilterCode.from_ds(ds),
+            nominal=getattr(ds, 'ImagePathFilterPassThroughwavelengthh', None),
+            low_pass=filter_band[0],
+            high_pass=filter_band[1]
+        )
+
 
-    def get_lut(self, identifer: str) -> Lut:
-        """Return lookup table for optical path with identifier.
+@dataclass
+class Illumination:
+    """Set of illumination conditions for optical path"""
+    def __init__(
+        self,
+        illumination_method: List[IlluminationCode],
+        illumination_wavelength: float = None,
+        illumination_color: IlluminationColorCode = None,
+        illuminator: IlluminatorCode = None
+    ):
+        if illumination_color is None and illumination_wavelength is None:
+            raise ValueError("Illumination color or wavelenght need to be set")
+        self.illumination_method = illumination_method
+        self.illumination_wavelength = illumination_wavelength
+        self.illumination_color = illumination_color
+        self.illuminator = illuminator
+
+    @classmethod
+    def from_ds(cls, ds: Dataset) -> 'Illumination':
+        """Returns Illuminatin object read from dataset (optical path sequence
+        item).
 
         Parameters
         ----------
-        identifier: str
-            The unique optical identifier to get the lookup table for
+        ds: Dataset
+           Optical path sequence item.
 
         Returns
         ----------
-        Optional[Lut]
-            The Lut for the optical path, or None if not set
+        Illumination
+            Object containing illumination conditions for optical path.
+
         """
-        path = self.get(identifer)
-        if path.lut is None:
-            raise WsiDicomNotFoundError(
-                f"Lut for identifier {identifer}",
-                "optical path manager"
-            )
-        return path.lut
+        return cls(
+            illumination_method=IlluminationCode.from_ds(ds),
+            illumination_wavelength=getattr(
+                ds, 'IlluminationWaveLength', None
+            ),
+            illumination_color=IlluminationColorCode.from_ds(ds),
+            illuminator=IlluminatorCode.from_ds(ds)
+        )
 
-    def apply_lut(self, image: Image, identifier: str) -> Image:
-        """Apply LUT of identifier to image. Converts gray scale image to RGB.
+    def insert_into_ds(self, ds: Dataset) -> Dataset:
+        """Codes and insert object into dataset.
 
         Parameters
         ----------
-        image: Image
-            Pillow image to apply LUT to.
-        identifier: str
-            The identifier of the LUT to apply
+        ds: Dataset
+           Optical path sequence item.
 
         Returns
         ----------
-        Image
-            Image with LUT applied.
+        Dataset
+            Dataset with object inserted.
+
         """
-        if(image.mode == 'L'):
-            image = image.convert('RGB')
-        lut = self.get_lut(identifier)
-        lut_array = lut.get_flat()/(2**lut.bits/256)
-        return image.point(lut_array)
+        if self.illumination_wavelength is not None:
+            ds.Illuminationwavelengthh = self.illumination_wavelength
+        if self.illumination_color is not None:
+            ds = self.illumination_color.insert_into_ds(ds)
+        if self.illuminator is not None:
+            ds = self.illuminator.insert_into_ds(ds)
+        for item in self.illumination_method:
+            ds = item.insert_into_ds(ds)
+        return ds
+
 
-    def get_icc(self, identifer: str) -> bytes:
-        """Return icc profile for optical path with identifier.
+@dataclass
+class Lenses:
+    """Set of lens conditions for optical path"""
+    lenses: Optional[List[LenseCode]]
+    condenser_power: Optional[float]
+    objective_power: Optional[float]
+    objective_na: Optional[float]
+
+    @classmethod
+    def from_ds(cls, ds: Dataset) -> 'Lenses':
+        """Returns Lenses object read from dataset (optical path sequence
+        item).
 
         Parameters
         ----------
-        identifier: str
-            The unique optical identifier to get the lookup table for
+        ds: Dataset
+           Optical path sequence item.
 
         Returns
         ----------
-        bytes
-            The Icc profile in bytes
+        Lenses
+            Object containing lense conditions for optical path.
+
         """
-        path = self.get(identifer)
-        return self._get_icc_bytes(path.icc_profile_name)
+        return cls(
+            lenses=LenseCode.from_ds(ds),
+            condenser_power=getattr(ds, 'CondenserLensPower', None),
+            objective_power=getattr(ds, 'ObjectiveLensPower', None),
+            objective_na=getattr(ds, 'ObjectiveLensNumericalAperture', None)
+        )
 
-    def _get_icc_bytes(self, name: str) -> bytes:
-        """Get Icc profile bytes by icc profile name.
+    def insert_into_ds(self, ds: Dataset) -> Dataset:
+        """Codes and insert object into dataset.
 
         Parameters
         ----------
-        name: str
-            The name of the icc profile
+        ds: Dataset
+           Optical path sequence item.
 
         Returns
         ----------
-        IccProfile
-            The profile with specified name
+        Dataset
+            Dataset with object inserted.
+
         """
-        icc_profile = self._get_icc(name)
-        return icc_profile.profile
+        if self.condenser_power is not None:
+            ds.CondenserLensPower = self.condenser_power
+        if self.objective_power is not None:
+            ds.ObjectiveLensPower = self.objective_power
+        if self.objective_na is not None:
+            ds.ObjectiveLensNumericalAperture = self.objective_na
+        if self.lenses is not None:
+            for lense in self.lenses:
+                ds = lense.insert_into_ds(ds)
+        return ds
 
-    def _get_icc(self, name: str) -> IccProfile:
-        """Get a Icc profile item by icc profile name.
+
+@dataclass
+class OpticalPath:
+    """Represents an optical path"""
+    def __init__(
+        self,
+        identifier: str,
+        illumination: Illumination,
+        photometric_interpretation: str,
+        description: str = None,
+        icc_profile: bytes = None,
+        lut: Lut = None,
+        light_path_filter: LightPathFilter = None,
+        image_path_filter: ImagePathFilter = None,
+        channel_description: List[ChannelDescriptionCode] = None,
+        lenses: Lenses = None
+    ):
+        """Create a OpticalPath from identifier, illumination, photometric
+        interpretation, and optional attributes.
 
         Parameters
         ----------
-        name: str
-            The name of the icc profile
+        identifier: str
+            String identifier for the optical path.
+        illumination: Illumination
+            The illumination condition used in the optical path.
+        photometric_interpretation: str
+            The photometric interpretation of the optical path.
+        description: str = None
+            Optional description of the optical path.
+        icc_profile: bytes = None
+            Optional ICC profile for the optical path.
+        lut: Lut = None
+            Optional Look-up table for the optical path.
+        light_path_filter: LightPathFilter = None
+            Optional light path filter description for the optical path.
+        image_path_filter: ImagePathFilter = None
+            Optional image path filter description for the optical path.
+        channel_description: List[ChannelDescriptionCode] = None
+            Optional channel description for the optical path.
+        lenses: Lenses = None
+            Optional lens description for the optical path.
+        """
+
+        if photometric_interpretation != 'MONOCHROME2' and icc_profile is None:
+            warnings.warn(
+                "Icc profile required if photometric is "
+                f"{photometric_interpretation}"
+            )
+        if lut is not None and icc_profile is None:
+            warnings.warn("Icc profile required if lut is present")
+
+        self.identifier = identifier
+        self.illumination = illumination
+        self.description = description
+        self.icc_profile = icc_profile
+        self.lut = lut
+        self.light_path_filter = light_path_filter
+        self.image_path_filter = image_path_filter
+        self.channel_description = channel_description
+        self.lenses = lenses
+
+    def __str__(self):
+        string = self.identifier
+        if self.description is not None:
+            string += ' - ' + self.description
+        return string
+
+    def to_ds(self) -> Dataset:
+        ds = Dataset()
+        ds.OpticalPathIdentifier = self.identifier
+        ds = self.illumination.insert_into_ds(ds)
+        if self.description is not None:
+            ds.OpticalPathDescription = self.description
+        if self.icc_profile is not None:
+            ds.ICCProfile = self.icc_profile
+        if self.lut is not None:
+            ds = self.lut.insert_into_ds(ds)
+        if self.light_path_filter is not None:
+            ds = self.light_path_filter.insert_into_ds(ds)
+        if self.image_path_filter is not None:
+            ds = self.light_path_filter.insert_into_ds(ds)
+        if self.channel_description is not None:
+            for item in self.channel_description:
+                ds = item.insert_into_ds(ds)
+        if self.lenses is not None:
+            ds = self.lenses.insert_into_ds(ds)
+        return ds
+
+    @classmethod
+    def from_ds(
+        cls,
+        ds: Dataset,
+        photometric_interpretation: str
+    ) -> 'OpticalPath':
+        """Create new optical path item populated with optical path
+        identifier, description, icc profile name and lookup table.
+
+        Parameters
+        ----------
+        optical_path: Dataset
+            Optical path dataset containing the optical path data
+        photometric_interpretation: str
+            Photometric interprentation for parent dataset.
 
         Returns
         ----------
-        IccProfile
-            The profile with specified name
+        OpticalPath
+            New optical path item
         """
-        icc_profile = self._icc_profiles.get(name)
-        if icc_profile is None:
-            raise WsiDicomNotFoundError(
-                f"icc profile {name}", "optical path manager"
-            )
-        return icc_profile
+        return OpticalPath(
+            identifier=str(ds.OpticalPathIdentifier),
+            illumination=Illumination.from_ds(ds),
+            photometric_interpretation=photometric_interpretation,
+            description=getattr(ds, 'OpticalPathDescription', None),
+            icc_profile=getattr(ds, 'ICCProfile', None),
+            lut=Lut.from_ds(ds),
+            light_path_filter=LightPathFilter.from_ds(ds),
+            image_path_filter=ImagePathFilter.from_ds(ds),
+            channel_description=ChannelDescriptionCode.from_ds(ds),
+            lenses=Lenses.from_ds(ds),
+        )
 
-    def _add_icc(self, name: str, profile: IccProfile):
-        """Add Icc profile item to manager
+
+class OpticalManager:
+    """Store optical paths loaded from dicom files."""
+    def __init__(
+        self,
+        optical_paths: List[OpticalPath] = None,
+    ):
+        """Create a OpticalManager from list of OpticalPaths.
 
         Parameters
         ----------
-        name: str
-            Profile name
-        profile: IccProfile
-            The profile to add
-
+        optical_paths: List[OpticalPath] = None
+            List of OpticalPaths.
         """
-        self._icc_profiles[name] = profile
+        self._optical_paths: Dict[str, OpticalPath] = {
+            optical_path.identifier: optical_path
+            for optical_path in optical_paths
+        }
 
-    def _read_icc(self, optical_path_ds: Dataset) -> str:
-        """Read Icc profile item from dataset. If profile is not already saved,
-        saves the profile. Returns the profile name if succesfull.
+    @classmethod
+    def open(cls, instances: List) -> 'OpticalManager':
+        """Parse optical path sequence in listed files and create an
+        OpticalManager out of the found (unique) OpticalPaths.
 
         Parameters
         ----------
-        optical_path_ds: Dataset
-            A dataset containing an icc profile
+        files: List[WsiDicomFile]
+            List of WsiDicom files to parse
 
         Returns
         ----------
-        str
-            The icc profile name
+        OpticalManager
+            OpticalManager for the found OpticalPaths
         """
+        optical_paths: Dict[str, OpticalPath] = {}
+        for instance in instances:
+            for dataset in instance.datasets:
+                optical_path_sequence = dataset.OpticalPathSequence
+                for optical_path in optical_path_sequence:
+                    identifier = str(optical_path.OpticalPathIdentifier)
+                    if identifier not in optical_paths:
+                        path = OpticalPath.from_ds(
+                            optical_path,
+                            dataset.PhotometricInterpretation
+                        )
+                        optical_paths[identifier] = path
+        return OpticalManager(optical_paths.values())
+
+    def insert_into_ds(self, ds: Dataset) -> Dataset:
+        """Codes and insert object into dataset.
+
+        Parameters
+        ----------
+        ds: Dataset
+           DICOM dataset.
+
+        Returns
+        ----------
+        Dataset
+            Dataset with object inserted.
+
+        """
+        ds.NumberOfOpticalPaths = len(self._optical_paths)
+        ds.OpticalPathSequence = DicomSequence([
+            optical_path.to_ds()
+            for optical_path in self._optical_paths.values()
+        ])
+        return ds
+
+    def get(self, identifier: str) -> OpticalPath:
         try:
-            icc_profile: bytes = optical_path_ds.ICCProfile
-            profile = ImageCms.ImageCmsProfile(io.BytesIO(icc_profile))
-            icc_profile_name = str(ImageCms.getProfileName(profile))
-            # Is the ICC name already known?
-            try:
-                self._get_icc(icc_profile_name)
-            except WsiDicomNotFoundError:
-                # Save profile
-                description = ImageCms.getProfileDescription(profile)
-                new_profile = IccProfile(
-                    name=icc_profile_name,
-                    description=description,
-                    profile=icc_profile
-                )
-                self._add_icc(icc_profile_name, new_profile)
-            return icc_profile_name
-        except (ImageCms.PyCMSError, AttributeError):
-            # No profile found or profile not valid
-            return ""
+            return self._optical_paths[identifier]
+        except KeyError:
+            raise WsiDicomNotFoundError(
+                f"identifier {identifier}",
+                "optical path manager"
+            )
 
-    def _new_optical_path(
-        self,
-        identifier: str,
-        optical_path: Dataset
-    ) -> OpticalPath:
-        """Create new optical path item populated with optical path
-        identifier, description, icc profile name and lookup table.
+    def apply_lut(self, image: Image, identifier: str) -> Image:
+        """Apply LUT of identifier to image. Converts gray scale image to RGB.
 
         Parameters
         ----------
+        image: Image
+            Pillow image to apply LUT to.
         identifier: str
-            Identifier of the new optical path
-        optical_path: Dataset
-            Optical path dataset containing the optical path data
+            The identifier of the LUT to apply
 
         Returns
         ----------
-        OpticalPath
-            New optical path item
+        Image
+            Image with LUT applied.
         """
-        # New optical path, try to load ICC, get description and lut
-        icc_profile_name = self._read_icc(optical_path)
-        description = getattr(
-            optical_path,
-            'OpticalPathDescription',
-            ''
-        )
-        lut: Optional[Lut] = None
-        if('PaletteColorLookupTableSequence' in optical_path):
-            sequence = optical_path.PaletteColorLookupTableSequence[0]
-            lenght, first, bits = \
-                sequence.RedPaletteColorLookupTableDescriptor
-            lut = Lut(lenght, bits)
-            lut.parse_lut(sequence)
-        # Create a new OpticalPath and add
-        path = OpticalPath(
-            identifier=identifier,
-            description=description,
-            icc_profile_name=icc_profile_name,
-            lut=lut
-        )
-        return path
+
+        path = self.get(identifier)
+        lut = path.lut
+        if lut is None:
+            raise WsiDicomNotFoundError(
+                f"Lut for identifier {identifier}",
+                "optical path manager"
+            )
+        # if(image.mode == 'L'):
+        #     image = image.convert('RGB')
+        return image.point(lut.array(image.mode))
+
+    @staticmethod
+    def get_path_identifers(optical_path_sequence: DicomSequence) -> List[str]:
+        """Parse optical path sequence and return list of optical path
+        identifiers
+
+        Parameters
+        ----------
+        optical_path_sequence: DicomSequence
+            Optical path sequence.
+
+        Returns
+        ----------
+        List[str]
+            List of optical path identifiers.
+        """
+        return list({
+            str(optical_ds.OpticalPathIdentifier)
+            for optical_ds in optical_path_sequence
+        })
diff --git a/wsidicom/uid.py b/wsidicom/uid.py
index c5d5b246..18f1563f 100644
--- a/wsidicom/uid.py
+++ b/wsidicom/uid.py
@@ -7,13 +7,22 @@
 
 @dataclass
 class BaseUids:
+    """Represents the UIDs that should be common for all files in the wsi."""
     study_instance: Uid
     series_instance: Uid
     frame_of_reference: Uid
 
+    def __str__(self) -> str:
+        return (
+            f"BaseUids study: {self.study_instance}, "
+            f"series: {self.series_instance}, "
+            f"frame of reference {self.frame_of_reference}"
+        )
+
 
 @dataclass
 class FileUids:
+    """Represents the UIDs in a DICOM-file."""
     instance: Uid
     concatenation: Uid
     base: BaseUids


Test patch:
diff --git a/test/data_gen.py b/test/data_gen.py
index 77dd034a..75a6e7e2 100644
--- a/test/data_gen.py
+++ b/test/data_gen.py
@@ -7,17 +7,7 @@
 from pathlib import Path
 
 
-def create_layer_file(filename: Path):
-    # File meta info data elements
-    file_meta = FileMetaDataset()
-    file_meta.FileMetaInformationGroupLength = 204
-    file_meta.FileMetaInformationVersion = b'\x00\x01'
-    file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.77.1.6'
-    file_meta.MediaStorageSOPInstanceUID = '2.25.276496919898213047623062974891537687524'
-    file_meta.TransferSyntaxUID = '1.2.840.10008.1.2.4.50'
-    file_meta.ImplementationClassUID = '1.2.826.0.1.3680043.1.2.100.8.40.110.2'
-    file_meta.ImplementationVersionName = 'DicomObjects.NET'
-
+def create_main_dataset() -> Dataset:
     # Main data elements
     ds = Dataset()
     ds.SpecificCharacterSet = 'ISO_IR 192'
@@ -244,6 +234,27 @@ def create_layer_file(filename: Path):
 
     ds.PixelData = b'\xfe\xff\x00\xe0\x00\x00\x00\x00\xfe\xff\x00\xe0\x08\\\x00\x00\xff\xd8\xff\xfe\x004Intel(R) IPP JPEG encoder [6.1.787] - Jun 10 2009\x00\xff\xdb\x00C\x00\x06\x04\x05\x06\x05\x04\x06\x06\x05\x06\x07\x07\x06\x08\n\x10\n\n\t\t\n\x14\x0e\x0f\x0c\x10\x17\x14\x18\x18\x17\x14\x16\x16\x1a\x1d%\x1f\x1a\x1b#\x1c\x16\x16 , #&\')*)\x19\x1f-0-(0%()(\xff\xdb\x00C\x01\x07\x07\x07\n\x08\n\x13\n\n\x13(\x1a\x16\x1a((((((((((((((((((((((((((((((((((((((((((((((((((\xff\xc0\x00\x11\x08\x04\x00\x04\x00\x03\x00!\x00\x01\x11\x01\x02\x11\x01\xff\xc4\x00\x1f\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\xff\xc4\x00\xb5\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05\x05\x04\x04\x00\x00\x01}\x01\x02\x03\x00\x04\x11\x05\x12!1A\x06\x13Qa\x07"q\x142\x81\x91\xa1\x08#B\xb1\xc1\x15R\xd1\xf0$3br\x82\t\n\x16\x17\x18\x19\x1a%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xff\xc4\x00\x1f\x01\x00\x03\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\xff\xc4\x00\xb5\x11\x00\x02\x01\x02\x04\x04\x03\x04\x07\x05\x04\x04\x00\x01\x02w\x00\x01\x02\x03\x11\x04\x05!1\x06\x12AQ\x07aq\x13"2\x81\x08\x14B\x91\xa1\xb1\xc1\t#3R\xf0\x15br\xd1\n\x16$4\xe1%\xf1\x17\x18\x19\x1a&\'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xff\xda\x00\x0c\x03\x00\x00\x01\x11\x02\x11\x00?\x00\xfa\xa6\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x06\xab\xe5\xd9pF;\x9e\xf4\xe1@\xda\n\r\x02\x10\x1c\xd2\xe6\x80\x0c\xd1@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x08q\xc6GN\x95\xcd\xe9\xba\xa5\xc5\xcf\x88\x1a2\xff\x00\xb9;\x80N\xc3\x15qWL\xe8\xa3\x05(\xc9\xbe\x88\xe91Tu\xc7e\xd3d\xf2\xf2\t\xc0\xce:T\xad\xcc\xe9+\xcd\'\xdc\xc6\xd2/\xa6\x86R$\x94\xbcg\x80\xa7\x9c}+\xa8\xa7-\xcd\xb1t\xd4%u\xd4(\xa99B\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x08/&\x16\xf6\xef!\xe4\xf4Q\xea{W;\xa0\xdb\x18\xb5\xa3\x8e\x9eYj\xb8\xec\xce\xaa:S\x93:\x9a\x8a\xe6?:\xdeH\xfaoR\xb9\xf4\xa89\x93\xb3\xb9\xc3\xd8Y\\\xc33+\xbbt\xc7\xcc{\xd7oi\xb8[D\x1c\xe5\xc2\x00O\xa9\xc5iQ\xa7\xb1\xdd\x8d\x92\x95\xacMEfp\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x18\x1e(\xb9h\xcd\xbcq\xf2s\xb8\xff\x00O\xebRh!d\x9aYG$(\\\xfdO\xff\x00Z\xb4\xfb\'m\xb9p\xf7\xfe\xb76\xa9I\xc0\xc9\xe0Vg\x11\xc5\xdd\xeaQ\x8b\x99\\g\x968\xc0\xae\xbe\x06\r\x120\xe8T\x11W8\xb4v\xe2\xa3h\xc4y4\xea\x83\x88(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x92\x808\xbf\x10;M\xaaJ0H\x04(\xcflV\xf7\x86\x10\xa6\x9b\xb8\xf5g<\xd6\xb2\xf8R=\n\xea\xd4\x12\xf45\xe9\x1b\xee\x9e3\xc5dy\xe7\x9f\xed\x8b\xcc9\xc6Ewv\xb8kh\x8aci@F=1[U\xbe\x97;\xb1n\xe9X\x97\x14\xb5\x89\xc2\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00rn<\xc3$\x8d\x91\x96<\x91\xcekwF\x1bt\xe8W\xd3?\xcc\xd6\x92\xd8\xed\xc4\xbfq"\xf0\xa4\x93\xee78\xe3\xaf\xa5fq#\xceQ\x98;d\xb3\x0c\xf4&\xbb\xed?"\xca\xdf\xfe\xb9\xaf\xf2\xad\xebt;\xf1mr"\xd0\xa2\xb08\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00)\x936\xc8doE&\x81\xad\xce>w2FK\xe4\x9c\xee\x15\xd4\xe9\xa8"\xb1\x81\x7f\xd8\x04\xfdkI\xbd\x0e\xdcR\xe5\x82E\x9a\x8e\xeb\x9biq\xfd\xc3\xfc\xab3\x8a;\x9c\x11Bd\x0b\xb4\xb1\'\x00\x01\xde\xbb\xe8Wd1\xa9\xc6U@5\xadG\xa2;q\xbd\x11%\x15\x91\xc2\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01P_\x9d\xb63\x9fH\xdb\xf9P\x8a\x8f\xc4\x8e1\xc9V\xe0\xfe&\xbbk\x7f\xf5\x11\xe4s\xb4g\xf2\xab\x91\xdf\x8d\xf8b>\xa3\xbb\x04\xda\xcc\x07\'a\xe3\xf0\xa8<\xf8\xee\x8e\x12;\x9d\x92|\x9c2\x9c\x8c\xd7|\xa7 \x1f^kI\xab\x1d\xd8\xe5\xf0\x8e\xa2\xb38\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00*\x96\xb0\xc4i\xb3\x11\xcf\x00\x1f\xa6i\xad\xcb\xa7\xf1\xa3\x97\xb7\xb7{\xa9\xd68\xd4\x92\xc7\x93\xe8+\xb4Q\x8e\x95S\xecuc\'v\x90\xb5\x05\xf4\x8b\x15\xa4\xac\xec\x10m\xc6q\x9e\xb5+s\x92*\xf2I\x1c\\v\xd1\xb4\xea\xa8\xe1\xd4\x9c\x02F\t\xae\xe5F>\x95sw;1\xafUq\xd4Vg\x08Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q\x9a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00*\x96\xb0\xea\x9a|\x81\x8e7\xfc\xa3\xde\x9a\xdc\xbaj\xf3Fw\x868\xfb@l\x126\x80q\x83\x8ekz\x9c\xfe"\xf1\x1f\xc4aY\x1e\'\x97\xcb\xd3\x82g\r#\x85\x14\xa3\xb8\xa8+\xd4\x89\x8f\xe1\xe8\x96]B=\xf9m\x99a\x9fn\x95\xd7\xd5T\xdc\xd7\x16\xef4\x80QPr\x85\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x01\xa4\x14\x00\xb4P\x01E\x00\x14P\x01E\x00\x15\x87\xe2\xad\xdeM\xbe\xdc\x93\xb8\xff\x00*\xa8|F\xb4?\x88\x83C\x0c\x93a\xc9\xf9\x93\x90}kn\x89n:\xfa\xce\xe1\xcdr>\'\xba\xf3oV \xc0\xacc\x07\xd8\xf7\xfe\x94\xe9\xab\xc8\xbc"\xbdK\x97\xbc/\x10i$\x9bn6\xae\xd1\xf8\xff\x00\xfa\xab\xa1\xa5=\xc5\x89\x7f\xbch(\xa99\xc2\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x04*\t\x04\xf5\x1d(\xa0\x05\xa2\x80\n(\x00\xa2\x80\n(\x00\xac\x7f\x12y\x82\x18Lc?>\x0f\x1e\xdf\xfdj\xa8nkG\xe3W249^MZ-\xe5\xd8\xe1\xba\xb7\x03\x8fJ\xeb\xaa\xaani\x8aV\x9aK\xb0\xd9\xa4\x10\xc2\xf2?DR\xc6\xbc\xf1\xb7\xb4\xc6G\xf9\x8b\xb1b}j\xa9uf\xf8(\xee\xce\xcb\xc3\xb1yZj\x920]\x8b\x7fO\xe9Z\x95\x94\xb79+;\xd4`(\xa4d\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01Y~!\x97\xca\xb3\x8c\x9c\x9f\xde\x0e\x07~\rTw5\xa3\xf1\xa3?\xc3\xec\xaf|K(\xdf\xb0\xf3\xf9WID\xf7/\x15\xf1\x99\x1e&\x9bf\x9e#\x04\x83+\x01\xc7\xa0\xe4\xd74\xb1\x17(\xa9\xd4\xf1\x81Z\xc3\xe1:\xb0\xbaS\xb9\xdc\xc3\x18\x8a\x14\x8ctP\x14S\xeb\x03\xcen\xee\xe0(\xa0AE\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x15\x87\xe2\xc7\xdbk\n\xfa\xbe\x7fO\xfe\xbdT>$mC\xf8\x88\xcb\xf0\xdc\xa5u%\xce>pW\xfa\xd7aN\xa7\xc4i\x8bV\x9f\xc8\xc1\xf1#\xaf\x99\x16y\xd9\xc9\x1fZ\xa1\xa0\xaf\xdau\x04#\xa2e\xcf\xe1\xff\x00\xd7\xc5Z\xd2\'E?v\x85\xfc\x8e\xb0\xd0+\x13\xcd\x16\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\xb9\xff\x00\x17m1\xdb\x02\tl\xb6\x07n\xd5P\xf8\x8d\xb0\xff\x00\xc4Fv\x85\x0bI\xa8C\x95`#;\x89\xc6\x05v$\x802N\x05:\x9b\x9a\xe3\x1d\xe6\x91\xe7\xfa\xa5\xd4\xb7z\xab:\x92`l\x9c\x03\xd0\x0e\x01\xae\x9b\xc36\xdeU\xab\xccF\x0b\x9c\x03\xec?\xfa\xf5s\xd26:16\x85.U\xe4mQX\x9e`Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00V\x1f\x8a\xf2\xb6\x90\xc8;>3\xf5\x1f\xfdj\xa8|H\xda\x87\xf1\x11_\xc3\x0ed\xb8m\xcc\x08T\xc8\x19\xc9\xcf\xf8V\xfd\xda<\x96\xb3${w\xb2\x10\xbb\xbag\x1d\xe9\xce\xcaEW\xd2\xa6\xa7;k\xe1\xe9\xe3\xb3X\xa4\x95L\xb2>\xe9\\\x1e\x00\x19\xc0\x03\xf5\xae\x92\x18\xd6(\x924\x18E\x18\x02\x89\xcf\x9b`\xaf[\xdah\x87\xd1Ps\x85\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05Cwn\x97P4R}\xd6\xee;{\xd0\x9d\xb5\x1cd\xe2\xee\x8a\x9a^\x93\x0e\x9c\xce\xc8\xcf$\x8d\xc1f\xf4\xf4\xad\x1ar\x973\xb9U*:\x92\xe6aF)\x10\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x00x\x1d3\xedL\x8aA$j\xe9\xca\xb0\xc8\xcd\x03\xb6\x97\x1d\xcf\xa0\xfc\xe9h\x10Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00TqD\x91)\x11\xa8PNp=h\x1d\xdd\xac?\x9aZ\x04\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14\xd9\x0b\x04;0[\x1cf\x80B+g\xfa\xd3\xc5\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14\x1a\x00\xc3\xf1E\xdc\x96\xd6j\x90\xb9G\x91\xb1\x91\xd7\x15sAi\x1fH\xb6iY\x99\xca\x9c\x969\'\x93\x8a\xab{\xb7:e\x14\xb0\xea]nhV\x0e\xa5\xaaOo~\xc9\x06\xd2\xa80U\xbb\x9a"\x93z\x93\x87\xa4\xaaI\xa6jX\xdd-\xdcA\x80\xc3`n\x15j\x93VfS\x8f,\x9cXQH\x90\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n\x0fNzP\x07+\xad\xcc\xb7\x8a\xd2dmN#\xfe\xa6\xba;$\xf2\xac\xe1\x8c\xf5T\x00\xfeUr\xd1$u\xd7\\\xb4\xe3\x12z\xe4<`\x97\x0b\x7f\x14\x90\xf0\x8d\x1f<ul\x9f\xfe\xb5\x14\xed}E\x83v\xaa_\xf0\xc9\x904\x8a\xc9\xb4l\x04\x9cw\xae\x80\xd2\x96\xe4bm\xed\x1d\x80QR`\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01Y\xfa\xed\xc8\xb7\xd3\xe4\xc1\xc3\xc86/\xf5\xa7\x15viJ<\xd3H\xe7th\xfc\xf9\xa2\x8d\x88o\x9b-\x8eF\x075\xd8\n\xaa\x9b\x9d\x18\xc7\xef$:\xb0\xbcEq\x1a\xcb\x14D\x8d\xc0d\xfe?\xfe\xaaQ\xdc\xcb\x0c\x9b\xa8\xacY\xd0\x19e\xb5w^F\xed\xbf\x90\xff\x00\xeb\xd6\xa5\'\xb95\xff\x00\x88\xc2\x8aFAE\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x15\xca\xf8\xb1\xe4\x96\xe68\xe3\xe5P`\xf1\xdc\xf3\xfe\x15t\xfe#\xa7\x08\xbfyrO\x08\xdb\x11-\xc4\xae>\xe8\n\x0f\xf3\xfe\x95\xd2\xd1Q\xdeB\xc5;\xd4a\\f\xb8\xa1\xf5i\xd8\x9cr\x01\xc9\xf6\x02\x9d=\xcb\xc1\xfcz\x9b\xfe\x1a\x83\xc8\xd3\x07\x18\xde\xe5\x8f\xf2\xfe\x95\xab\x8a\x99;\xb3\n\xce\xf5\x1b\n*L\xc2\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00+\x97\xbbe3O,\xc4\x01\xbf\x00\x0es\xfeE\\7:\xb0\xbb\xbb\x1a\x9e\x1fP\xb6$\xaf!\x9c\x90}{V\xa6je\xb9\x8do\x8d\x89^\x7f\xa8N\xe7T\xb8s\xb4\x91!\x19\xc6{\xf6\xfd+Z]M\xf0i9;\x9dw\x87A\x1aL\'\x07\xe6,\x7fZ\xd3\xcdd\xf70\xad\xfcIz\x85\x14\x8c\xc2\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00\x8a\xf2O*\xd6i\x07UBG\xe5\\K6\xf0T\xf5\xeb\xc5\\t\xd4\xf40Q\xbagga\x00\xb7\xb3\x861\xfc*3\xf5\xefV*\x19\xc3\'v\xd8\xc9\x9bd.\xc3\xf8T\x9a\xf3\xc6\xe5\xf9\xc0\'\xaekj]N\xdc\x12Vl\xef\xecc\xf2\xac\xa0B0U\x00#\xdf\x15=b\xce);\xb6\xc2\x8a\t\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x02\x86\xb9\'\x97\xa6\xc9\xea\xd8_\xf1\xfd3\\\x84Jf\xbaDRAf\n?\x13ZG\xe1=L\x0e\x90rg{KY\x9eYSV`\x9am\xcb\x1c\xe3a\x1c{\xd7\x1d\xa6\xba}\xb6\x02\x17p\xf3\x17\xaf\xd4V\x90\xbd\x8fG\x08\xbfw&wt\xb5\x99\xe7\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x18\x9e\'$E\x06\x0f\x04\x91\x8f\xeb\xfe}j\xb6\x85\xa6\xbbN\xb73p\x8a~A\x8e\xa7\xd6\xb4\xbd\xa2wB\xa2\x85\x0fS\xa3\xe9Efp\x99\xfa\xe3\x81`\xf1\x19R6\x90`\x16=\xbb\xd7?\xa3\xdaf\xfe\x1e\x98V\xdd\xc7|sZE\xda,\xf4(7\x1a-\x9d~\xeaufy\xe1E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x077\xae\\\x0b\xab\xa6\x80\x05\xc4Y\x1c\xf7=\xebcJ]\xbau\xb8\x1f\xdc\x02\xb4\x96\x91H\xea\xaa\xb9iE\x17(\xac\xceS\x93\xf1=\xc8\x96\xfcD\x98>R\xfc\xdfSV\xfc/\x07\xcb%\xc3rO\xc8\xbe\xde\xb5\xae\xd0=\x19\xfb\x98t\x8d\xfe\xb4\xea\xc8\xf3\x82\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x0eM\xfc\x93y<\xd2L\xbbK6\x07\\\xf3[\xfa;#\xe9\xf1\x18\xf1\xb7\x07\xa7\xd4\xd6\xb3\xbd\x8e\xca\xf7\xe4E\xc3\x9e\xd5\x05\xfd\xca\xdaZ\xc93\x906\x8e3\xdc\xf6\xac\xd6\xac\xe5\x8cy\xa4\x91\xc1\xad\xc3Kp\xed(\xdc\\\xe4\x9c\xf5\xcdw:U\xbf\xd9l"\x88\xfd\xecd\xfdMiSk\x1d\xf8\xc7h\xa8\x96\x8d-dy\xc1E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x01E\x00\x14P\x07\rwlC\x96\x8c\x86\x19\'\x18\xf7\xae\xb3J\x8d\xa2\xd3\xe0I8`\xb9#\x18\xc6y\xad\xaaJ\xe9\x1d\xd8\xaf\x81\x173\\\x9f\x8b\xee\r\xc4\xf1\xdbF\xdf,_;\x80{\xff\x00\x9f\xe7QO\xe22\xc2G\x9a\xaao\xa1W@\x87\xcf\xbe\x856|\xaaw\xb1\xc7a]\xbd:\x8fR\xf1\xaf\xdfH(\xac\xce0\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\nd\xef\xe5\xc3#\xff\x00uI\xfd(\x1aWv8h\xe5c"\x8c\xfc\xc4\xed\x1c\xfe\x95\xdd\x81\x81Z\xd5\xe8vb\xfaX\x8e\xe2Q\x05\xbc\x92\xbfDRMp\x8d\x99$\x92I\x1b\xe7rN~\xb4S\xd9\xb2\xb0Kvt\xde\x17\xb5\xf2\xad\x1av\x07t\x87\x03>\x83\xff\x00\xaf["\xa2[\x9c\xd5\xe5\xcdQ\xb1h\xa91\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xaa\x1a\xec\xbeV\x971\xe7,\x02\x8f\xc4\xd3\x8e\xe5\xd2W\x9aG\x1f\x0f\xca\xc2N\xe3\xd3\xadw\xcar\xa0\xfa\x8c\xd6\x95N\xccj\xf8L\xddvP\x96\x853\xcb\xff\x00!\\\xaa)\xb9\xb9\x8e8\xf8b\xd8\x1f\xe3D6\xb9\xa6\x13\xdd\xa7\xcc\xce\xe6\x04X\xa1H\xd3\xee\xa8\xda)\xe2\xb2<\xd6\xee\xee-\x14\x08(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\xb1\xfcM#.\x9e\x15[hg\x01\x8f\xb5TwF\xb4\x7f\x88\x8cM.\xcf\xed\x17Q\xe1\xd1\xa3\xdd\xf3.2k\xb3\xaa\xa8\xf56\xc5\xca\xf2H\xe0u\xeb\x995-K\xf791)\xf5\xe3h\xef\xfe}kg\xc2\xf6cs\xdc\xb0\xe0|\xa9\x9f^\xf5R|\xb1\xb1\xd5Z\xd4\xe8[\xad\x8e\x94t\xa2\xb1<\xa0\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xa2\x80\n(\x00\xac\x9f\x13B\xd2\xe9\x84\xa7\xf00s\xf4\xe7\xfcj\xa3\xba4\xa2\xedQ\\\xcc\xf0\x99\x12\xddH\xc03\x04\x8fh~\xc3$q];\x82\xf1\xb2\xab\x15$`0\xed\xefN\xa7\xc4k\x89\xb2\xa8aA\xe1\xd8\xe1\xb60\x89\x99\xdaG\xdd,\xac\xbf3c\xa0\xad\xb8aH!H\xa3\x18E\x18\x14J|\xc4U\xac\xea+\x0f\xc5-A\x88Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Q@\x05\x14\x00Ph\x01\x15B\xae\x14\x00=\x00\xa2\x80\x16\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x02\x8a\x00(\xa0\x0f\xff\xd9\x00'
 
+    return ds
+
+
+def create_meta_dataset() -> FileMetaDataset:
+    # File meta info data elements
+    file_meta = FileMetaDataset()
+    file_meta.FileMetaInformationGroupLength = 204
+    file_meta.FileMetaInformationVersion = b'\x00\x01'
+    file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.77.1.6'
+    file_meta.MediaStorageSOPInstanceUID = '2.25.276496919898213047623062974891537687524'
+    file_meta.TransferSyntaxUID = '1.2.840.10008.1.2.4.50'
+    file_meta.ImplementationClassUID = '1.2.826.0.1.3680043.1.2.100.8.40.110.2'
+    file_meta.ImplementationVersionName = 'DicomObjects.NET'
+
+    return file_meta
+
+
+def create_layer_file(filename: Path):
+    file_meta = create_meta_dataset()
+    ds = create_main_dataset()
+
     ds.file_meta = file_meta
     ds.is_implicit_VR = False
     ds.is_little_endian = True
diff --git a/test/test_dcmfiles.py b/test/test_dcmfiles.py
index 64c699e7..f8953e3e 100644
--- a/test/test_dcmfiles.py
+++ b/test/test_dcmfiles.py
@@ -21,10 +21,10 @@ class WsiFolder(TypedDict):
 
 
 @pytest.mark.integration
-class WsiDicomTests(unittest.TestCase):
+class WsiDicomFilesTests(unittest.TestCase):
 
     def __init__(self, *args, **kwargs):
-        super(WsiDicomTests, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         self.test_folders: Dict[Path, WsiDicom]
 
     @classmethod
diff --git a/test/test_interface.py b/test/test_interface.py
index 6b4346f5..56c6f6eb 100644
--- a/test/test_interface.py
+++ b/test/test_interface.py
@@ -5,19 +5,23 @@
 import numpy as np
 import pydicom
 import pytest
+from pydicom.dataset import Dataset
+from pydicom.sequence import Sequence as DicomSequence
+from wsidicom.conceptcode import (CidConceptCode, IlluminationCode,
+                                  IlluminationColorCode)
 from wsidicom.errors import WsiDicomNotFoundError
-from wsidicom.interface import (Point, PointMm, Region, RegionMm, Size, SizeMm,
-                                WsiDicom)
-from wsidicom.optical import Lut
+from wsidicom.interface import (DicomImageData, Point, PointMm, Region,
+                                RegionMm, Size, SizeMm, WsiDicom)
+from wsidicom.optical import Illumination, Lut, OpticalManager, OpticalPath
 
-from .data_gen import create_layer_file
+from .data_gen import create_layer_file, create_main_dataset
 
 
 @pytest.mark.unittest
-class WsiDicomTests(unittest.TestCase):
+class WsiDicomInterfaceTests(unittest.TestCase):
 
     def __init__(self, *args, **kwargs):
-        super(WsiDicomTests, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         self.tempdir: TemporaryDirectory
         self.slide: WsiDicom
 
@@ -67,47 +71,51 @@ def test_calculate_scale(self):
     def test_get_frame_number(self):
         base_level = self.slide.levels.get_level(0)
         instance = base_level.get_instance()
-        number = instance.tiles.get_frame_index(Point(0, 0), 0, '0')
+        image_data: DicomImageData = instance._image_data
+        number = image_data.tiles.get_frame_index(Point(0, 0), 0, '0')
         self.assertEqual(number, 0)
 
     def test_get_blank_color(self):
         base_level = self.slide.levels.get_level(0)
         instance = base_level.get_instance()
         color = instance._get_blank_color(
-            instance._photometric_interpretation)
+            instance.photometric_interpretation)
         self.assertEqual(color, (255, 255, 255))
 
     def test_get_frame_file(self):
         base_level = self.slide.levels.get_level(0)
         instance = base_level.get_instance()
-        file = instance._get_file(0)
-        self.assertEqual(file, (instance._files[0]))
+        image_data: DicomImageData = instance._image_data
+        file = image_data._get_file(0)
+        self.assertEqual(file, (image_data._files[0]))
 
         self.assertRaises(
             WsiDicomNotFoundError,
-            instance._get_file,
+            image_data._get_file,
             10
         )
 
     def test_valid_tiles(self):
         base_level = self.slide.levels.get_level(0)
         instance = base_level.get_instance()
-        test = instance.tiles.valid_tiles(
+        image_data: DicomImageData = instance._image_data
+        test = image_data.valid_tiles(
+
             Region(Point(0, 0), Size(0, 0)), 0, '0'
         )
         self.assertTrue(test)
 
-        test = instance.tiles.valid_tiles(
+        test = image_data.valid_tiles(
             Region(Point(0, 0), Size(0, 2)), 0, '0'
         )
         self.assertFalse(test)
 
-        test = instance.tiles.valid_tiles(
+        test = image_data.valid_tiles(
             Region(Point(0, 0), Size(0, 0)), 1, '0'
         )
         self.assertFalse(test)
 
-        test = instance.tiles.valid_tiles(
+        test = image_data.valid_tiles(
             Region(Point(0, 0), Size(0, 0)), 0, '1'
         )
         self.assertFalse(test)
@@ -155,7 +163,7 @@ def test_get_tiles(self):
             position=Point(0, 0),
             size=Size(100, 100)
         )
-        get_tiles = instance.tiles.get_range(region, 0, '0')
+        get_tiles = instance.get_tile_range(region, 0, '0')
         expected = Region(Point(0, 0), Size(0, 0))
         self.assertEqual(get_tiles, expected)
 
@@ -163,7 +171,7 @@ def test_get_tiles(self):
             position=Point(0, 0),
             size=Size(1024, 1024)
         )
-        get_tiles = instance.tiles.get_range(region, 0, '0')
+        get_tiles = instance.get_tile_range(region, 0, '0')
         expected = Region(Point(0, 0), Size(0, 0))
         self.assertEqual(get_tiles, expected)
 
@@ -171,7 +179,7 @@ def test_get_tiles(self):
             position=Point(300, 400),
             size=Size(500, 500)
         )
-        get_tiles = instance.tiles.get_range(region, 0, '0')
+        get_tiles = instance.get_tile_range(region, 0, '0')
         expected = Region(Point(0, 0), Size(0, 0))
         self.assertEqual(get_tiles, expected)
 
@@ -179,7 +187,6 @@ def test_crop_region_to_level_size(self):
         base_level = self.slide.levels.get_level(0)
         instance = base_level.get_instance()
         image_size = base_level.size
-        tile_size = instance.tile_size
         region = Region(
             position=Point(0, 0),
             size=Size(100, 100)
@@ -329,8 +336,8 @@ def test_get_instance(self):
         self.assertEqual(instance, wsi_level.default_instance)
 
     def test_parse_lut(self):
-        lut = Lut(256, 8)
         ds = pydicom.dataset.Dataset()
+        ds.RedPaletteColorLookupTableDescriptor = [256, 0, 8]
         ds.SegmentedRedPaletteColorLookupTableData = (
             b'\x00\x00\x01\x00\x00\x00\x01\x00\xff\x00\x00\x00'
         )
@@ -340,13 +347,15 @@ def test_parse_lut(self):
         ds.SegmentedBluePaletteColorLookupTableData = (
             b'\x00\x00\x01\x00\x00\x00\x01\x00\xff\x00\xff\x00'
         )
-        lut.parse_lut(ds)
+        lut = Lut(DicomSequence([ds]))
         test = np.zeros((3, 256), dtype=np.uint16)
         test[2, :] = np.linspace(0, 255, 256, dtype=np.uint16)
+        print(lut.get())
+        print(test)
         self.assertTrue(np.array_equal(lut.get(), test))
 
-        lut = Lut(256, 16)
         ds = pydicom.dataset.Dataset()
+        ds.RedPaletteColorLookupTableDescriptor = [256, 0, 16]
         ds.SegmentedRedPaletteColorLookupTableData = (
             b'\x01\x00\x00\x01\xff\xff'
         )
@@ -356,7 +365,43 @@ def test_parse_lut(self):
         ds.SegmentedBluePaletteColorLookupTableData = (
             b'\x01\x00\x00\x01\x00\x00'
         )
-        lut.parse_lut(ds)
+        lut = Lut(DicomSequence([ds]))
         test = np.zeros((3, 256), dtype=np.uint16)
         test[0, :] = np.linspace(0, 65535, 256, dtype=np.uint16)
         self.assertTrue(np.array_equal(lut.get(), test))
+
+    def test_recreate_optical_module(self):
+        ds = create_main_dataset()
+        original_optical = Dataset()
+        original_optical.OpticalPathSequence = ds.OpticalPathSequence
+        original_optical.NumberOfOpticalPaths = ds.NumberOfOpticalPaths
+        restored_optical_ds = self.slide.optical.insert_into_ds(Dataset())
+        self.assertEqual(original_optical, restored_optical_ds)
+
+    def test_make_optical(self):
+        illumination_method = IlluminationCode('Transmission illumination')
+        illumination_color = IlluminationColorCode('Full Spectrum')
+        illumination = Illumination(
+            illumination_method=[illumination_method],
+            illumination_color=illumination_color
+        )
+        path = OpticalPath(
+            identifier='1',
+            illumination=illumination,
+            photometric_interpretation='YBR_FULL_422',
+            icc_profile=bytes(0)
+        )
+        optical = OpticalManager([path])
+
+        self.assertEqual(optical.get('1'), path)
+
+    def test_concept_codes(self):
+        for code_class in CidConceptCode.__subclasses__():
+            print(code_class)
+            available_codes = code_class.cid.values()
+            for available_code in available_codes:
+                code = code_class(available_code.meaning)
+                self.assertEqual(
+                    available_code,
+                    code
+                )