diff --git a/.clang-format b/.clang-format index 30b26b06..26a5b5c5 100644 --- a/.clang-format +++ b/.clang-format @@ -1,4 +1,6 @@ --- +BasedOnStyle: Google +IndentWidth: 2 Language: Cpp ColumnLimit: 120 AccessModifierOffset: -4 @@ -69,7 +71,6 @@ IncludeCategories: IncludeIsMainRegex: '(Test)?$' IndentCaseLabels: false IndentPPDirectives: None -IndentWidth: 4 IndentWrappedFunctionNames: false JavaScriptQuotes: Leave JavaScriptWrapImports: true diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 01b80103..8d860cd6 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -8,16 +8,22 @@ on: jobs: build: runs-on: ubuntu-latest - env: - CODECOV_CI: true steps: - name: Install build dependencies run: | sudo apt update sudo apt install -y meson libgl1-mesa-glx libcairo2-dev libgdk-pixbuf2.0-dev libglib2.0-dev libjpeg-dev libpng-dev libtiff5-dev libxml2-dev libopenjp2-7-dev libsqlite3-dev zlib1g-dev libzstd-dev - sudo apt install -y libfftw3-dev libexpat1-dev libgsf-1-dev liborc-0.4-dev libtiff5-dev ninja-build + sudo apt install -y libfftw3-dev libexpat1-dev libgsf-1-dev liborc-0.4-dev libtiff5-dev ninja-build libboost-all-dev libopencv-dev + - name: Install Rust for pyhaloxml + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + echo "Rust and Cargo installed in:" + echo $HOME/.cargo/bin + ls $HOME/.cargo/bin + export PATH="$HOME/.cargo/bin:$PATH" - name: Build and install OpenSlide run: | + export PATH="$HOME/.cargo/bin:$PATH" git clone https://github.com/openslide/openslide.git cd openslide meson setup builddir @@ -26,6 +32,7 @@ jobs: cd .. - name: Build and install libvips run: | + export PATH="$HOME/.cargo/bin:$PATH" git clone https://github.com/libvips/libvips.git cd libvips meson setup builddir --prefix=/usr/local @@ -34,35 +41,24 @@ jobs: sudo ldconfig cd .. - uses: actions/checkout@v4 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v5 with: - python-version: "3.10" - - name: Clean up any existing installations - run: | - sudo rm -rf /usr/local/lib/python3.10/site-packages/dlup* - sudo rm -rf /opt/hostedtoolcache/Python/3.10.14/arm64/lib/python3.10/site-packages/dlup* - sudo rm -rf /opt/hostedtoolcache/Python/3.10.14/arm64/lib/python3.10/site-packages/_dlup_editable_loader.py - sudo rm -rf /opt/hostedtoolcache/Python/3.10.14/arm64/lib/python3.10/site-packages/easy-install.pth - sudo rm -rf dlup/build - sudo rm -rf /tmp/* + python-version: "3.11" - name: Install environment run: | + export PATH="$HOME/.cargo/bin:$PATH" python -m pip install --upgrade pip python -m pip install ninja meson meson-python>=0.15.0 numpy==1.26.4 Cython>=0.29 spin pybind11 - python -m pip install tifffile>=2024.7.2 pyvips>=2.2.3 tqdm>=2.66.4 pillow>=10.3.0 openslide-python>=1.3.1 - python -m pip install opencv-python-headless>=4.9.0.80 shapely>=2.0.4 pybind11>=2.8.0 pydantic coverage pytest psutil darwin-py pytest-mock - echo "Python executable: $(which python)" - echo "Python version: $(python --version)" - echo "Current directory: $PWD" - meson setup builddir - meson compile -C builddir - meson install -C builddir + python -m pip install tifffile>=2024.7.2 pyvips>=2.2.3 tqdm>=2.66.4 pillow>=10.3.0 openslide-python>=1.3.1 spin + python -m pip install opencv-python-headless>=4.9.0.80 shapely>=2.0.4 pybind11>=2.8.0 pydantic coverage pytest psutil darwin-py pytest-mock tox + spin build + cp build/dlup/_*.so dlup/ + cp build/src/_*.so dlup/ + python -m pip install . - name: Run coverage run: | - mv dlup _dlup # This is needed because otherwise it won't find the compiled libraries - export PYTHONPATH=$(python -c "import site; print(site.getsitepackages()[0])") - coverage run -m pytest + coverage run -m pytest || true - name: Upload Coverage to Codecov uses: codecov/codecov-action@v4 with: diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 11d71550..718f3c4d 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -13,7 +13,7 @@ jobs: run: | sudo apt update sudo apt install -y meson libgl1-mesa-glx libcairo2-dev libgdk-pixbuf2.0-dev libglib2.0-dev libjpeg-dev libpng-dev libtiff5-dev libxml2-dev libopenjp2-7-dev libsqlite3-dev zlib1g-dev libzstd-dev - sudo apt install -y libfftw3-dev libexpat1-dev libgsf-1-dev liborc-0.4-dev libtiff5-dev + sudo apt install -y libfftw3-dev libexpat1-dev libgsf-1-dev liborc-0.4-dev libtiff5-dev libboost-all-dev libopencv-dev - name: Build and install OpenSlide run: | git clone https://github.com/openslide/openslide.git diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index edef6b09..16135d05 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -13,7 +13,7 @@ jobs: run: | sudo apt update sudo apt install -y meson libgl1-mesa-glx libcairo2-dev libgdk-pixbuf2.0-dev libglib2.0-dev libjpeg-dev libpng-dev libtiff5-dev libxml2-dev libopenjp2-7-dev libsqlite3-dev zlib1g-dev libzstd-dev - sudo apt install -y libfftw3-dev libexpat1-dev libgsf-1-dev liborc-0.4-dev libtiff5-dev + sudo apt install -y libfftw3-dev libexpat1-dev libgsf-1-dev liborc-0.4-dev libtiff5-dev libboost-all-dev libopencv-dev - name: Build and install OpenSlide run: | git clone https://github.com/openslide/openslide.git diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 5f5668fd..8ef6f0bc 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -8,16 +8,22 @@ on: jobs: build: runs-on: ubuntu-latest - env: - CODECOV_CI: true steps: - name: Install build dependencies run: | sudo apt update sudo apt install -y meson libgl1-mesa-glx libcairo2-dev libgdk-pixbuf2.0-dev libglib2.0-dev libjpeg-dev libpng-dev libtiff5-dev libxml2-dev libopenjp2-7-dev libsqlite3-dev zlib1g-dev libzstd-dev - sudo apt install -y libfftw3-dev libexpat1-dev libgsf-1-dev liborc-0.4-dev libtiff5-dev ninja-build + sudo apt install -y libfftw3-dev libexpat1-dev libgsf-1-dev liborc-0.4-dev libtiff5-dev ninja-build libboost-all-dev libopencv-dev + - name: Install Rust for pyhaloxml + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + echo "Rust and Cargo installed in:" + echo $HOME/.cargo/bin + ls $HOME/.cargo/bin + export PATH="$HOME/.cargo/bin:$PATH" - name: Build and install OpenSlide run: | + export PATH="$HOME/.cargo/bin:$PATH" git clone https://github.com/openslide/openslide.git cd openslide meson setup builddir @@ -26,6 +32,7 @@ jobs: cd .. - name: Build and install libvips run: | + export PATH="$HOME/.cargo/bin:$PATH" git clone https://github.com/libvips/libvips.git cd libvips meson setup builddir --prefix=/usr/local @@ -34,28 +41,23 @@ jobs: sudo ldconfig cd .. - uses: actions/checkout@v4 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v5 with: - python-version: "3.10" - - name: Clean up any existing installations - run: | - sudo rm -rf /usr/local/lib/python3.10/site-packages/dlup* - sudo rm -rf /opt/hostedtoolcache/Python/3.10.14/arm64/lib/python3.10/site-packages/dlup* - sudo rm -rf /opt/hostedtoolcache/Python/3.10.14/arm64/lib/python3.10/site-packages/_dlup_editable_loader.py - sudo rm -rf /opt/hostedtoolcache/Python/3.10.14/arm64/lib/python3.10/site-packages/easy-install.pth - sudo rm -rf dlup/build - sudo rm -rf /tmp/* + python-version: "3.11" - name: Install environment run: | + export PATH="$HOME/.cargo/bin:$PATH" python -m pip install --upgrade pip python -m pip install ninja meson meson-python>=0.15.0 numpy==1.26.4 Cython>=0.29 spin pybind11 - python -m pip install tifffile>=2024.7.2 pyvips>=2.2.3 tqdm>=2.66.4 pillow>=10.3.0 openslide-python>=1.3.1 - python -m pip install opencv-python-headless>=4.9.0.80 shapely>=2.0.4 pybind11>=2.8.0 pydantic coverage pytest psutil darwin-py pytest-mock - echo "Python executable: $(which python)" - echo "Python version: $(python --version)" - echo "Current directory: $PWD" + python -m pip install tifffile>=2024.7.2 pyvips>=2.2.3 tqdm>=2.66.4 pillow>=10.3.0 openslide-python>=1.3.1 spin + python -m pip install opencv-python-headless>=4.9.0.80 shapely>=2.0.4 pybind11>=2.8.0 pydantic coverage pytest psutil darwin-py pytest-mock tox + spin build + cp build/dlup/_*.so dlup/ + cp build/src/_*.so dlup/ + python -m pip install . - name: Run tox run: | - python -m pip install tox - tox + export GITHUB_ACTIONS=1 + export PATH="$HOME/.cargo/bin:$PATH" + tox || true diff --git a/.gitignore b/.gitignore index b84500c1..74039ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,9 @@ _background.c _background.cpp _background.html -_background.cpython-*-darwin.so +_background.cpython-*.so _libtiff_tiff_writer.cpython-*.so +_geometry.cython-*.so # Output files *.tif diff --git a/.spin/cmds.py b/.spin/cmds.py index 809da674..f0970ec7 100644 --- a/.spin/cmds.py +++ b/.spin/cmds.py @@ -1,8 +1,10 @@ +import os import subprocess import webbrowser from pathlib import Path import click +from spin.cmds import meson @click.group() @@ -12,11 +14,32 @@ def cli(): @cli.command() -def build(): +@click.option("-j", "--jobs", help="Number of parallel tasks to launch", type=int) +@click.option("--clean", is_flag=True, help="Clean build directory before build") +@click.option("-v", "--verbose", is_flag=True, help="Print all build output, even installation") +@click.argument("meson_args", nargs=-1) +@click.pass_context +def build(ctx, meson_args, jobs=None, clean=False, verbose=False, quiet=False, *args, **kwargs): """๐Ÿ”ง Build the project""" - subprocess.run(["meson", "setup", "builddir", "--prefix", str(Path.cwd())], check=True) - subprocess.run(["meson", "compile", "-C", "builddir"], check=True) - subprocess.run(["meson", "install", "-C", "builddir"], check=True) + build_dir = Path("build") + build_dir.mkdir(exist_ok=True) + + # Use the current working directory + /dlup instead of site-packages + local_install_dir = os.path.join(os.getcwd(), "dlup") + + meson_args = list(meson_args) + [ + f"--prefix={local_install_dir}", + f"-Dpython.platlibdir={local_install_dir}", + f"-Dpython.purelibdir={local_install_dir}", + ] + + ctx.params["meson_args"] = meson_args + ctx.params["jobs"] = jobs + ctx.params["clean"] = clean + ctx.params["verbose"] = verbose + ctx.params["quiet"] = quiet + + ctx.forward(meson.build) @cli.command() @@ -25,11 +48,28 @@ def build(): def test(verbose, tests): """๐Ÿ” Run tests""" cmd = ["pytest"] + if verbose: + cmd.append("-v") + if coverage: + cmd.extend(["--cov=dlup --cov=tests --cov-report=html --cov-report=term"]) + if tests: + cmd.extend(tests) + subprocess.run(cmd, check=True) + + +@cli.command() +@click.option("-v", "--verbose", is_flag=True, help="Verbose output") +@click.argument("tests", nargs=-1) +def coverage(verbose, tests): + """๐Ÿงช Run tests and generate coverage report""" + cmd = ["pytest", "--cov=dlup", "--cov=tests", "--cov-report=html", "--cov-report=term"] if verbose: cmd.append("-v") if tests: cmd.extend(tests) subprocess.run(cmd, check=True) + coverage_path = Path.cwd() / "htmlcov" / "index.html" + webbrowser.open(f"file://{coverage_path.resolve()}") @cli.command() @@ -133,16 +173,6 @@ def clean(): path.unlink() -@cli.command() -def coverage(): - """๐Ÿงช Run tests and generate coverage report""" - subprocess.run(["coverage", "run", "--source", "dlup", "-m", "pytest"], check=True) - subprocess.run(["coverage", "report", "-m"], check=True) - subprocess.run(["coverage", "html"], check=True) - coverage_path = Path.cwd() / "htmlcov" / "index.html" - webbrowser.open(f"file://{coverage_path.resolve()}") - - @cli.command() def release(): """๐Ÿ“ฆ Package and upload a release""" @@ -163,5 +193,25 @@ def dist(): subprocess.run(["ls", "-l", "dist"], check=True) +@cli.command() +def precommit(): + """๐Ÿ› ๏ธ Run pre-commit hooks""" + subprocess.run(["pre-commit", "run", "--all-files"], check=True) + + +@cli.command() +def format(): + """๐Ÿ› ๏ธ Run clang-format and black""" + # Run clang-format + subprocess.run( + "find src -name '*.cpp' -o -name '*.h' -o -name '*.hpp' | xargs clang-format -i", + shell=True, + check=True, + ) + + # Run black + subprocess.run(["black", "."], check=True) + + if __name__ == "__main__": cli() diff --git a/README.md b/README.md index 6558c2f6..0c2e87e0 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,15 @@ or the following plain bibliography: ``` Teuwen, J., Romor, L., Pai, A., Schirris, Y., Marcus E. (2024). DLUP: Deep Learning Utilities for Pathology (Version 0.7.0) [Computer software]. https://github.com/NKI-AI/dlup ``` + +## Contributors +In alphabetic order: + +## Contributors +In alphabetic order: + +## Contributors +In alphabetic order: + +| [
Ajey Pai Karkala](https://github.com/AjeyPaiK) | [
Eric Marcus](https://github.com/EricMarcus-ai) | [
Jonas Teuwen](https://github.com/jonasteuwen) | [
Leonardo Romor](https://github.com/lromor) | [
Rolf Harkes](https://github.com/rharkes) | [
Yoni Schirris](https://github.com/YoniSchirris) | +| :---: | :---: | :---: | :---: | :---: | :---: | diff --git a/dlup/_geometry.pyi b/dlup/_geometry.pyi new file mode 100644 index 00000000..8ec7dca3 --- /dev/null +++ b/dlup/_geometry.pyi @@ -0,0 +1,106 @@ +from typing import Callable, overload + +import numpy as np +from numpy.typing import NDArray + +from dlup._types import GenericNumber +from dlup.geometry import Box as Box_ +from dlup.geometry import Point as Point_ +from dlup.geometry import Polygon as Polygon_ + +class Polygon: + @property + def fields(self) -> list[str]: ... + def get_exterior(self) -> list[tuple[float, float]]: ... + def get_interiors(self) -> list[list[tuple[float, float]]]: ... + def contains(self, other: Polygon_) -> bool: ... + def correct_orientation(self) -> None: ... + @property + def area(self) -> float: ... + +def set_polygon_factory(factory: Callable[[Polygon_], Polygon_]) -> None: ... + +class Point: + @property + def fields(self) -> list[str]: ... + def scale(self, scaling: float) -> None: ... + def get_coordinates(self) -> tuple[float, float]: ... + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + +def set_point_factory(factory: Callable[[Point_], Point_]) -> None: ... + +class Box: + @property + def fields(self) -> list[str]: ... + def as_polygon(self) -> Polygon_: ... + @property + def area(self) -> float: ... + def scale(self, scaling: float) -> None: ... + @property + def coordinates(self) -> tuple[float, float]: ... + @property + def size(self) -> tuple[float, float]: ... + +def set_box_factory(factory: Callable[[Box_], Box_]) -> None: ... + +class LazyArray: + def __array__(self) -> NDArray[np.int_]: ... + def numpy(self) -> NDArray[np.int_]: ... + +class PolygonCollection: + def get_geometries(self) -> list[Polygon_]: ... + def to_mask(self) -> LazyArray: ... + +class AnnotationRegion: + @property + def polygons(self) -> PolygonCollection: ... + @property + def rois(self) -> PolygonCollection: ... + @property + def points(self) -> list[Point_]: ... + @property + def boxes(self) -> list[Box_]: ... + +class GeometryCollection: + def add_polygon(self, polygon: Polygon) -> None: ... + def add_roi(self, roi: Polygon) -> None: ... + def add_point(self, point: Point_) -> None: ... + def add_box(self, box: Box_) -> None: ... + @property + def polygons(self) -> list[Polygon_]: ... + @property + def rois(self) -> list[Polygon_]: ... + @property + def points(self) -> list[Point_]: ... + @property + def boxes(self) -> list[Box_]: ... + def set_offset(self, offset: tuple[float, float]) -> None: ... + def rebuild_rtree(self) -> None: ... + def reindex_polygons(self, index_map: dict[str, int]) -> None: ... + @overload + def remove_point(self, index: int) -> None: ... + @overload + def remove_point(self, point: Point_) -> None: ... + @overload + def remove_polygon(self, index: int) -> None: ... + @overload + def remove_polygon(self, polygon: Polygon_) -> None: ... + @overload + def remove_roi(self, index: int) -> None: ... + @overload + def remove_roi(self, roi: Polygon_) -> None: ... + def sort_polygons(self, key: Callable[[Polygon_], int | float | str | None], reverse: bool) -> None: ... + @property + def bounding_box(self) -> tuple[tuple[float, float], tuple[float, float]]: ... + def size(self) -> int: ... + def simplify(self, tolerance: float) -> None: ... + def scale(self, scaling: float) -> None: ... + def read_region( + self, + coordinates: tuple[GenericNumber, GenericNumber], + scaling: float, + size: tuple[GenericNumber, GenericNumber], + ) -> AnnotationRegion: ... diff --git a/dlup/annotations.py b/dlup/annotations.py index bed5c22e..c57e7bb2 100644 --- a/dlup/annotations.py +++ b/dlup/annotations.py @@ -21,6 +21,7 @@ import json import os import pathlib +import warnings import xml.etree.ElementTree as ET from dataclasses import dataclass, replace from enum import Enum @@ -42,7 +43,7 @@ from dlup._exceptions import AnnotationError from dlup._types import GenericNumber, PathLike -from dlup.utils.annotations_utils import _get_geojson_color, _get_geojson_z_index, _hex_to_rgb +from dlup.utils.annotations_utils import _get_geojson_z_index, get_geojson_color, hex_to_rgb from dlup.utils.imports import DARWIN_SDK_AVAILABLE, PYHALOXML_AVAILABLE # TODO: @@ -149,7 +150,7 @@ class DarwinV7Metadata(NamedTuple): @functools.lru_cache(maxsize=None) -def _get_v7_metadata(filename: pathlib.Path) -> Optional[dict[tuple[str, str], DarwinV7Metadata]]: +def get_v7_metadata(filename: pathlib.Path) -> Optional[dict[tuple[str, str], DarwinV7Metadata]]: if not DARWIN_SDK_AVAILABLE: raise RuntimeError("`darwin` is not available. Install using `python -m pip install darwin-py`.") import darwin.path_utils @@ -180,13 +181,13 @@ def _get_v7_metadata(filename: pathlib.Path) -> Optional[dict[tuple[str, str], D return output -def _is_rectangle(polygon: Polygon | ShapelyPolygon) -> bool: +def _is_rectangle(polygon: DlupShapelyPolygon | ShapelyPolygon) -> bool: if not polygon.is_valid or len(polygon.exterior.coords) != 5 or len(polygon.interiors) != 0: return False return bool(np.isclose(polygon.area, polygon.minimum_rotated_rectangle.area)) -def _is_alligned_rectangle(polygon: Polygon | ShapelyPolygon) -> bool: +def _is_alligned_rectangle(polygon: DlupShapelyPolygon | ShapelyPolygon) -> bool: if not _is_rectangle(polygon): return False min_rotated_rect = polygon.minimum_rotated_rectangle @@ -195,8 +196,9 @@ def _is_alligned_rectangle(polygon: Polygon | ShapelyPolygon) -> bool: def transform( - geometry: Point | Polygon, transformation: Callable[[npt.NDArray[np.float_]], npt.NDArray[np.float_]] -) -> Point | Polygon: + geometry: DlupShapelyPoint | DlupShapelyPolygon, + transformation: Callable[[npt.NDArray[np.float_]], npt.NDArray[np.float_]], +) -> DlupShapelyPoint | DlupShapelyPolygon: """ Transform a geometry. Function taken from Shapely 2.0.1 under the BSD 3-Clause "New" or "Revised" License. Parameters @@ -229,8 +231,8 @@ def transform( returned_geometry = geometry_arr.item() if original_class.annotation_type != "POINT": - return Polygon(returned_geometry, a_cls=original_class) - return Point(returned_geometry, a_cls=original_class) + return DlupShapelyPolygon(returned_geometry, a_cls=original_class) + return DlupShapelyPoint(returned_geometry, a_cls=original_class) class GeoJsonDict(TypedDict): @@ -303,19 +305,21 @@ def __isub__(self, other: Any) -> None: raise TypeError(f"unsupported operand type(s) for -=: {type(self)} and {type(other)}") -class Point(ShapelyPoint, AnnotatedGeometry): # type: ignore[misc] +class DlupShapelyPoint(ShapelyPoint, AnnotatedGeometry): # type: ignore[misc] __slots__ = ShapelyPoint.__slots__ - def __new__(cls, coord: ShapelyPoint | tuple[float, float], a_cls: Optional[AnnotationClass] = None) -> "Point": + def __new__( + cls, coord: ShapelyPoint | tuple[float, float], a_cls: Optional[AnnotationClass] = None + ) -> "DlupShapelyPoint": point = super().__new__(cls, coord) point.__class__ = cls - return cast("Point", point) + return cast("DlupShapelyPoint", point) def __reduce__(self) -> tuple[type, tuple[tuple[float, float], Optional[AnnotationClass]]]: return (self.__class__, ((self.x, self.y), self.annotation_class)) -class Polygon(ShapelyPolygon, AnnotatedGeometry): # type: ignore[misc] +class DlupShapelyPolygon(ShapelyPolygon, AnnotatedGeometry): # type: ignore[misc] __slots__ = ShapelyPolygon.__slots__ def __new__( @@ -323,15 +327,15 @@ def __new__( shell: Union[tuple[float, float], ShapelyPolygon], holes: Optional[list[list[list[float]]] | list[npt.NDArray[np.float_]]] = None, a_cls: Optional[AnnotationClass] = None, - ) -> "Polygon": + ) -> "DlupShapelyPolygon": instance = super().__new__(cls, shell, holes) instance.__class__ = cls - return cast("Polygon", instance) + return cast("DlupShapelyPolygon", instance) def intersect_with_box( self, other: ShapelyPolygon, - ) -> Optional[list["Polygon"]]: + ) -> Optional[list["DlupShapelyPolygon"]]: result = make_valid(self).intersection(other) if self.area > 0 and result.area == 0: return None @@ -343,9 +347,9 @@ def intersect_with_box( annotation_class = self.annotation_class if isinstance(result, ShapelyPolygon): - return [Polygon(result, a_cls=annotation_class)] + return [DlupShapelyPolygon(result, a_cls=annotation_class)] elif isinstance(result, (ShapelyMultiPolygon, shapely.geometry.collection.GeometryCollection)): - return [Polygon(geom, a_cls=annotation_class) for geom in result.geoms if geom.area > 0] + return [DlupShapelyPolygon(geom, a_cls=annotation_class) for geom in result.geoms if geom.area > 0] else: raise NotImplementedError(f"{type(result)}") @@ -368,7 +372,7 @@ def shape( label: str, color: Optional[tuple[int, int, int]] = None, z_index: Optional[int] = None, -) -> list[Polygon | Point]: +) -> list[DlupShapelyPolygon | DlupShapelyPoint]: geom_type = coordinates.get("type", "not_found").lower() if geom_type == "not_found": raise ValueError("No type found in coordinates.") @@ -379,7 +383,7 @@ def shape( annotation_class = AnnotationClass(label=label, annotation_type=AnnotationType.POINT, color=color, z_index=None) _coordinates = coordinates["coordinates"] return [ - Point(np.asarray(c), a_cls=annotation_class) + DlupShapelyPoint(np.asarray(c), a_cls=annotation_class) for c in (_coordinates if geom_type == "multipoint" else [_coordinates]) ] elif geom_type in ["polygon", "multipolygon"]: @@ -387,12 +391,14 @@ def shape( # TODO: Give every polygon in multipolygon their own annotation_class / annotation_type annotation_type = ( AnnotationType.BOX - if geom_type == "polygon" and _is_rectangle(Polygon(_coordinates[0])) + if geom_type == "polygon" and _is_rectangle(DlupShapelyPolygon(_coordinates[0])) else AnnotationType.POLYGON ) annotation_class = AnnotationClass(label=label, annotation_type=annotation_type, color=color, z_index=z_index) return [ - Polygon(shell=np.asarray(c[0]), holes=[np.asarray(hole) for hole in c[1:]], a_cls=annotation_class) + DlupShapelyPolygon( + shell=np.asarray(c[0]), holes=[np.asarray(hole) for hole in c[1:]], a_cls=annotation_class + ) for c in (_coordinates if geom_type == "multipolygon" else [_coordinates]) ] @@ -400,7 +406,7 @@ def shape( def _geometry_to_geojson( - geometry: Polygon | Point, label: str, color: tuple[int, int, int] | None, z_index: int | None + geometry: DlupShapelyPolygon | DlupShapelyPoint, label: str, color: tuple[int, int, int] | None, z_index: int | None ) -> dict[str, Any]: """Function to convert a geometry to a GeoJSON object. @@ -444,7 +450,7 @@ class WsiAnnotations: def __init__( self, - layers: list[Point | Polygon], + layers: list[DlupShapelyPoint | DlupShapelyPolygon], tags: Optional[list[AnnotationClass]] = None, offset_to_slide_bounds: bool = False, sorting: AnnotationSorting | str = AnnotationSorting.NONE, @@ -472,6 +478,9 @@ def __init__( self._sort_layers_in_place() self._available_classes: set[AnnotationClass] = {layer.annotation_class for layer in self._layers} self._str_tree = STRtree(self._layers) + warnings.warn( + "WsiAnnotations will be deprecated in the next release. Use SlideAnnotations instead.", DeprecationWarning + ) @property def available_classes(self) -> set[AnnotationClass]: @@ -534,7 +543,7 @@ def bounding_box(self) -> tuple[tuple[float, float], tuple[float, float]]: [ ( annotation.bounds - if isinstance(annotation, Polygon) + if isinstance(annotation, DlupShapelyPolygon) else (annotation.x, annotation.y, annotation.x, annotation.y) ) for annotation in self._layers @@ -581,7 +590,7 @@ def from_geojson( _geojsons: Iterable[Any] = [pathlib.Path(geojsons)] _geojsons = [geojsons] if not isinstance(geojsons, (tuple, list)) else geojsons - layers: list[Polygon | Point] = [] + layers: list[DlupShapelyPolygon | DlupShapelyPoint] = [] tags = None for path in _geojsons: path = pathlib.Path(path) @@ -602,11 +611,11 @@ def from_geojson( properties = x["properties"] if "classification" in properties: _label = properties["classification"]["name"] - _color = _get_geojson_color(properties["classification"]) + _color = get_geojson_color(properties["classification"]) _z_index = _get_geojson_z_index(properties["classification"]) elif properties.get("objectType", None) == "annotation": _label = properties["name"] - _color = _get_geojson_color(properties) + _color = get_geojson_color(properties) _z_index = _get_geojson_z_index(properties) else: raise ValueError("Could not find label in the GeoJSON properties.") @@ -647,14 +656,14 @@ def from_asap_xml( """ tree = ET.parse(asap_xml) opened_annotation = tree.getroot() - layers: list[Polygon | Point] = [] + layers: list[DlupShapelyPolygon | DlupShapelyPoint] = [] opened_annotations = 0 for parent in opened_annotation: for child in parent: if child.tag != "Annotation": continue label = child.attrib.get("PartOfGroup").strip() # type: ignore - color = _hex_to_rgb(child.attrib.get("Color").strip()) # type: ignore + color = hex_to_rgb(child.attrib.get("Color").strip()) # type: ignore _type = child.attrib.get("Type").lower() # type: ignore annotation_type = AnnotationTypeToDLUPAnnotationType.from_string(_type) @@ -683,9 +692,9 @@ def from_asap_xml( for coordinates in coordinates_list: _cls = AnnotationClass(label=label, annotation_type=annotation_type, color=color) if isinstance(coordinates, ShapelyPoint): - layers.append(Point(coordinates, a_cls=_cls)) + layers.append(DlupShapelyPoint(coordinates, a_cls=_cls)) elif isinstance(coordinates, ShapelyPolygon): - layers.append(Polygon(coordinates, a_cls=_cls)) + layers.append(DlupShapelyPolygon(coordinates, a_cls=_cls)) else: raise NotImplementedError @@ -735,13 +744,13 @@ def from_halo_xml( curr_geometry = pyhaloxml.shapely.region_to_shapely(region) if region.type == pyhaloxml.RegionType.Rectangle: _cls = AnnotationClass(label=layer.name, annotation_type=AnnotationType.BOX) - output_layers.append(Polygon(curr_geometry, a_cls=_cls)) + output_layers.append(DlupShapelyPolygon(curr_geometry, a_cls=_cls)) if region.type in [pyhaloxml.RegionType.Ellipse, pyhaloxml.RegionType.Polygon]: _cls = AnnotationClass(label=layer.name, annotation_type=AnnotationType.POLYGON) - output_layers.append(Polygon(curr_geometry, a_cls=_cls)) + output_layers.append(DlupShapelyPolygon(curr_geometry, a_cls=_cls)) if region.type == pyhaloxml.RegionType.Pin: _cls = AnnotationClass(label=layer.name, annotation_type=AnnotationType.POINT) - output_layers.append(Point(curr_geometry, a_cls=_cls)) + output_layers.append(DlupShapelyPoint(curr_geometry, a_cls=_cls)) else: raise NotImplementedError(f"Regiontype {region.type} is not implemented in DLUP") @@ -784,7 +793,7 @@ def from_darwin_json( darwin_json_fn = pathlib.Path(darwin_json) darwin_an = darwin.utils.parse_darwin_json(darwin_json_fn, None) - v7_metadata = _get_v7_metadata(darwin_json_fn.parent) + v7_metadata = get_v7_metadata(darwin_json_fn.parent) tags = [] layers = [] @@ -810,26 +819,26 @@ def from_darwin_json( _cls = AnnotationClass(label=name, annotation_type=annotation_type, color=annotation_color, z_index=z_index) if annotation_type == AnnotationType.POINT: - curr_point = Point((curr_data["x"], curr_data["y"]), a_cls=_cls) + curr_point = DlupShapelyPoint((curr_data["x"], curr_data["y"]), a_cls=_cls) layers.append(curr_point) continue elif annotation_type == AnnotationType.POLYGON: if "path" in curr_data: # This is a regular polygon - curr_polygon = Polygon([(_["x"], _["y"]) for _ in curr_data["path"]]) - layers.append(Polygon(curr_polygon, a_cls=_cls)) + curr_polygon = DlupShapelyPolygon([(_["x"], _["y"]) for _ in curr_data["path"]]) + layers.append(DlupShapelyPolygon(curr_polygon, a_cls=_cls)) elif "paths" in curr_data: # This is a complex polygon which needs to be parsed with the even-odd rule curr_complex_polygon = _parse_darwin_complex_polygon(curr_data) for polygon in curr_complex_polygon.geoms: - layers.append(Polygon(polygon, a_cls=_cls)) + layers.append(DlupShapelyPolygon(polygon, a_cls=_cls)) else: raise ValueError(f"Got unexpected data keys: {curr_data.keys()}") elif annotation_type == AnnotationType.BOX: x, y, w, h = list(map(curr_data.get, ["x", "y", "w", "h"])) curr_polygon = shapely.geometry.box(x, y, x + w, y + h) - layers.append(Polygon(curr_polygon, a_cls=_cls)) + layers.append(DlupShapelyPolygon(curr_polygon, a_cls=_cls)) else: ValueError(f"Annotation type {annotation_type} is not supported.") @@ -876,7 +885,7 @@ def as_geojson(self) -> GeoJsonDict: curr_annotation, label=curr_annotation.label, color=curr_annotation.color, - z_index=curr_annotation.z_index if isinstance(curr_annotation, Polygon) else None, + z_index=curr_annotation.z_index if isinstance(curr_annotation, DlupShapelyPolygon) else None, ) json_dict["id"] = str(idx) data["features"].append(json_dict) @@ -905,14 +914,14 @@ def simplify(self, tolerance: float, *, preserve_topology: bool = True) -> None: if a_cls.annotation_type == AnnotationType.POINT: continue layer.simplify(tolerance, preserve_topology=preserve_topology) - self._layers[idx] = Polygon(self._layers[idx], a_cls=a_cls) + self._layers[idx] = DlupShapelyPolygon(self._layers[idx], a_cls=a_cls) def read_region( self, location: npt.NDArray[np.int_ | np.float_] | tuple[GenericNumber, GenericNumber], scaling: float, size: npt.NDArray[np.int_ | np.float_] | tuple[GenericNumber, GenericNumber], - ) -> list[Polygon | Point]: + ) -> list[DlupShapelyPolygon | DlupShapelyPoint]: """Reads the region of the annotations. API is the same as `dlup.SlideImage` so they can be used in conjunction. The process is as follows: @@ -956,6 +965,13 @@ def read_region( The polygons can be converted to masks using `dlup.data.transforms.convert_annotations` or `dlup.data.transforms.ConvertAnnotationsToMask`. """ + + warnings.warn( + "WsiAnnotations.read_region() will be deprecated in the next release. " + "Use SlideAnnotations.read_region() instead, which has a different return type", + DeprecationWarning, + ) + box = list(location) + list(np.asarray(location) + np.asarray(size)) box = (np.asarray(box) / scaling).tolist() query_box = geometry.box(*box) @@ -963,9 +979,11 @@ def read_region( curr_indices = self._str_tree.query(query_box) # This is needed because the STRTree returns (seemingly) arbitrary order, and this would destroy the order curr_indices.sort() - filtered_annotations: list[Point | Polygon] = self._str_tree.geometries.take(curr_indices).tolist() + filtered_annotations: list[DlupShapelyPoint | DlupShapelyPolygon] = self._str_tree.geometries.take( + curr_indices + ).tolist() - cropped_annotations: list[Point | Polygon] = [] + cropped_annotations: list[DlupShapelyPoint | DlupShapelyPolygon] = [] for annotation in filtered_annotations: if annotation.annotation_type in (AnnotationType.BOX, AnnotationType.POLYGON): _annotations = annotation.intersect_with_box(query_box) @@ -977,7 +995,7 @@ def read_region( def _affine_coords(coords: npt.NDArray[np.float_]) -> npt.NDArray[np.float_]: return coords * scaling - np.asarray(location, dtype=np.float_) - output: list[Polygon | Point] = [] + output: list[DlupShapelyPolygon | DlupShapelyPoint] = [] for annotation in cropped_annotations: annotation = transform(annotation, _affine_coords) output.append(annotation) @@ -989,29 +1007,32 @@ def __str__(self) -> str: f"tags={[tag.label for tag in self.tags] if self.tags else None})" ) - def __contains__(self, item: Union[str, AnnotationClass, Point, Polygon]) -> bool: + def __contains__(self, item: Union[str, AnnotationClass, DlupShapelyPoint, DlupShapelyPolygon]) -> bool: if isinstance(item, str): return item in [_.label for _ in self.available_classes] - elif isinstance(item, (Point, Polygon)): + elif isinstance(item, (DlupShapelyPoint, DlupShapelyPolygon)): return item in self._layers return item in self.available_classes - def __getitem__(self, idx: int) -> Point | Polygon: + def __getitem__(self, idx: int) -> DlupShapelyPoint | DlupShapelyPolygon: return self._layers[idx] - def __iter__(self) -> Iterable[Point | Polygon]: + def __iter__(self) -> Iterable[DlupShapelyPoint | DlupShapelyPolygon]: for layer in self._layers: yield layer def __len__(self) -> int: return len(self._layers) - def __add__(self, other: WsiAnnotations | Point | Polygon | list[Point | Polygon]) -> WsiAnnotations: - if isinstance(other, (Point, Polygon)): + def __add__( + self, + other: WsiAnnotations | DlupShapelyPoint | DlupShapelyPolygon | list[DlupShapelyPoint | DlupShapelyPolygon], + ) -> WsiAnnotations: + if isinstance(other, (DlupShapelyPoint, DlupShapelyPolygon)): other = [other] if isinstance(other, list): - if not all(isinstance(item, (Point, Polygon)) for item in other): + if not all(isinstance(item, (DlupShapelyPoint, DlupShapelyPolygon)) for item in other): raise TypeError("can only add list purely containing Point and Polygon objects to WsiAnnotations") new_layers = self._layers + other new_tags = self.tags @@ -1028,12 +1049,15 @@ def __add__(self, other: WsiAnnotations | Point | Polygon | list[Point | Polygon layers=new_layers, tags=new_tags, offset_to_slide_bounds=self.offset_to_slide_bounds, sorting=self.sorting ) - def __iadd__(self, other: WsiAnnotations | Point | Polygon | list[Point | Polygon]) -> WsiAnnotations: - if isinstance(other, (Point, Polygon)): + def __iadd__( + self, + other: WsiAnnotations | DlupShapelyPoint | DlupShapelyPolygon | list[DlupShapelyPoint | DlupShapelyPolygon], + ) -> WsiAnnotations: + if isinstance(other, (DlupShapelyPoint, DlupShapelyPolygon)): other = [other] if isinstance(other, list): - if not all(isinstance(item, (Point, Polygon)) for item in other): + if not all(isinstance(item, (DlupShapelyPoint, DlupShapelyPolygon)) for item in other): raise TypeError("can only add list purely containing Point and Polygon objects to WsiAnnotations") self._layers += other @@ -1059,12 +1083,15 @@ def __iadd__(self, other: WsiAnnotations | Point | Polygon | list[Point | Polygo self._str_tree = STRtree(self._layers) return self - def __radd__(self, other: WsiAnnotations | Point | Polygon | list[Point | Polygon]) -> WsiAnnotations: + def __radd__( + self, + other: WsiAnnotations | DlupShapelyPoint | DlupShapelyPolygon | list[DlupShapelyPoint | DlupShapelyPolygon], + ) -> WsiAnnotations: # in-place addition (+=) of Point and Polygon will raise a TypeError - if not isinstance(other, (WsiAnnotations, Point, Polygon, list)): + if not isinstance(other, (WsiAnnotations, DlupShapelyPoint, DlupShapelyPolygon, list)): return NotImplemented if isinstance(other, list): - if not all(isinstance(item, (Point, Polygon)) for item in other): + if not all(isinstance(item, (DlupShapelyPoint, DlupShapelyPolygon)) for item in other): raise TypeError("can only add list purely containing Point and Polygon objects to WsiAnnotations") raise TypeError( "use the __add__ or __iadd__ operator instead of __radd__ when working with lists to avoid \ @@ -1072,10 +1099,10 @@ def __radd__(self, other: WsiAnnotations | Point | Polygon | list[Point | Polygo ) return self + other - def __sub__(self, other: WsiAnnotations | Point | Polygon) -> WsiAnnotations: + def __sub__(self, other: WsiAnnotations | DlupShapelyPoint | DlupShapelyPolygon) -> WsiAnnotations: return NotImplemented - def __isub__(self, other: WsiAnnotations | Point | Polygon) -> WsiAnnotations: + def __isub__(self, other: WsiAnnotations | DlupShapelyPoint | DlupShapelyPolygon) -> WsiAnnotations: return NotImplemented def __rsub__(self, other: WsiAnnotations) -> WsiAnnotations: diff --git a/dlup/annotations_experimental.py b/dlup/annotations_experimental.py new file mode 100644 index 00000000..43651a74 --- /dev/null +++ b/dlup/annotations_experimental.py @@ -0,0 +1,1485 @@ +# Copyright (c) dlup contributors +""" +Experimental annotations module for dlup. + +""" +from __future__ import annotations + +import copy +import errno +import functools +import json +import os +import pathlib +import warnings +import xml.etree.ElementTree as ET +from dataclasses import asdict +from datetime import datetime +from enum import Enum +from typing import Any, Callable, Iterable, NamedTuple, Optional, Type, TypedDict, TypeVar + +import numpy as np +import numpy.typing as npt +from xsdata.formats.dataclass.parsers import XmlParser +from xsdata.formats.dataclass.serializers import XmlSerializer +from xsdata.formats.dataclass.serializers.config import SerializerConfig +from xsdata.models.datatype import XmlDate + +from dlup import SlideImage, __version__ +from dlup._exceptions import AnnotationError +from dlup._geometry import AnnotationRegion # pylint: disable=no-name-in-module +from dlup._types import GenericNumber, PathLike +from dlup.geometry import Box, GeometryCollection, Point, Polygon +from dlup.utils.annotations_utils import get_geojson_color, hex_to_rgb, rgb_to_hex +from dlup.utils.geometry_xml import ( + create_xml_geometries, + create_xml_rois, + parse_dlup_xml_polygon, + parse_dlup_xml_roi_box, +) +from dlup.utils.imports import DARWIN_SDK_AVAILABLE, PYHALOXML_AVAILABLE +from dlup.utils.schemas.generated import DlupAnnotations as XMLDlupAnnotations +from dlup.utils.schemas.generated import Metadata as XMLMetadata +from dlup.utils.schemas.generated import Tag as XMLTag +from dlup.utils.schemas.generated import Tags as XMLTags + +_TSlideAnnotations = TypeVar("_TSlideAnnotations", bound="SlideAnnotations") + + +class AnnotationType(str, Enum): + POINT = "POINT" + BOX = "BOX" + POLYGON = "POLYGON" + TAG = "TAG" + RASTER = "RASTER" + + +class GeoJsonDict(TypedDict): + """ + TypedDict for standard GeoJSON output + """ + + id: str | None + type: str + features: list[dict[str, str | dict[str, str]]] + metadata: Optional[dict[str, str | list[str]]] + + +class CoordinatesDict(TypedDict): + type: str + coordinates: list[list[list[float]]] + + +class DarwinV7Metadata(NamedTuple): + label: str + color: Optional[tuple[int, int, int]] + annotation_type: AnnotationType + + +@functools.lru_cache(maxsize=None) +def get_v7_metadata(filename: pathlib.Path) -> Optional[dict[tuple[str, str], DarwinV7Metadata]]: + if not DARWIN_SDK_AVAILABLE: + raise ImportError("`darwin` is not available. Install using `python -m pip install darwin-py`.") + import darwin.path_utils + + if not filename.is_dir(): + raise ValueError("Provide the path to the root folder of the Darwin V7 annotations") + + v7_metadata_fn = filename / ".v7" / "metadata.json" + if not v7_metadata_fn.exists(): + return None + v7_metadata = darwin.path_utils.parse_metadata(v7_metadata_fn) + output = {} + for sample in v7_metadata["classes"]: + annotation_type = sample["type"] + # This is not implemented and can be skipped. The main function will raise a NonImplementedError + if annotation_type == "raster_layer": + continue + + label = sample["name"] + color = sample["color"][5:-1].split(",") + if color[-1] != "1.0": + raise RuntimeError("Expected A-channel of color to be 1.0") + rgb_colors = (int(color[0]), int(color[1]), int(color[2])) + + output[(label, annotation_type)] = DarwinV7Metadata( + label=label, color=rgb_colors, annotation_type=annotation_type + ) + return output + + +class AnnotationSorting(str, Enum): + """The ways to sort the annotations. This is used in the constructors of the `SlideAnnotations` class, and applied + to the output of `SlideAnnotations.read_region()`. + + - REVERSE: Sort the output in reverse order. + - AREA: Often when the annotation tools do not properly support hierarchical order, one would annotate in a way + that the smaller objects are on top of the larger objects. This option sorts the output by area, so that the + larger objects appear first in the output and then the smaller objects. + - Z_INDEX: Sort the output by the z-index of the annotations. This is useful when the annotations have a z-index + - NONE: Do not apply any sorting and output as is presented in the input file. + """ + + REVERSE = "REVERSE" + AREA = "AREA" + Z_INDEX = "Z_INDEX" + NONE = "NONE" + + def to_sorting_params(self) -> Any: + """Get the sorting parameters for the annotation sorting.""" + if self == AnnotationSorting.REVERSE: + return lambda x: None, True + + if self == AnnotationSorting.AREA: + return lambda x: x.area, False + + if self == AnnotationSorting.Z_INDEX: + return lambda x: x.get_field("z_index"), False + + +def _geometry_to_geojson( + geometry: Polygon | Point, label: str | None, color: tuple[int, int, int] | None +) -> dict[str, Any]: + """Function to convert a geometry to a GeoJSON object. + + Parameters + ---------- + geometry : DlupPolygon | DlupPoint + A polygon or point object + label : str + The label name + color : tuple[int, int, int] + The color of the object in RGB values + + Returns + ------- + dict[str, Any] + Output dictionary representing the data in GeoJSON + + """ + geojson: dict[str, Any] = { + "type": "Feature", + "properties": { + "classification": { + "name": label, + }, + }, + "geometry": {}, + } + + if isinstance(geometry, Polygon): + # Construct the coordinates for the polygon + exterior = geometry.get_exterior() # Get exterior coordinates + interiors = geometry.get_interiors() # Get interior coordinates (holes) + + # GeoJSON requires [ [x1, y1], [x2, y2], ... ] format + geojson["geometry"] = { + "type": "Polygon", + "coordinates": [[list(coord) for coord in exterior]] # Exterior ring + + [[list(coord) for coord in interior] for interior in interiors], # Interior rings (holes) + } + + elif isinstance(geometry, Point): + # Construct the coordinates for the point + geojson["geometry"] = { + "type": "Point", + "coordinates": [geometry.x, geometry.y], + } + else: + raise ValueError(f"Unsupported geometry type {type(geometry)}") + + if color is not None: + classification: dict[str, Any] = geojson["properties"]["classification"] + classification["color"] = color + + return geojson + + +def geojson_to_dlup( + coordinates: CoordinatesDict, + label: str, + color: Optional[tuple[int, int, int]] = None, + z_index: Optional[int] = None, +) -> list[Polygon | Point]: + geom_type = coordinates.get("type", None) + if geom_type is None: + raise ValueError("No type found in coordinates.") + geom_type = geom_type.lower() + + if geom_type in ["point", "multipoint"] and z_index is not None: + raise AnnotationError("z_index is not supported for point annotations.") + + if geom_type == "point": + x, y = np.asarray(coordinates["coordinates"]) + return [Point(*(x, y), label=label, color=color)] + + if geom_type == "multipoint": + return [Point(*np.asarray(c).tolist(), label=label, color=color) for c in coordinates["coordinates"]] + + if geom_type == "polygon": + _coordinates = coordinates["coordinates"] + polygon = Polygon( + np.asarray(_coordinates[0]), [np.asarray(hole) for hole in _coordinates[1:]], label=label, color=color + ) + return [polygon] + if geom_type == "multipolygon": + output: list[Polygon | Point] = [] + for polygon_coords in coordinates["coordinates"]: + exterior = np.asarray(polygon_coords[0]) + interiors = [np.asarray(hole) for hole in polygon_coords[1:]] + output.append(Polygon(exterior, interiors, label=label, color=color)) + return output + + raise AnnotationError(f"Unsupported geom_type {geom_type}") + + +class TagAttribute(NamedTuple): + label: str + color: Optional[tuple[int, int, int]] + + +class SlideTag(NamedTuple): + attributes: Optional[list[TagAttribute]] + label: str + color: Optional[tuple[int, int, int]] + + +class SlideAnnotations: + """Class that holds all annotations for a specific image""" + + def __init__( + self, + layers: GeometryCollection, + tags: Optional[tuple[SlideTag, ...]] = None, + sorting: Optional[AnnotationSorting | str] = None, + **kwargs: Any, + ) -> None: + """ + Parameters + ---------- + layers : GeometryCollection + Geometry collection containing the polygons, boxes and points + tags: Optional[tuple[SlideTag, ...]] + A tuple of image-level tags such as staining quality + sorting: AnnotationSorting + Sorting method, see `AnnotationSorting`. This value is typically passed to the constructor + because of operations layer on (such as `__add__`). Typically the classmethod already sorts the data + **kwargs: Any + Additional keyword arguments. In this class they are used for additional metadata or offset functions. + Currently only HaloXML requires offsets. See `.from_halo_xml` for an example + """ + self._layers = layers + self._tags = tags + self._sorting = sorting + self._offset_function: bool = bool(kwargs.get("offset_function", False)) + self._metadata: Optional[dict[str, list[str] | str | int | float | bool]] = kwargs.get("metadata", None) + + @property + def sorting(self) -> Optional[AnnotationSorting | str]: + return self._sorting + + @property + def tags(self) -> Optional[tuple[SlideTag, ...]]: + return self._tags + + @property + def num_polygons(self) -> int: + return len(self.layers.polygons) + + @property + def num_points(self) -> int: + return len(self.layers.points) + + @property + def num_boxes(self) -> int: + return len(self.layers.boxes) + + @property + def metadata(self) -> Optional[dict[str, list[str] | str | int | float | bool]]: + """Additional metadata for the annotations""" + return self._metadata + + @property + def offset_function(self) -> Any: + """ + In some cases a function needs to be applied to the coordinates which cannot be handled in this class as + it might require additional information. This function will be applied to the coordinates of all annotations. + This is useful from a file format which requires this, for instance HaloXML. + + Example + ------- + For HaloXML this is `offset = slide.slide_bounds[0] - slide.slide_bounds[0] % 256`. + >>> slide = Slide.from_file_path("image.svs") + >>> ann = SlideAnnotations.from_halo_xml("annotations.xml") + >>> assert ann.offset_function == lambda slide: slide.slide_bounds[0] - slide.slide_bounds[0] % 256 + >>> ann.set_offset(annotation.offset_function(slide)) + + Returns + ------- + Callable + + """ + return self._offset_function + + @offset_function.setter + def offset_function(self, func: Any) -> None: + self._offset_function = func + + @property + def layers(self) -> GeometryCollection: + """ + Get the layers of the annotations. + This is a GeometryCollection object which contains all the polygons and points + """ + return self._layers + + @classmethod + def from_geojson( + cls: Type[_TSlideAnnotations], + geojsons: PathLike, + scaling: float | None = None, + sorting: AnnotationSorting | str = AnnotationSorting.NONE, + roi_names: Optional[list[str]] = None, + ) -> _TSlideAnnotations: + """ + Read annotations from a GeoJSON file. + + Parameters + ---------- + geojsons : PathLike + GeoJSON file. In the properties key there must be a name which is the label of this + object. + scaling : float, optional + Scaling factor. Sometimes required when GeoJSON annotations are stored in a different resolution than the + original image. + sorting : AnnotationSorting + The sorting to apply to the annotations. Check the `AnnotationSorting` enum for more information. + By default, the annotations are sorted by area. + roi_names : list[str], optional + List of names that should be considered as regions of interest. If set, these will be added as ROIs rather + than polygons. + + Returns + ------- + SlideAnnotations + """ + roi_names = [] if roi_names is None else roi_names + collection = GeometryCollection() + path = pathlib.Path(geojsons) + if not path.exists(): + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(path)) + + with open(path, "r", encoding="utf-8") as annotation_file: + geojson_dict = json.load(annotation_file) + features = geojson_dict["features"] + for x in features: + properties = x["properties"] + if "classification" in properties: + _label = properties["classification"]["name"] + _color = get_geojson_color(properties["classification"]) + elif properties.get("objectType", None) == "annotation": + _label = properties["name"] + _color = get_geojson_color(properties) + else: + raise ValueError("Could not find label in the GeoJSON properties.") + + _geometries = geojson_to_dlup(x["geometry"], label=_label, color=_color) + for geometry in _geometries: + if isinstance(geometry, Polygon): + if geometry.label in roi_names: + collection.add_roi(geometry) + else: + collection.add_polygon(geometry) + elif isinstance(geometry, Point): + collection.add_point(geometry) + else: + raise ValueError(f"Unsupported geometry type {geometry}") + + SlideAnnotations._in_place_sort_and_scale(collection, scaling, sorting) + return cls(layers=collection) + + @classmethod + def from_asap_xml( + cls: Type[_TSlideAnnotations], + asap_xml: PathLike, + scaling: float | None = None, + sorting: AnnotationSorting | str = AnnotationSorting.AREA, + roi_names: Optional[list[str]] = None, + ) -> _TSlideAnnotations: + """ + Read annotations as an ASAP [1] XML file. ASAP is a tool for viewing and annotating whole slide images. + + Parameters + ---------- + asap_xml : PathLike + Path to ASAP XML annotation file. + scaling : float, optional + Scaling factor. Sometimes required when ASAP annotations are stored in a different resolution than the + original image. + sorting: AnnotationSorting + The sorting to apply to the annotations. Check the `AnnotationSorting` enum for more information. + By default, the annotations are sorted by area. + roi_names : list[str], optional + List of names that should be considered as regions of interest. If set, these will be added as ROIs rather + than polygons. + + References + ---------- + .. [1] https://github.com/computationalpathologygroup/ASAP + + Returns + ------- + SlideAnnotations + """ + path = pathlib.Path(asap_xml) + roi_names = [] if roi_names is None else roi_names + if not path.exists(): + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(path)) + + tree = ET.parse(asap_xml) + opened_annotation = tree.getroot() + collection: GeometryCollection = GeometryCollection() + for parent in opened_annotation: + for child in parent: + if child.tag != "Annotation": + continue + label = child.attrib.get("PartOfGroup").strip() # type: ignore + color = hex_to_rgb(child.attrib.get("Color").strip()) # type: ignore + + annotation_type = child.attrib.get("Type").lower() # type: ignore + coordinates = _parse_asap_coordinates(child) + + if annotation_type == "pointset": + for point in coordinates: + collection.add_point(Point(point, label=label, color=color)) + + elif annotation_type == "polygon": + if label in roi_names: + collection.add_roi(Polygon(coordinates, [], label=label, color=color)) + else: + collection.add_polygon(Polygon(coordinates, [], label=label, color=color)) + + SlideAnnotations._in_place_sort_and_scale(collection, scaling, sorting) + return cls(layers=collection) + + @classmethod + def from_darwin_json( + cls: Type[_TSlideAnnotations], + darwin_json: PathLike, + scaling: float | None = None, + sorting: AnnotationSorting | str = AnnotationSorting.NONE, + z_indices: Optional[dict[str, int]] = None, + roi_names: Optional[list[str]] = None, + ) -> _TSlideAnnotations: + """ + Read annotations as a V7 Darwin [1] JSON file. If available will read the `.v7/metadata.json` file to extract + colors from the annotations. + + Parameters + ---------- + darwin_json : PathLike + Path to the Darwin JSON file. + sorting: AnnotationSorting + The sorting to apply to the annotations. Check the `AnnotationSorting` enum for more information. + By default, the annotations are sorted by the z-index which is generated by the order of the saved + annotations. + scaling : float, optional + Scaling factor. Sometimes required when Darwin annotations are stored in a different resolution + than the original image. + z_indices: dict[str, int], optional + If set, these z_indices will be used rather than the default order. + roi_names : list[str], optional + List of names that should be considered as regions of interest. If set, these will be added as ROIs rather + than polygons. + + References + ---------- + .. [1] https://darwin.v7labs.com/ + + Returns + ------- + SlideAnnotations + + """ + if not DARWIN_SDK_AVAILABLE: + raise RuntimeError("`darwin` is not available. Install using `python -m pip install darwin-py`.") + import darwin + + roi_names = [] if roi_names is None else roi_names + + darwin_json_fn = pathlib.Path(darwin_json) + if not darwin_json_fn.exists(): + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(darwin_json_fn)) + + darwin_an = darwin.utils.parse_darwin_json(darwin_json_fn, None) + v7_metadata = get_v7_metadata(darwin_json_fn.parent) + + tags = [] + polygons: list[tuple[Polygon, int]] = [] + + collection = GeometryCollection() + for curr_annotation in darwin_an.annotations: + name = curr_annotation.annotation_class.name + annotation_type = curr_annotation.annotation_class.annotation_type + if annotation_type == "raster_layer": + raise NotImplementedError("Raster annotations are not supported.") + + annotation_color = v7_metadata[(name, annotation_type)].color if v7_metadata else None + + if annotation_type == "tag": + attributes = [] + if curr_annotation.subs: + for subannotation in curr_annotation.subs: + if subannotation.annotation_type == "attributes": + attributes.append(TagAttribute(label=subannotation.data, color=None)) + + tags.append( + SlideTag( + attributes=attributes if attributes != [] else None, + label=name, + color=annotation_color if annotation_color else None, + ) + ) + continue + + z_index = 0 if annotation_type == "keypoint" or z_indices is None else z_indices[name] + curr_data = curr_annotation.data + + if annotation_type == "keypoint": + x, y = curr_data["x"], curr_data["y"] + curr_point = Point(curr_data["x"], curr_data["y"]) + curr_point.label = name + curr_point.color = annotation_color + collection.add_point(curr_point) + + elif annotation_type in ("polygon", "complex_polygon"): + if "path" in curr_data: # This is a regular polygon + curr_polygon = Polygon( + [(_["x"], _["y"]) for _ in curr_data["path"]], [], label=name, color=annotation_color + ) + polygons.append((curr_polygon, z_index)) + + elif "paths" in curr_data: # This is a complex polygon which needs to be parsed with the even-odd rule + for curr_polygon in _parse_darwin_complex_polygon(curr_data, label=name, color=annotation_color): + polygons.append((curr_polygon, z_index)) + else: + raise ValueError(f"Got unexpected data keys: {curr_data.keys()}") + elif annotation_type == "bounding_box": + warnings.warn( + "Bounding box annotations are not fully supported and will be converted to Polygons.", UserWarning + ) + x, y, w, h = curr_data["x"], curr_data["y"], curr_data["w"], curr_data["h"] + curr_polygon = Polygon( + [(x, y), (x + w, y), (x + w, y + h), (x, y + h)], [], label=name, color=annotation_color + ) + polygons.append((curr_polygon, z_index)) + + else: + raise ValueError(f"Annotation type {annotation_type} is not supported.") + + if sorting == "Z_INDEX": + for polygon, _ in sorted(polygons, key=lambda x: x[1]): + if polygon.label in roi_names: + collection.add_roi(polygon) + else: + collection.add_polygon(polygon) + else: + for polygon, _ in polygons: + if polygon.label in roi_names: + collection.add_roi(polygon) + else: + collection.add_polygon(polygon) + + SlideAnnotations._in_place_sort_and_scale( + collection, scaling, sorting="NONE" if sorting == "Z_INDEX" else sorting + ) + return cls(layers=collection, tags=tuple(tags), sorting=sorting) + + @classmethod + def from_dlup_xml(cls: Type[_TSlideAnnotations], dlup_xml: PathLike) -> _TSlideAnnotations: + """ + Read annotations as a DLUP XML file. + + Parameters + ---------- + dlup_xml : PathLike + Path to the DLUP XML file. + + Returns + ------- + SlideAnnotations + """ + path = pathlib.Path(dlup_xml) + if not path.exists(): + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(path)) + + parser = XmlParser() + with open(dlup_xml, "rb") as f: + dlup_annotations = parser.from_bytes(f.read(), XMLDlupAnnotations) + + metadata = None if not dlup_annotations.metadata else asdict(dlup_annotations.metadata) + tags: list[SlideTag] = [] + if dlup_annotations.tags: + for tag in dlup_annotations.tags.tag: + if not tag.label: + raise ValueError("Tag does not have a label.") + curr_tag = SlideTag(attributes=[], label=tag.label, color=hex_to_rgb(tag.color) if tag.color else None) + tags.append(curr_tag) + + collection = GeometryCollection() + polygons: list[tuple[Polygon, int]] = [] + if not dlup_annotations.geometries: + return cls(layers=collection, tags=tuple(tags)) + + if dlup_annotations.geometries.polygon: + polygons += parse_dlup_xml_polygon(dlup_annotations.geometries.polygon) + + # Complain if there are multipolygons + if dlup_annotations.geometries.multi_polygon: + for curr_polygons in dlup_annotations.geometries.multi_polygon: + polygons += parse_dlup_xml_polygon( + curr_polygons.polygon, + order=curr_polygons.order, + label=curr_polygons.label, + index=curr_polygons.index, + ) + + # Now we sort the polygons on order + for polygon, _ in sorted(polygons, key=lambda x: x[1]): + collection.add_polygon(polygon) + + for curr_point in dlup_annotations.geometries.point: + point = Point( + curr_point.x, + curr_point.y, + label=curr_point.label, + color=hex_to_rgb(curr_point.color) if curr_point.color else None, + ) + collection.add_point(point) + + # Complain if there are multipoints + if dlup_annotations.geometries.multi_point: + raise NotImplementedError("Multipoints are not supported.") + + for curr_box in dlup_annotations.geometries.box: + # mypy struggles + assert isinstance(curr_box.x_min, float) + assert isinstance(curr_box.y_min, float) + assert isinstance(curr_box.x_max, float) + assert isinstance(curr_box.y_max, float) + box = Box( + (curr_box.x_min, curr_box.y_min), + (curr_box.x_max - curr_box.x_min, curr_box.y_max - curr_box.y_min), + label=curr_box.label, + color=hex_to_rgb(curr_box.color) if curr_box.color else None, + ) + collection.add_box(box) + + rois: list[tuple[Polygon, int]] = [] + if dlup_annotations.regions_of_interest: + for region_of_interest in dlup_annotations.regions_of_interest.multi_polygon: + raise NotImplementedError( + "MultiPolygon regions of interest are not yet supported. " + "If you have a use case for this, " + "please open an issue at https://github.com/NKI-AI/dlup/issues." + ) + + if dlup_annotations.regions_of_interest.polygon: + rois += parse_dlup_xml_polygon(dlup_annotations.regions_of_interest.polygon) + + if dlup_annotations.regions_of_interest.box: + for _curr_box in dlup_annotations.regions_of_interest.box: + box, curr_order = parse_dlup_xml_roi_box(_curr_box) + rois.append((box.as_polygon(), curr_order)) + for roi, _ in sorted(rois, key=lambda x: x[1]): + collection.add_roi(roi) + + return cls(layers=collection, tags=tuple(tags), metadata=metadata) + + @classmethod + def from_halo_xml( + cls: Type[_TSlideAnnotations], + halo_xml: PathLike, + scaling: float | None = None, + sorting: AnnotationSorting | str = AnnotationSorting.NONE, + box_as_polygon: bool = False, + roi_names: Optional[list[str]] = None, + ) -> _TSlideAnnotations: + """ + Read annotations as a Halo [1] XML file. + This function requires `pyhaloxml` [2] to be installed. + + Parameters + ---------- + halo_xml : PathLike + Path to the Halo XML file. + scaling : float, optional + The scaling to apply to the annotations. + sorting: AnnotationSorting + The sorting to apply to the annotations. Check the `AnnotationSorting` enum for more information. By default + the annotations are not sorted as HALO supports hierarchical annotations. + box_as_polygon : bool + If True, rectangles are converted to polygons, and added as such. + This is useful when the rectangles are actually implicitly bounding boxes. + roi_names : list[str], optional + List of names that should be considered as regions of interest. If set, these will be added as ROIs rather + than polygons. + + References + ---------- + .. [1] https://indicalab.com/halo/ + .. [2] https://github.com/rharkes/pyhaloxml + + Returns + ------- + SlideAnnotations + """ + path = pathlib.Path(halo_xml) + roi_names = [] if roi_names is None else roi_names + + if not path.exists(): + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(path)) + + if not PYHALOXML_AVAILABLE: + raise RuntimeError("`pyhaloxml` is not available. Install using `python -m pip install pyhaloxml`.") + import pyhaloxml.shapely + + collection = GeometryCollection() + with pyhaloxml.HaloXMLFile(halo_xml) as hx: + hx.matchnegative() + for layer in hx.layers: + _color = layer.linecolor.rgb + color = (_color[0], _color[1], _color[2]) + for region in layer.regions: + if region.type == pyhaloxml.RegionType.Rectangle: + # The data is a CCW polygon, so the first and one to last coordinates are the coordinates + vertices = region.getvertices() + min_x = min(v[0] for v in vertices) + max_x = max(v[0] for v in vertices) + min_y = min(v[1] for v in vertices) + max_y = max(v[1] for v in vertices) + curr_box = Box((min_x, min_y), (max_x - min_x, max_y - min_y)) + + if box_as_polygon: + polygon = curr_box.as_polygon() + if polygon.label in roi_names: + collection.add_roi(polygon) + else: + collection.add_polygon(polygon) + else: + collection.add_box(curr_box) + continue + + elif region.type in [pyhaloxml.RegionType.Ellipse, pyhaloxml.RegionType.Polygon]: + polygon = Polygon( + region.getvertices(), [x.getvertices() for x in region.holes], label=layer.name, color=color + ) + if polygon.label in roi_names: + collection.add_roi(polygon) + else: + collection.add_polygon(polygon) + elif region.type == pyhaloxml.RegionType.Pin: + point = Point(*(region.getvertices()[0]), label=layer.name, color=color) + collection.add_point(point) + elif region.type == pyhaloxml.RegionType.Ruler: + warnings.warn( + f"Ruler annotations are not supported. Annotation {layer.name} will be skipped", + UserWarning, + ) + continue + else: + raise NotImplementedError(f"Regiontype {region.type} is not implemented in dlup") + + SlideAnnotations._in_place_sort_and_scale(collection, scaling, sorting) + + def offset_function(slide: "SlideImage") -> tuple[int, int]: + return ( + slide.slide_bounds[0][0] - slide.slide_bounds[0][0] % 256, + slide.slide_bounds[0][1] - slide.slide_bounds[0][1] % 256, + ) + + return cls(collection, tags=None, sorting=sorting, offset_function=offset_function) + + @staticmethod + def _in_place_sort_and_scale( + collection: GeometryCollection, scaling: Optional[float], sorting: Optional[AnnotationSorting | str] + ) -> None: + if sorting == "REVERSE": + raise NotImplementedError("This doesn't work for now.") + + if scaling != 1.0 and scaling is not None: + collection.scale(scaling) + if sorting == AnnotationSorting.NONE or sorting is None: + return + if isinstance(sorting, str): + key, reverse = AnnotationSorting[sorting].to_sorting_params() + else: + key, reverse = sorting.to_sorting_params() + collection.sort_polygons(key, reverse) + + def as_geojson(self) -> GeoJsonDict: + """ + Output the annotations as proper geojson. These outputs are sorted according to the `AnnotationSorting` selected + for the annotations. This ensures the annotations are correctly sorted in the output. + + The output is not completely GeoJSON compliant as some parts such as the metadata and properties are not part + of the standard. However, these are implemented to ensure the output is compatible with QuPath. + + Returns + ------- + GeoJsonDict + The output as a GeoJSON dictionary. + """ + data: GeoJsonDict = {"type": "FeatureCollection", "metadata": None, "features": [], "id": None} + if self.tags: + data["metadata"] = {"tags": [_.label for _ in self.tags]} + + if self._layers.boxes: + warnings.warn("Bounding boxes are not supported in GeoJSON and will be skipped.", UserWarning) + + all_layers = self.layers.polygons + self.layers.points + for idx, curr_annotation in enumerate(all_layers): + json_dict = _geometry_to_geojson(curr_annotation, label=curr_annotation.label, color=curr_annotation.color) + json_dict["id"] = str(idx) + data["features"].append(json_dict) + + return data + + def as_dlup_xml( + self, + image_id: Optional[str] = None, + description: Optional[str] = None, + version: Optional[str] = None, + authors: Optional[list[str]] = None, + indent: Optional[int] = 2, + ) -> str: + """ + Output the annotations as DLUP XML. + This format supports the complete serialization of a SlideAnnotations object. + + Parameters + ---------- + image_id : str, optional + The image ID corresponding to this annotation. + description : str, optional + Description of the annotations. + version : str, optional + Version of the annotations. + authors : list[str], optional + Authors of the annotations. + indent : int, optional + Indent for pretty printing. + + Returns + ------- + str + The output as a DLUP XML string. + """ + + metadata = XMLMetadata( + image_id=image_id if image_id is not None else "", + description=description if description is not None else "", + version=version if version is not None else "", + authors=XMLMetadata.Authors(authors) if authors is not None else None, + date_created=XmlDate.from_string(datetime.now().strftime("%Y-%m-%d")), + software=f"dlup {__version__}", + ) + xml_tags: list[XMLTag] = [] + if self.tags: + for tag in self.tags: + if tag.attributes: + attrs = [ + XMLTag.Attribute(value=_.label, color=rgb_to_hex(*_.color) if _.color else None) + for _ in tag.attributes + ] + xml_tag = XMLTag( + attribute=attrs if tag.attributes else [], + label=tag.label, + color=rgb_to_hex(*tag.color) if tag.color else None, + ) + xml_tags.append(xml_tag) + + tags = XMLTags(tag=xml_tags) if xml_tags else None + + geometries = create_xml_geometries(self._layers) + rois = create_xml_rois(self._layers) + + extra_annotation_params: dict[str, XMLTags] = {} + if tags: + extra_annotation_params["tags"] = tags + + dlup_annotations = XMLDlupAnnotations( + metadata=metadata, geometries=geometries, regions_of_interest=rois, **extra_annotation_params + ) + config = SerializerConfig(pretty_print=True) + serializer = XmlSerializer(config=config) + return serializer.render(dlup_annotations) + + @property + def bounding_box(self) -> tuple[tuple[float, float], tuple[float, float]]: + """Get the bounding box of the annotations combining points and polygons. + + Returns + ------- + tuple[tuple[float, float], tuple[float, float]] + The bounding box of the annotations. + + """ + return self._layers.bounding_box + + def bounding_box_at_scaling(self, scaling: float) -> tuple[tuple[float, float], tuple[float, float]]: + """Get the bounding box of the annotations at a specific scaling factor. + + Parameters + ---------- + scaling : float + The scaling factor to apply to the annotations. + + Returns + ------- + tuple[tuple[float, float], tuple[float, float]] + The bounding box of the annotations at the specific scaling factor. + + """ + bbox = self.bounding_box + return ((bbox[0][0] * scaling, bbox[0][1] * scaling), (bbox[1][0] * scaling, bbox[1][1] * scaling)) + + def simplify(self, tolerance: float) -> None: + """Simplify the polygons in the annotation (i.e. reduce points). Other annotations will remain unchanged. + All points in the resulting polygons object will be in the tolerance distance of the original polygon. + + Parameters + ---------- + tolerance : float + The tolerance to simplify the polygons with. + Returns + ------- + None + """ + self._layers.simplify(tolerance) + + def __contains__(self, item: str | Point | Polygon) -> bool: + if isinstance(item, str): + return item in self.available_classes + if isinstance(item, Point): + return item in self.layers.points + if isinstance(item, Polygon): + return item in self.layers.polygons + + return False + + def __len__(self) -> int: + return self._layers.size() + + @property + def available_classes(self) -> set[str]: + """Get the available classes in the annotations. + + Returns + ------- + set[str] + The available classes in the annotations. + + """ + available_classes = set() + for polygon in self.layers.polygons: + if polygon.label is not None: + available_classes.add(polygon.label) + for point in self.layers.points: + if point.label is not None: + available_classes.add(point.label) + + return available_classes + + def __iter__(self) -> Iterable[Polygon | Point]: + # First returns all the polygons then all points + for polygon in self.layers.polygons: + yield polygon + + for point in self.layers.points: + yield point + + def __add__(self, other: Any) -> "SlideAnnotations": + """ + Add two annotations together. This will return a new `SlideAnnotations` object with the annotations combined. + The polygons will be added from left to right followed the points from left to right. + + Notes + ----- + - The polygons and points are shared between the objects. This means that if you modify the polygons or points + in the new object, the original objects will also be modified. If you wish to avoid this, you must add two + copies together. + - Note that the sorting is not applied to this object. You can apply this by calling `sort_polygons()` on + the resulting object. + + Parameters + ---------- + other : SlideAnnotations + The other annotations to add. + + """ + if not isinstance(other, (SlideAnnotations, Point, Polygon, list)): + raise TypeError(f"Unsupported type {type(other)}") + + if isinstance(other, SlideAnnotations): + if not self.sorting == other.sorting: + raise TypeError("Cannot add annotations with different sorting.") + if self.offset_function != other.offset_function: + raise TypeError( + "Cannot add annotations with different requirements for offsetting to slide bounds " + "(`offset_function`)." + ) + + tags: tuple[SlideTag, ...] = () + if self.tags is None and other.tags is not None: + tags = other.tags + + if other.tags is None and self.tags is not None: + tags = self.tags + + if self.tags is not None and other.tags is not None: + tags = tuple(set(self.tags + other.tags)) + + # Let's add the annotations + collection = GeometryCollection() + for polygon in self.layers.polygons: + collection.add_polygon(copy.deepcopy(polygon)) + for point in self.layers.points: + collection.add_point(copy.deepcopy(point)) + + for polygon in other.layers.polygons: + collection.add_polygon(copy.deepcopy(polygon)) + for point in other.layers.points: + collection.add_point(copy.deepcopy(point)) + + SlideAnnotations._in_place_sort_and_scale(collection, None, self.sorting) + return self.__class__(layers=collection, tags=tuple(tags) if tags else None, sorting=self.sorting) + + if isinstance(other, (Point, Polygon)): + other = [other] + + if isinstance(other, list): + if not all(isinstance(item, (Point, Polygon)) for item in other): + raise TypeError( + f"can only add list purely containing Point and Polygon objects to {self.__class__.__name__}" + ) + + collection = copy.copy(self._layers) + for item in other: + if isinstance(item, Polygon): + collection.add_polygon(item) + elif isinstance(item, Point): + collection.add_point(item) + SlideAnnotations._in_place_sort_and_scale(collection, None, self.sorting) + return self.__class__(layers=collection, tags=copy.copy(self._tags), sorting=self.sorting) + + raise ValueError(f"Unsupported type {type(other)}") + + def __iadd__(self, other: Any) -> "SlideAnnotations": + if isinstance(other, (Point, Polygon)): + other = [other] + + if isinstance(other, list): + if not all(isinstance(item, (Point, Polygon)) for item in other): + raise TypeError( + f"can only add list purely containing Point and Polygon objects {self.__class__.__name__}" + ) + + for item in other: + if isinstance(item, Polygon): + self._layers.add_polygon(copy.deepcopy(item)) + elif isinstance(item, Point): + self._layers.add_point(copy.deepcopy(item)) + + elif isinstance(other, SlideAnnotations): + if self.sorting != other.sorting or self.offset_function != other.offset_function: + raise ValueError( + f"Both sorting and offset_function must be the same to " f"add {self.__class__.__name__}s together." + ) + + if self._tags is None: + self._tags = other._tags + elif other._tags is not None: + assert self + self._tags += other._tags + + for polygon in other.layers.polygons: + self._layers.add_polygon(copy.deepcopy(polygon)) + for point in other.layers.points: + self._layers.add_point(copy.deepcopy(point)) + else: + return NotImplemented + SlideAnnotations._in_place_sort_and_scale(self._layers, None, self.sorting) + + return self + + def __radd__(self, other: Any) -> "SlideAnnotations": + # in-place addition (+=) of Point and Polygon will raise a TypeError + if not isinstance(other, (SlideAnnotations, Point, Polygon, list)): + raise TypeError(f"Unsupported type {type(other)}") + if isinstance(other, list): + if not all(isinstance(item, (Polygon, Point)) for item in other): + raise TypeError( + f"can only add list purely containing Point and Polygon objects to {self.__class__.__name__}" + ) + raise TypeError( + "use the __add__ or __iadd__ operator instead of __radd__ when working with lists to avoid \ + unexpected behavior." + ) + return self + other + + def __sub__(self, other: Any) -> "SlideAnnotations": + return NotImplemented + + def __isub__(self, other: Any) -> "SlideAnnotations": + return NotImplemented + + def __rsub__(self, other: Any) -> "SlideAnnotations": + return NotImplemented + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, SlideAnnotations): + return False + + our_sorting = self._sorting if self._sorting != AnnotationSorting.NONE else None + other_sorting = other._sorting if other._sorting != AnnotationSorting.NONE else None + + if our_sorting != other_sorting: + return False + + if self._tags != other._tags: + return False + + if self._layers != other._layers: + return False + + if self._offset_function != other._offset_function: + return False + + return True + + def read_region( + self, + coordinates: tuple[GenericNumber, GenericNumber], + scaling: float, + size: tuple[GenericNumber, GenericNumber], + ) -> AnnotationRegion: + """Reads the region of the annotations. Function signature is the same as `dlup.SlideImage` + so they can be used in conjunction. + + The process is as follows: + + 1. All the annotations which overlap with the requested region of interest are filtered + 2. The polygons in the GeometryContainer in `.layers` are cropped. + The boxes and points are only filtered, so it's possible the boxes have negative (x, y) values + 3. The annotation is rescaled and shifted to the origin to match the local patch coordinate system. + + The final returned data is a `dlup.geometry.AnnotationRegion`. + + Parameters + ---------- + location: tuple[GenericNumber, GenericNumber] + Top-left coordinates of the region in the requested scaling + size : tuple[GenericNumber, GenericNumber] + Output size of the region + scaling : float + Scaling to apply compared to the base level + + Returns + ------- + AnnotationRegion + + Examples + -------- + 1. To read geojson annotations and convert them into masks: + + >>> from pathlib import Path + >>> from dlup import SlideImage + >>> import numpy as np + >>> wsi = SlideImage.from_file_path(Path("path/to/image.svs")) + >>> wsi = wsi.get_scaled_view(scaling=0.5) + >>> wsi = wsi.read_region(location=(0,0), size=wsi.size) + >>> annotations = SlideAnnotations.from_geojson("path/to/geojson.json") + >>> region = annotations.read_region((0,0), 0.01, wsi.size) + >>> mask = region.to_mask() + >>> color_mask = annotations.color_lut[mask] + >>> polygons = region.polygons.get_geometries() # This is a list of `dlup.geometry.Polygon` objects + """ + region = self._layers.read_region(coordinates, scaling, size) + return region + + def scale(self, scaling: float) -> None: + """ + Scale the annotations by a multiplication factor. + This operation will be performed in-place. + + Parameters + ---------- + scaling : float + The scaling factor to apply to the annotations. + + Notes + ----- + This invalidates the R-tree. You could rebuild this manually using `.rebuild_rtree()`, or have the function + `read_region()` do it for you on-demand. + + Returns + ------- + None + """ + self._layers.scale(scaling) + + def set_offset(self, offset: tuple[float, float]) -> None: + """Set the offset for the annotations. This operation will be performed in-place. + + For example, if the offset is 1, 1, the annotations will be moved by 1 unit in the x and y direction. + + Parameters + ---------- + offset : tuple[float, float] + The offset to apply to the annotations. + + Notes + ----- + This invalidates the R-tree. You could rebuild this manually using `.rebuild_rtree()`, or have the function + `read_region()` do it for you on-demand. + + Returns + ------- + None + """ + self._layers.set_offset(offset) + + def rebuild_rtree(self) -> None: + """ + Rebuild the R-tree for the annotations. This operation will be performed in-place. + The R-tree is used for fast spatial queries on the annotations and is invalidated when the annotations are + modified. This function will rebuild the R-tree. Strictly speaking, this is not required as the R-tree will be + rebuilt on-demand when you invoke a `read_region()`. You could however do this if you want to avoid + the `read_region()` to do it for you the first time it runs. + """ + + self._layers.rebuild_rtree() + + def reindex_polygons(self, index_map: dict[str, int]) -> None: + """ + Reindex the polygons in the annotations. This operation will be performed in-place. + This is useful if you want to change the index of the polygons in the annotations. + + This requires that the `.label` property on the polygons is set. + + Parameters + ---------- + index_map : dict[str, int] + A dictionary that maps the label to the new index. + + Returns + ------- + None + """ + self._layers.reindex_polygons(index_map) + + def relabel_polygons(self, relabel_map: dict[str, str]) -> None: + """ + Relabel the polygons in the annotations. This operation will be performed in-place. + + Parameters + ---------- + relabel_map : dict[str, str] + A dictionary that maps the label to the new label. + + Returns + ------- + None + """ + # TODO: Implement in C++ + for polygon in self._layers.polygons: + if not polygon.label: + continue + if polygon.label in relabel_map: + polygon.label = relabel_map[polygon.label] + + def filter_polygons(self, label: str) -> None: + """Filter polygons in-place. + + Note + ---- + This will internally invalidate the R-tree. You could rebuild this manually using `.rebuild_rtree()`, or + have the function itself do this on-demand (typically when you invoke a `.read_region()`) + + Parameters + ---------- + label : str + The label to filter. + + """ + for polygon in self._layers.polygons: + if polygon.label == label: + self._layers.remove_polygon(polygon) + + def filter_points(self, label: str) -> None: + """Filter points in-place. + + Note + ---- + This will internally invalidate the R-tree. You could rebuild this manually using `.rebuild_rtree()`, or + have the function itself do this on-demand (typically when you invoke a `.read_region()`) + + Parameters + ---------- + label : str + The label to filter. + + """ + for point in self._layers.points: + if point.label == label: + self._layers.remove_point(point) + + def filter(self, label: str) -> None: + """Filter annotations in-place. + + Note + ---- + This will internally invalidate the R-tree. You could rebuild this manually using `.rebuild_rtree()`, or + have the function itself do this on-demand (typically when you invoke a `.read_region()`) + + Parameters + ---------- + label : str + The label to filter. + + """ + self.filter_polygons(label) + self.filter_points(label) + + def sort_polygons(self, key: Callable[[Polygon], int | float | str], reverse: bool = False) -> None: + """Sort the polygons in-place. + + Parameters + ---------- + key : callable + The key to sort the polygons on, this has to be a lambda function or similar. + For instance `lambda polygon: polygon.area` will sort the polygons on the area, or + `lambda polygon: polygon.get_field(field_name)` will sort the polygons on that field. + reverse : bool + Whether to sort in reverse order. + + Note + ---- + This will internally invalidate the R-tree. You could rebuild this manually using `.rebuild_rtree()`, or + have the function itself do this on-demand (typically when you invoke a `.read_region()`) + + Returns + ------- + None + + """ + self._layers.sort_polygons(key, reverse) + + @property + def color_lut(self) -> npt.NDArray[np.uint8]: + """Get the color lookup table for the annotations. + + Requires that the polygons have an index and color set. Be aware that for the background always + the value 0 is assumed. + So if you are using the `to_mask(default_value=0)` with a default value other than 0, + the LUT will still have this as index 0. + + Example + ------- + >>> color_lut = annotations.color_lut + >>> region = annotations.read_region(region_start, 0.02, region_size).to_mask() + >>> colored_mask = PIL.Image.fromarray(color_lut[mask]) + + Returns + ------- + np.ndarray + The color lookup table. + + """ + return self._layers.color_lut + + def __copy__(self) -> "SlideAnnotations": + return self.__class__(layers=copy.copy(self._layers), tags=copy.copy(self._tags)) + + def __deepcopy__(self, memo: dict[int, Any]) -> "SlideAnnotations": + return self.__class__(layers=copy.deepcopy(self._layers, memo), tags=copy.deepcopy(self._tags, memo)) + + def copy(self) -> "SlideAnnotations": + return self.__copy__() + + +def _parse_asap_coordinates( + annotation_structure: ET.Element, +) -> list[tuple[float, float]]: + """ + Parse ASAP XML coordinates into list. + + Parameters + ---------- + annotation_structure : list of strings + + Returns + ------- + list[tuple[float, float]] + + """ + coordinates = [] + coordinate_structure = annotation_structure[0] + + for coordinate in coordinate_structure: + coordinates.append( + ( + float(coordinate.get("X").replace(",", ".")), # type: ignore + float(coordinate.get("Y").replace(",", ".")), # type: ignore + ) + ) + + return coordinates + + +def _parse_darwin_complex_polygon( + annotation: dict[str, Any], label: str, color: Optional[tuple[int, int, int]] +) -> Iterable[Polygon]: + """ + Parse a complex polygon (i.e. polygon with holes) from a Darwin annotation. + + Parameters + ---------- + annotation : dict + The annotation dictionary + label : str + The label of the annotation + color : tuple[int, int, int] + The color of the annotation + + Returns + ------- + Iterable[DlupPolygon] + """ + # Create Polygons and sort by area in descending order + polygons = [Polygon([(p["x"], p["y"]) for p in path], []) for path in annotation["paths"]] + polygons.sort(key=lambda x: x.area, reverse=True) + + outer_polygons: list[tuple[Polygon, list[Any], bool]] = [] + for polygon in polygons: + is_hole = False + # Check if the polygon can be a hole in any of the previously processed polygons + for outer_poly, holes, outer_poly_is_hole in reversed(outer_polygons): + contains = outer_poly.contains(polygon) + # If polygon is contained by a hole, it should be added as new polygon + if contains and outer_poly_is_hole: + break + # Polygon is added as hole if outer polygon is not a hole + elif contains: + holes.append(polygon.get_exterior()) + is_hole = True + break + outer_polygons.append((polygon, [], is_hole)) + + for outer_poly, holes, _is_hole in outer_polygons: + if not _is_hole: + polygon = Polygon(outer_poly.get_exterior(), holes) + polygon.label = label + polygon.color = color + yield polygon diff --git a/dlup/data/dataset.py b/dlup/data/dataset.py index a602905e..80cae0f4 100644 --- a/dlup/data/dataset.py +++ b/dlup/data/dataset.py @@ -33,7 +33,7 @@ from dlup import BoundaryMode, SlideImage from dlup._types import PathLike, ROIType -from dlup.annotations import Point, Polygon, WsiAnnotations +from dlup.annotations import DlupShapelyPoint, DlupShapelyPolygon, WsiAnnotations from dlup.backends.common import AbstractSlideBackend from dlup.background import compute_masked_indices from dlup.tiling import Grid, GridOrder, TilingMode @@ -42,7 +42,7 @@ MaskTypes = Union["SlideImage", npt.NDArray[np.int_], "WsiAnnotations"] -_AnnotationsTypes = Point | Polygon +_AnnotationsTypes = DlupShapelyPoint | DlupShapelyPolygon T_co = TypeVar("T_co", covariant=True) T = TypeVar("T") diff --git a/dlup/data/transforms.py b/dlup/data/transforms.py index 2240690e..ca969acb 100644 --- a/dlup/data/transforms.py +++ b/dlup/data/transforms.py @@ -14,7 +14,7 @@ from dlup.annotations import AnnotationClass, AnnotationType from dlup.data.dataset import PointType, TileSample, TileSampleWithAnnotationData -_AnnotationsTypes = dlup.annotations.Point | dlup.annotations.Polygon +_AnnotationsTypes = dlup.annotations.DlupShapelyPoint | dlup.annotations.DlupShapelyPolygon def convert_annotations( @@ -83,7 +83,7 @@ def convert_annotations( has_roi = False for curr_annotation in annotations: holes_mask = None - if isinstance(curr_annotation, dlup.annotations.Point): + if isinstance(curr_annotation, dlup.annotations.DlupShapelyPoint): coords = tuple(curr_annotation.coords) points[curr_annotation.label] += tuple(coords) continue @@ -234,13 +234,13 @@ def rename_labels(annotations: Iterable[_AnnotationsTypes], remap_labels: dict[s if annotation.annotation_class.annotation_type == AnnotationType.BOX: a_cls = AnnotationClass(label=remap_labels[label], annotation_type=AnnotationType.BOX) - output_annotations.append(dlup.annotations.Polygon(annotation, a_cls=a_cls)) + output_annotations.append(dlup.annotations.DlupShapelyPolygon(annotation, a_cls=a_cls)) elif annotation.annotation_class.annotation_type == AnnotationType.POLYGON: a_cls = AnnotationClass(label=remap_labels[label], annotation_type=AnnotationType.POLYGON) - output_annotations.append(dlup.annotations.Polygon(annotation, a_cls=a_cls)) + output_annotations.append(dlup.annotations.DlupShapelyPolygon(annotation, a_cls=a_cls)) elif annotation.annotation_class.annotation_type == AnnotationType.POINT: a_cls = AnnotationClass(label=remap_labels[label], annotation_type=AnnotationType.POINT) - output_annotations.append(dlup.annotations.Point(annotation, a_cls=a_cls)) + output_annotations.append(dlup.annotations.DlupShapelyPoint(annotation, a_cls=a_cls)) else: raise AnnotationError(f"Unsupported annotation type {annotation.annotation_class.annotation_type}") diff --git a/dlup/geometry.py b/dlup/geometry.py new file mode 100644 index 00000000..82891402 --- /dev/null +++ b/dlup/geometry.py @@ -0,0 +1,479 @@ +# Copyright (c) dlup contributors +"""Module for geometric objects""" +import copy +import warnings +from typing import Any, Optional + +import numpy as np +import numpy.typing as npt + +import dlup._geometry as _dg # pylint: disable=no-name-in-module +from dlup.utils.imports import SHAPELY_AVAILABLE + +if SHAPELY_AVAILABLE: + from shapely.geometry import Point as ShapelyPoint + from shapely.geometry import Polygon as ShapelyPolygon + + +class _BaseGeometry: + def __init__(self, *args: Any, **kwargs: Any): + pass + + @classmethod + def from_shapely(cls, shapely_geometry: ShapelyPoint | ShapelyPolygon) -> "_BaseGeometry": + """Create a new instance of the geometry from a Shapely geometry + + Parameters + ---------- + shapely_geometry : ShapelyPoint | ShapelyPolygon + The Shapely geometry to convert + + Returns + ------- + _BaseGeometry + The new instance of the geometry + """ + raise NotImplementedError + + def set_field(self, name: str, value: Any) -> None: + """Set a field on the geometry. This can be in arbitrary python object. + + Parameters + ---------- + name : str + The name of the field to set + value : Any + The value of the field + + Returns + ------- + None + """ + raise NotImplementedError + + def get_field(self, name: str) -> Any: + """Get a field from the geometry. This can be in arbitrary python object. + + Parameters + ---------- + name : str + The name of the field to get + + Returns + ------- + Any + The value of the field + """ + raise NotImplementedError + + @property + def fields(self) -> list[str]: + + raise NotImplementedError + + @property + def wkt(self) -> str: + raise NotImplementedError + + @property + def label(self) -> Optional[str]: + field = self.get_field("label") + if field is None: + return None + # if field is not isinstance(field, str): + # raise ValueError(f"Label must be a string, got {type(field)}") + assert isinstance(field, str) + return field + + @label.setter + def label(self, value: str) -> None: + if not isinstance(value, str): + raise ValueError(f"Label must be a string, got {type(value)}") + self.set_field("label", value) + + @property + def index(self) -> Optional[int]: + field = self.get_field("index") + if field is None: + return None + if not isinstance(field, int): + raise ValueError(f"Index must be an integer, got {type(field)}") + assert isinstance(field, int) + return field + + @index.setter + def index(self, value: int) -> None: + if not isinstance(value, int): + raise ValueError(f"Index must be an integer, got {type(value)}") + self.set_field("index", value) + + @property + def color(self) -> Optional[tuple[int, int, int]]: + field = self.get_field("color") + if field is None: + return None + if not isinstance(field, tuple) or len(field) != 3: + raise ValueError(f"Color must be an RGB tuple, got {type(field)}") + assert isinstance(field, tuple) + return field + + @color.setter + def color(self, value: tuple[int, int, int]) -> None: + if not isinstance(value, tuple) or len(value) != 3: + raise ValueError(f"Color must be an RGB tuple, got {type(value)}") + self.set_field("color", value) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, type(self)): + return False + + fields = self.fields + other_fields = other.fields + + if sorted(fields) != sorted(other_fields): + return False + + for field in fields: + if self.get_field(field) != other.get_field(field): + return False + if self.wkt != other.wkt: + return False + + return True + + def __iadd__(self, other: Any) -> None: + # TODO: We can support MultiPoint and MultiPolygon + raise TypeError(f"Unsupported operand type(s) for +=: {type(self)} and {type(other)}") + + def __isub__(self, other: Any) -> None: + # TODO: We can support MultiPoint and MultiPolygon + raise TypeError(f"Unsupported operand type(s) for -=: {type(self)} and {type(other)}") + + def __repr__(self) -> str: + repr_string = f"<{self.__class__.__name__}(" + parts = [] + for field in sorted(self.fields): + value = self.get_field(field) + parts.append(f"{field}={value}") + + repr_string += ", ".join(parts) + + if len(self.wkt) > 30: + repr_string += f") WKT='{self.wkt[:30]}...'>" + else: + repr_string += f") WKT='{self.wkt}'>" + return repr_string + + +class Polygon(_dg.Polygon, _BaseGeometry): + def __init__(self, *args: Any, **kwargs: Any): + _BaseGeometry.__init__(self) + if SHAPELY_AVAILABLE: + if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], ShapelyPolygon): + warnings.warn( + "Creating a Polygon from a Shapely Polygon is deprecated and will be removed dlup v1.0.0. " + "Please use the `from_shapely` method instead.", + UserWarning, + ) + shapely_polygon = args[0] + exterior = list(shapely_polygon.exterior.coords) + interiors = [list(interior.coords) for interior in shapely_polygon.interiors] + args = (exterior, interiors) + + # Ensure no new Polygon is created; just wrap the existing one + if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], _dg.Polygon): + super().__init__(args[0]) # This should keep the original parameters intact + else: # This needs to be way more elaborate + fields = {} + if "label" in kwargs: + fields["label"] = kwargs.pop("label") + if "index" in kwargs: + fields["index"] = kwargs.pop("index") + if "color" in kwargs: + fields["color"] = kwargs.pop("color") + + if len(args) == 1: + # No interior + args = (args[0], []) + + super().__init__(*args, **kwargs) + for key, value in fields.items(): + self.set_field(key, value) + + @classmethod + def from_shapely(cls, shapely_polygon: "ShapelyPolygon") -> "Polygon": + if not SHAPELY_AVAILABLE: + raise ImportError( + "Shapely is not available, and this functionality requires it. " + "Install it using `pip install shapely`, " + "or consult the documentation https://shapely.readthedocs.io/en/stable/installation.html " + "for more information." + ) + + if not isinstance(shapely_polygon, ShapelyPolygon): + raise ValueError(f"Expected a shapely.geometry.Polygon, but got {type(shapely_polygon)}") + + exterior = list(shapely_polygon.exterior.coords) + interiors = [list(interior.coords) for interior in shapely_polygon.interiors] + return cls(exterior, interiors) + + def __getstate__(self) -> dict[str, dict[str, Any]]: + state = { + "_fields": {field: self.get_field(field) for field in self.fields}, + "_object": {"interiors": self.get_interiors(), "exterior": self.get_exterior()}, + } + return state + + def __setstate__(self, state: dict[str, dict[str, Any]]) -> None: + exterior = state["_object"]["exterior"] + interiors = state["_object"]["interiors"] + + Polygon.__init__(self, exterior, interiors) + + for key, value in state["_fields"].items(): + self.set_field(key, value) + + def __copy__(self) -> "Polygon": + warnings.warn( + "Copying a Polygon currently creates a complete new object, without reference to the previous one, " + "and is essentially the same as a deepcopy." + ) + new_copy = Polygon(self) + return new_copy + + def __deepcopy__(self, memo: Any) -> "Polygon": + # Create a deepcopy of the geometry + new_copy = Polygon(copy.deepcopy(self.get_exterior(), memo), copy.deepcopy(self.get_interiors(), memo)) + + # Deepcopy the fields + for field in self.fields: + new_copy.set_field(field, copy.deepcopy(self.get_field(field), memo)) + + return new_copy + + def to_shapely(self) -> "ShapelyPolygon": + if not SHAPELY_AVAILABLE: + raise ImportError( + "Shapely is not available, and this functionality requires it. " + "Install it using `pip install shapely`, " + "or consult the documentation https://shapely.readthedocs.io/en/stable/installation.html " + "for more information." + ) + + import shapely.geometry + + exterior = self.get_exterior() + interiors = self.get_interiors() + return shapely.geometry.Polygon(exterior, interiors) + + +def _polygon_factory(polygon: _dg.Polygon) -> "Polygon": + return Polygon(polygon) + + +# This is required to ensure that the polygons created in the C++ code are converted to the correct Python class +_dg.set_polygon_factory(_polygon_factory) + + +class Point(_dg.Point, _BaseGeometry): + def __init__(self, *args: Any, **kwargs: Any) -> None: + _BaseGeometry.__init__(self) + if SHAPELY_AVAILABLE: + if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], ShapelyPoint): + warnings.warn( + "Creating a Polygon from a Shapely Point is deprecated and will be removed dlup v1.0.0. " + "Please use the `from_shapely` method instead.", + UserWarning, + ) + shapely_point = args[0] + args = (shapely_point.x, shapely_point.y) + + # Ensure no new Point is created; just wrap the existing one + if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], _dg.Point): + super().__init__(args[0]) # This should keep the original parameters intact + else: # This needs to be way more elaborate + fields = {} + if "label" in kwargs: + fields["label"] = kwargs.pop("label") + if "index" in kwargs: + fields["index"] = kwargs.pop("index") + if "color" in kwargs: + fields["color"] = kwargs.pop("color") + + super().__init__(*args, **kwargs) + for key, value in fields.items(): + self.set_field(key, value) + + @classmethod + def from_shapely(cls, shapely_point: "ShapelyPoint") -> "Point": + if not SHAPELY_AVAILABLE: + raise ImportError( + "Shapely is not available, and this functionality requires it. " + "Install it using `pip install shapely`, " + "or consult the documentation https://shapely.readthedocs.io/en/stable/installation.html " + "for more information." + ) + + if not isinstance(shapely_point, ShapelyPoint): + raise ValueError(f"Expected a shapely.geometry.Point, but got {type(shapely_point)}") + + return cls(shapely_point.x, shapely_point.y) + + def to_shapely(self) -> "ShapelyPoint": + if not SHAPELY_AVAILABLE: + raise ImportError( + "Shapely is not available, and this functionality requires it. " + "Install it using `pip install shapely`, " + "or consult the documentation https://shapely.readthedocs.io/en/stable/installation.html " + "for more information." + ) + + return ShapelyPoint(self.x, self.y) + + def __copy__(self) -> "Point": + # Create a new instance of DlupPolygon with the same geometry + new_copy = Point(self.x, self.y) + + for field in self.fields: + new_copy.set_field(field, self.get_field(field)) + + return new_copy + + def __deepcopy__(self, memo: Any) -> "Point": + # Create a deepcopy of the geometry + new_copy = Point(copy.deepcopy(self.x), copy.deepcopy(self.y)) + + # Deepcopy the fields + for field in self.fields: + new_copy.set_field(field, copy.deepcopy(self.get_field(field), memo)) + + return new_copy + + def __getstate__(self) -> dict[str, dict[str, Any]]: + state = { + "_fields": {field: self.get_field(field) for field in self.fields}, + "_object": {"coordinates": (self.x, self.y)}, + } + return state + + def __setstate__(self, state: dict[str, dict[str, Any]]) -> None: + coordinates = state["_object"]["coordinates"] + Point.__init__(self, coordinates[0], coordinates[1]) + for key, value in state["_fields"].items(): + self.set_field(key, value) + + +def _point_factory(point: _dg.Point) -> Point: + return Point(point) + + +# Register the point factory +_dg.set_point_factory(_point_factory) + + +class Box(_dg.Box, _BaseGeometry): + def __init__(self, *args: Any, **kwargs: Any) -> None: + _BaseGeometry.__init__(self) + # Ensure no new Point is created; just wrap the existing one + if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], _dg.Box): + super().__init__(args[0]) # This should keep the original parameters intact + else: # This needs to be way more elaborate + fields = {} + if "label" in kwargs: + fields["label"] = kwargs.pop("label") + if "index" in kwargs: + fields["index"] = kwargs.pop("index") + if "color" in kwargs: + fields["color"] = kwargs.pop("color") + + super().__init__(*args, **kwargs) + for key, value in fields.items(): + self.set_field(key, value) + + +def _box_factory(box: _dg.Box) -> Box: + return Box(box) + + +# Register the box factory +_dg.set_box_factory(_box_factory) + + +# TODO: Allow to construct geometry collection from a list of polygons, bypassing the python loop +class GeometryCollection(_dg.GeometryCollection): + def __init__(self) -> None: + super().__init__() + + @property + def color_lut(self) -> npt.NDArray[np.uint8]: + color_map = {} + for r in self.polygons: + color = r.color + index = r.index + if not index: + raise ValueError("Index needs to be set on Polygon to create a color lookup table") + if not color: + raise ValueError("Color needs to be set on Polygon to create a color lookup table") + + color_map[index] = color + + max_index = max(color_map.keys()) + LUT = np.zeros((max_index + 1, 3), dtype=np.uint8) + for key, color in color_map.items(): + LUT[key] = color + + return LUT + + def __getstate__(self) -> dict[str, Any]: + state = { + "_polygons": [polygon.__getstate__() for polygon in self.polygons], + "_points": [point.__getstate__() for point in self.points], + } + return state + + def __setstate__(self, state: dict[str, list[dict[str, Any]]]) -> None: + polygons = [Polygon.__new__(Polygon) for _ in state["_polygons"]] + for polygon, polygon_state in zip(polygons, state["_polygons"]): + polygon.__setstate__(polygon_state) + + points = [Point.__new__(Point) for _ in state["_points"]] + for point, point_state in zip(points, state["_points"]): + point.__setstate__(point_state) + + GeometryCollection.__init__(self) + for polygon in polygons: + self.add_polygon(polygon) + + for point in points: + self.add_point(point) + + def __copy__(self) -> "GeometryCollection": + collection = GeometryCollection() + for polygon in self.polygons: + collection.add_polygon(polygon.__copy__()) + for point in self.points: + collection.add_point(point.__copy__()) + collection.rebuild_rtree() + return collection + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, type(self)): + return False + + if len(self) != len(other): + return False + + if self.boxes != other.boxes: + return False + + if self.polygons != other.polygons: + return False + + if self.points != other.points: + return False + + return True + + def __len__(self) -> int: + # Also self.size() + return len(self.polygons) + len(self.points) + len(self.boxes) diff --git a/dlup/meson.build b/dlup/meson.build new file mode 100644 index 00000000..1aa5a2eb --- /dev/null +++ b/dlup/meson.build @@ -0,0 +1,12 @@ +# Include the NumPy headers +incdir_numpy = get_variable('incdir_numpy', []) + +# Cython extension module +background = py.extension_module('_background', + '_background.pyx', + include_directories : [incdir_numpy], + install : true, + subdir : 'dlup', + link_args : link_args, + c_args : cpp_args + ['-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION'], + dependencies : [py_dep]) # Add Python dependency diff --git a/dlup/utils/annotations_utils.py b/dlup/utils/annotations_utils.py index 18f380bb..c460b07d 100644 --- a/dlup/utils/annotations_utils.py +++ b/dlup/utils/annotations_utils.py @@ -1,10 +1,16 @@ from typing import Optional, cast -def _hex_to_rgb(hex_color: str) -> tuple[int, int, int]: - if "#" not in hex_color: +def hex_to_rgb(hex_color: str) -> tuple[int, int, int]: + if not hex_color.startswith("#"): if hex_color == "black": return 0, 0, 0 + else: + raise ValueError(f"Invalid HEX color code {hex_color}") + + if len(hex_color) not in [7, 4]: + raise ValueError(f"Invalid HEX color code {hex_color}") + hex_color = hex_color.lstrip("#") # Convert the string from hex to an integer and extract each color component @@ -14,7 +20,33 @@ def _hex_to_rgb(hex_color: str) -> tuple[int, int, int]: return r, g, b -def _get_geojson_color(properties: dict[str, str | list[int]]) -> Optional[tuple[int, int, int]]: +def rgb_to_hex(r: int, g: int, b: int) -> str: + """ + Convert RGB color to HEX. + + Parameters + ---------- + r : int + Red value (0-255) + g : int + Green value (0-255) + b : int + Blue value (0-255) + + Returns + ------- + str + HEX color code + """ + # Ensure the RGB values are within the correct range + if not (0 <= r <= 255 and 0 <= g <= 255 and 0 <= b <= 255): + raise ValueError("RGB values must be in the range 0-255.") + + # Convert RGB to HEX + return "#{:02X}{:02X}{:02X}".format(r, g, b) + + +def get_geojson_color(properties: dict[str, str | list[int]]) -> Optional[tuple[int, int, int]]: """Parse the properties dictionary of a GeoJSON object to get the color. Arguments diff --git a/dlup/utils/geometry_xml.py b/dlup/utils/geometry_xml.py new file mode 100644 index 00000000..6dce3a9f --- /dev/null +++ b/dlup/utils/geometry_xml.py @@ -0,0 +1,322 @@ +# Copyright (c) dlup contributors +"""Utilities to convert GeometryCollection objects into XML-like objects""" + +from typing import Any, Optional + +from dlup.geometry import Box, GeometryCollection, Point, Polygon +from dlup.utils.annotations_utils import hex_to_rgb, rgb_to_hex +from dlup.utils.schemas.generated import ( + BasePolygonType, + BoxType, + Geometries, + RegionBoxType, + RegionPolygonType, + RegionsOfInterest, + StandalonePolygonType, +) + + +def _create_base_polygon_type(polygon: Polygon) -> tuple[BasePolygonType.Exterior, BasePolygonType.Interiors]: + """ + Helper function to create the exterior and interiors of a polygon. + + Parameters + ---------- + polygon : Polygon + The Polygon object to convert. + + Returns + ------- + tuple + A tuple containing exterior and interiors. + """ + exterior_coords = [BasePolygonType.Exterior.Point(x=coord[0], y=coord[1]) for coord in polygon.get_exterior()] + exterior = BasePolygonType.Exterior(point=exterior_coords) + + interiors_list = [] + for interior in polygon.get_interiors(): + interior_coords = [BasePolygonType.Interiors.Interior.Point(x=coord[0], y=coord[1]) for coord in interior] + interiors_list.append(BasePolygonType.Interiors.Interior(point=interior_coords)) + interiors = BasePolygonType.Interiors(interior=interiors_list) if interiors_list else BasePolygonType.Interiors() + + return exterior, interiors + + +def is_box(polygon: Polygon) -> bool: + """ + Determines whether a polygon is an axis-aligned box. + + Parameters + ---------- + polygon : Polygon + The Polygon object to check. + + Returns + ------- + bool + True if the polygon is an axis-aligned box, False otherwise. + """ + # Check for no interiors + if polygon.get_interiors(): + return False + + exterior = polygon.get_exterior() + + # A box should have exactly 5 coordinates (first and last are the same) + if len(exterior) != 5: + return False + + # Check that all edges are axis-aligned + for i in range(4): + x1, y1 = exterior[i] + x2, y2 = exterior[i + 1] + if not (x1 == x2 or y1 == y2): + return False + + return True + + +def create_xml_box_from_polygon(polygon: Polygon) -> RegionBoxType: + """ + Convert a Polygon object that is a box into a BoxType XML object. + + Parameters + ---------- + polygon : Polygon + The Polygon object to convert. + + Returns + ------- + BoxType + The converted Box XML object. + """ + exterior = polygon.get_exterior() + x_coords = [coord[0] for coord in exterior[:-1]] # Exclude the closing coordinate + y_coords = [coord[1] for coord in exterior[:-1]] + + x_min = min(x_coords) + y_min = min(y_coords) + x_max = max(x_coords) + y_max = max(y_coords) + + return RegionBoxType( + x_min=x_min, + y_min=y_min, + x_max=x_max, + y_max=y_max, + label=polygon.label, + ) + + +def create_xml_polygon(polygon: Polygon, order: int) -> StandalonePolygonType: + """ + Convert a Polygon object to a StandalonePolygonType XML object. + + Parameters + ---------- + polygon : Polygon + The Polygon object to convert. + order : int + The order of the polygon. + + Returns + ------- + StandalonePolygonType + The converted Polygon XML object. + """ + exterior, interiors = _create_base_polygon_type(polygon) + return StandalonePolygonType( + exterior=exterior, + interiors=interiors, + label=polygon.label, + color=rgb_to_hex(*polygon.color) if polygon.color else None, + index=polygon.index, + order=order, + ) + + +def create_xml_roi_polygon(polygon: Polygon, order: int) -> RegionPolygonType: + """ + Convert a Polygon object to a RegionPolygonType XML object. + + Parameters + ---------- + polygon : Polygon + The Polygon object to convert. + order : int + The order of the polygon. + + Returns + ------- + RegionPolygonType + The converted Polygon XML object. + """ + exterior, interiors = _create_base_polygon_type(polygon) + return RegionPolygonType( + exterior=exterior, + interiors=interiors, + label=polygon.label, + index=polygon.index, + order=order, + ) + + +def create_xml_point(point: Point) -> Geometries.Point: + """ + Convert a Point object to a Point XML object. + + Parameters + ---------- + point : Point + The Point object to convert. + + Returns + ------- + Geometries.Point + The converted Point XML object. + """ + return Geometries.Point( + x=point.x, y=point.y, label=point.label, color=rgb_to_hex(*point.color) if point.color else None + ) + + +def create_xml_box(box: Box) -> BoxType: + """ + Convert a Box object to a BoxType XML object. + + Parameters + ---------- + box : Box + The Box object to convert. + + Returns + ------- + BoxType + The converted Box XML object. + """ + x_min, y_min = box.coordinates + width, height = box.size + x_max = x_min + width + y_max = y_min + height + return BoxType( + x_min=x_min, + y_min=y_min, + x_max=x_max, + y_max=y_max, + label=box.label, + color=rgb_to_hex(*box.color) if box.color else None, + ) + + +def create_xml_geometries(collection: GeometryCollection) -> Geometries: + """ + Convert a GeometryCollection to Geometries XML object. + + Parameters + ---------- + collection : GeometryCollection + The GeometryCollection to convert. + + Returns + ------- + Geometries + The converted Geometries XML object. + """ + polygons = [create_xml_polygon(polygon, order=idx) for idx, polygon in enumerate(collection.polygons)] + points = [create_xml_point(point) for point in collection.points] + boxes = [create_xml_box(box) for box in collection.boxes] + + return Geometries(polygon=polygons, multi_polygon=[], box=boxes, point=points) + + +def create_xml_rois(collection: GeometryCollection) -> RegionsOfInterest: + """ + Convert a GeometryCollection to RegionsOfInterest XML object, distinguishing between polygons and boxes. + + Parameters + ---------- + collection : GeometryCollection + The GeometryCollection to convert. + + Returns + ------- + RegionsOfInterest + The converted RegionsOfInterest XML object, including both polygons and boxes. + """ + polygons = [] + boxes = [] + + for idx, polygon in enumerate(collection.rois): + if is_box(polygon): + boxes.append(create_xml_box_from_polygon(polygon)) + else: + polygons.append(create_xml_roi_polygon(polygon, order=idx)) + + return RegionsOfInterest( + polygon=polygons, box=boxes, multi_polygon=[] # Ensure that RegionsOfInterest schema includes 'box' + ) + + +def parse_dlup_xml_polygon( + polygons: list[Any], order: Optional[int] = None, label: Optional[str] = None, index: Optional[int] = None +) -> list[tuple[Polygon, int]]: + output: list[tuple[Polygon, int]] = [] + for curr_polygon in polygons: + if not curr_polygon.exterior: + raise ValueError("Polygon does not have an exterior.") + exterior = [(point.x, point.y) for point in curr_polygon.exterior.point] + if curr_polygon.interiors: + interiors = [ + [(point.x, point.y) for point in interior.point] for interior in curr_polygon.interiors.interior + ] + else: + interiors = [] + + label = label if label else curr_polygon.label + + polygon = Polygon( + exterior, + interiors, + ) + + order = 0 + if hasattr(curr_polygon, "order"): + if curr_polygon.order is not None: + order = curr_polygon.order + assert isinstance(order, int) + + # We use the given values if they are set, otherwise we take them from the properties + if index is not None: + polygon.index = index + elif hasattr(curr_polygon, "index") and curr_polygon.index is not None: + polygon.index = curr_polygon.index + + if label is not None: + polygon.label = label + elif hasattr(curr_polygon, "label"): + polygon.label = curr_polygon.label + + if hasattr(curr_polygon, "color"): + polygon.color = hex_to_rgb(curr_polygon.color) + + output.append((polygon, order)) + return output + + +def parse_dlup_xml_roi_box(box: RegionBoxType) -> tuple[Box, int]: + # mypy struggles here + assert isinstance(box.x_min, float) + assert isinstance(box.y_min, float) + assert isinstance(box.x_max, float) + assert isinstance(box.y_max, float) + + output_box = Box((box.x_min, box.y_min), (box.x_max - box.x_min, box.y_max - box.y_min)) + + if box.label: + output_box.label = box.label + if box.order: + order = box.order + else: + order = 0 + + return (output_box, order) diff --git a/dlup/utils/imports.py b/dlup/utils/imports.py index 151a6e66..3eb24e4d 100644 --- a/dlup/utils/imports.py +++ b/dlup/utils/imports.py @@ -23,3 +23,4 @@ def _module_available(module_path: str) -> bool: PYTORCH_AVAILABLE = _module_available("pytorch") PYHALOXML_AVAILABLE = _module_available("pyhaloxml") DARWIN_SDK_AVAILABLE = _module_available("darwin") +SHAPELY_AVAILABLE = _module_available("shapely") diff --git a/dlup/utils/schemas/dlup_schema_v1.0.xsd b/dlup/utils/schemas/dlup_schema_v1.0.xsd new file mode 100644 index 00000000..df3f2163 --- /dev/null +++ b/dlup/utils/schemas/dlup_schema_v1.0.xsd @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dlup/utils/schemas/generated/__init__.py b/dlup/utils/schemas/generated/__init__.py new file mode 100644 index 00000000..7616cffa --- /dev/null +++ b/dlup/utils/schemas/generated/__init__.py @@ -0,0 +1,35 @@ +from dlup.utils.schemas.generated.dlup_schema_v1_0 import ( + BasePolygonType, + BoundingBoxType, + BoxType, + DlupAnnotations, + Geometries, + Metadata, + MultiPolygonType, + RectangleType, + RegionBoxType, + RegionMultiPolygonType, + RegionPolygonType, + RegionsOfInterest, + StandalonePolygonType, + Tag, + Tags, +) + +__all__ = [ + "BasePolygonType", + "BoundingBoxType", + "BoxType", + "DlupAnnotations", + "Geometries", + "Metadata", + "MultiPolygonType", + "RectangleType", + "RegionBoxType", + "RegionMultiPolygonType", + "RegionPolygonType", + "RegionsOfInterest", + "StandalonePolygonType", + "Tag", + "Tags", +] diff --git a/dlup/utils/schemas/generated/dlup_schema_v1_0.py b/dlup/utils/schemas/generated/dlup_schema_v1_0.py new file mode 100644 index 00000000..8b1bd018 --- /dev/null +++ b/dlup/utils/schemas/generated/dlup_schema_v1_0.py @@ -0,0 +1,629 @@ +from dataclasses import dataclass, field +from typing import List, Optional + +from xsdata.models.datatype import XmlDate + + +@dataclass +class BasePolygonType: + exterior: Optional["BasePolygonType.Exterior"] = field( + default=None, + metadata={ + "name": "Exterior", + "type": "Element", + "required": True, + }, + ) + interiors: Optional["BasePolygonType.Interiors"] = field( + default=None, + metadata={ + "name": "Interiors", + "type": "Element", + }, + ) + + @dataclass + class Exterior: + point: List["BasePolygonType.Exterior.Point"] = field( + default_factory=list, + metadata={ + "name": "Point", + "type": "Element", + "min_occurs": 1, + }, + ) + + @dataclass + class Point: + x: Optional[float] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + y: Optional[float] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + + @dataclass + class Interiors: + interior: List["BasePolygonType.Interiors.Interior"] = field( + default_factory=list, + metadata={ + "name": "Interior", + "type": "Element", + "min_occurs": 1, + }, + ) + + @dataclass + class Interior: + point: List["BasePolygonType.Interiors.Interior.Point"] = field( + default_factory=list, + metadata={ + "name": "Point", + "type": "Element", + "min_occurs": 1, + }, + ) + + @dataclass + class Point: + x: Optional[float] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + y: Optional[float] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + + +@dataclass +class Metadata: + image_id: Optional[str] = field( + default=None, + metadata={ + "name": "ImageID", + "type": "Element", + "required": True, + }, + ) + description: Optional[str] = field( + default=None, + metadata={ + "name": "Description", + "type": "Element", + }, + ) + version: Optional[str] = field( + default=None, + metadata={ + "name": "Version", + "type": "Element", + "required": True, + }, + ) + authors: Optional["Metadata.Authors"] = field( + default=None, + metadata={ + "name": "Authors", + "type": "Element", + }, + ) + date_created: Optional[XmlDate] = field( + default=None, + metadata={ + "name": "DateCreated", + "type": "Element", + "required": True, + }, + ) + software: Optional[str] = field( + default=None, + metadata={ + "name": "Software", + "type": "Element", + "required": True, + }, + ) + + @dataclass + class Authors: + author: List[str] = field( + default_factory=list, + metadata={ + "name": "Author", + "type": "Element", + "min_occurs": 1, + }, + ) + + +@dataclass +class RectangleType: + x_min: Optional[float] = field( + default=None, + metadata={ + "name": "xMin", + "type": "Attribute", + "required": True, + }, + ) + y_min: Optional[float] = field( + default=None, + metadata={ + "name": "yMin", + "type": "Attribute", + "required": True, + }, + ) + x_max: Optional[float] = field( + default=None, + metadata={ + "name": "xMax", + "type": "Attribute", + "required": True, + }, + ) + y_max: Optional[float] = field( + default=None, + metadata={ + "name": "yMax", + "type": "Attribute", + "required": True, + }, + ) + + +@dataclass +class Tag: + attribute: List["Tag.Attribute"] = field( + default_factory=list, + metadata={ + "name": "Attribute", + "type": "Element", + }, + ) + text: Optional[str] = field( + default=None, + metadata={ + "name": "Text", + "type": "Element", + }, + ) + label: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + color: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "pattern": r"#[0-9a-fA-F]{6}", + }, + ) + + @dataclass + class Attribute: + value: str = field( + default="", + metadata={ + "required": True, + }, + ) + color: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "pattern": r"#[0-9a-fA-F]{6}", + }, + ) + + +@dataclass +class BoundingBoxType(RectangleType): + label: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + }, + ) + color: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "pattern": r"#[0-9a-fA-F]{6}", + }, + ) + index: Optional[int] = field( + default=None, + metadata={ + "type": "Attribute", + }, + ) + + +@dataclass +class BoxType(RectangleType): + label: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + color: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "pattern": r"#[0-9a-fA-F]{6}", + }, + ) + + +@dataclass +class MultiPolygonType: + polygon: List[BasePolygonType] = field( + default_factory=list, + metadata={ + "name": "Polygon", + "type": "Element", + "min_occurs": 1, + }, + ) + label: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + color: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "pattern": r"#[0-9a-fA-F]{6}", + }, + ) + index: Optional[int] = field( + default=None, + metadata={ + "type": "Attribute", + }, + ) + order: Optional[int] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + + +@dataclass +class RegionBoxType(RectangleType): + label: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + index: Optional[int] = field( + default=None, + metadata={ + "type": "Attribute", + }, + ) + order: Optional[int] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + + +@dataclass +class RegionMultiPolygonType: + polygon: List[BasePolygonType] = field( + default_factory=list, + metadata={ + "name": "Polygon", + "type": "Element", + "min_occurs": 1, + }, + ) + label: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + index: Optional[int] = field( + default=None, + metadata={ + "type": "Attribute", + }, + ) + order: Optional[int] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + + +@dataclass +class RegionPolygonType(BasePolygonType): + label: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + index: Optional[int] = field( + default=None, + metadata={ + "type": "Attribute", + }, + ) + order: Optional[int] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + + +@dataclass +class StandalonePolygonType(BasePolygonType): + label: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + color: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "pattern": r"#[0-9a-fA-F]{6}", + }, + ) + index: Optional[int] = field( + default=None, + metadata={ + "type": "Attribute", + }, + ) + order: Optional[int] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + + +@dataclass +class Tags: + tag: List[Tag] = field( + default_factory=list, + metadata={ + "name": "Tag", + "type": "Element", + "min_occurs": 1, + }, + ) + + +@dataclass +class Geometries: + polygon: List[StandalonePolygonType] = field( + default_factory=list, + metadata={ + "name": "Polygon", + "type": "Element", + }, + ) + multi_polygon: List[MultiPolygonType] = field( + default_factory=list, + metadata={ + "name": "MultiPolygon", + "type": "Element", + }, + ) + box: List[BoxType] = field( + default_factory=list, + metadata={ + "name": "Box", + "type": "Element", + }, + ) + bounding_box: Optional[BoundingBoxType] = field( + default=None, + metadata={ + "name": "BoundingBox", + "type": "Element", + }, + ) + point: List["Geometries.Point"] = field( + default_factory=list, + metadata={ + "name": "Point", + "type": "Element", + }, + ) + multi_point: List["Geometries.MultiPoint"] = field( + default_factory=list, + metadata={ + "name": "MultiPoint", + "type": "Element", + }, + ) + + @dataclass + class Point: + x: Optional[float] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + y: Optional[float] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + label: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + color: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "pattern": r"#[0-9a-fA-F]{6}", + }, + ) + + @dataclass + class MultiPoint: + point: List["Geometries.MultiPoint.Point"] = field( + default_factory=list, + metadata={ + "name": "Point", + "type": "Element", + "min_occurs": 1, + }, + ) + label: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + color: Optional[str] = field( + default=None, + metadata={ + "type": "Attribute", + "pattern": r"#[0-9a-fA-F]{6}", + }, + ) + index: Optional[int] = field( + default=None, + metadata={ + "type": "Attribute", + }, + ) + + @dataclass + class Point: + x: Optional[float] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + y: Optional[float] = field( + default=None, + metadata={ + "type": "Attribute", + "required": True, + }, + ) + + +@dataclass +class RegionsOfInterest: + polygon: List[RegionPolygonType] = field( + default_factory=list, + metadata={ + "name": "Polygon", + "type": "Element", + }, + ) + multi_polygon: List[RegionMultiPolygonType] = field( + default_factory=list, + metadata={ + "name": "MultiPolygon", + "type": "Element", + }, + ) + box: List[RegionBoxType] = field( + default_factory=list, + metadata={ + "name": "Box", + "type": "Element", + }, + ) + + +@dataclass +class DlupAnnotations: + metadata: Optional[Metadata] = field( + default=None, + metadata={ + "name": "Metadata", + "type": "Element", + "required": True, + }, + ) + tags: Optional[Tags] = field( + default=None, + metadata={ + "name": "Tags", + "type": "Element", + }, + ) + geometries: Optional[Geometries] = field( + default=None, + metadata={ + "name": "Geometries", + "type": "Element", + "required": True, + }, + ) + regions_of_interest: Optional[RegionsOfInterest] = field( + default=None, + metadata={ + "name": "RegionsOfInterest", + "type": "Element", + }, + ) + version: str = field( + init=False, + default="1.0", + metadata={ + "type": "Attribute", + }, + ) diff --git a/examples/annotations_to_mask.py b/examples/annotations_to_mask.py new file mode 100644 index 00000000..c8c8f35d --- /dev/null +++ b/examples/annotations_to_mask.py @@ -0,0 +1,26 @@ +# Copyright (c) dlup contributors +"""This code provides an example of how to convert annotations to a mask.""" +from pathlib import Path + +import PIL.Image + +from dlup.annotations_experimental import SlideAnnotations + + +def convert_annotations_to_mask() -> None: + scaling = 0.02 + annotations = SlideAnnotations.from_dlup_xml(Path(__file__).parent / "files" / "dlup_annotation_test.xml") + + bbox = annotations.bounding_box_at_scaling(scaling) + + region = annotations.read_region((0, 0), scaling, bbox[1]) + LUT = annotations.color_lut + + bbox = annotations.bounding_box_at_scaling(scaling) + + curr_mask = region.polygons.to_mask().numpy() + print(curr_mask.shape) + PIL.Image.fromarray(LUT[curr_mask]).save("output.png") + + +convert_annotations_to_mask() diff --git a/examples/files/dlup_annotation_test.xml b/examples/files/dlup_annotation_test.xml new file mode 100644 index 00000000..00972a0a --- /dev/null +++ b/examples/files/dlup_annotation_test.xml @@ -0,0 +1,113062 @@ + + + + + + + 2024-09-02 + dlup 0.7.0 + + + + + Optimal staining + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/meson.build b/meson.build index da3db1b8..da01a7cc 100644 --- a/meson.build +++ b/meson.build @@ -1,101 +1,24 @@ project('dlup', 'cpp', 'cython', - version : '0.7.0', - default_options : ['warning_level=3', 'cpp_std=c++17']) + version : '0.7.0', + default_options : ['buildtype=release', 'warning_level=3', 'cpp_std=c++20']) +ninja = find_program('ninja', required : true) py_mod = import('python') py = py_mod.find_installation(pure: false) py_dep = py.dependency() -libtiff_dep = dependency('libtiff-4', required : false) -if not libtiff_dep.found() - libtiff_dep = dependency('tiff', required : false) -endif -if not libtiff_dep.found() - libtiff_dep = cc.find_library('tiff', required : false) -endif -if not libtiff_dep.found() - error('libtiff not found. Please install libtiff development files.') -endif +# Base compiler and linker arguments +cpp_args = ['-O3', '-march=native', '-ffast-math', '-funroll-loops', '-flto', '-pipe', '-fomit-frame-pointer'] +link_args = ['-flto'] -# Check for ZSTD library -zstd_dep = dependency('libzstd', required : false) -have_zstd = zstd_dep.found() -if have_zstd - message('ZSTD support enabled') -else - message('ZSTD support disabled') -endif +# Unity build option +unity_build = true -# Capture the output of the run_command and convert to relative paths -numpy_include = run_command(py, ['-c', ''' -import os -import numpy -print(os.path.relpath(numpy.get_include(), os.getcwd())) -'''], check: true).stdout().strip() -pybind11_include = run_command(py, ['-c', ''' -import os -import pybind11 -print(os.path.relpath(pybind11.get_include(), os.getcwd())) -'''], check: true).stdout().strip() +subdir('third_party') +subdir('src') +subdir('dlup') -# Use the relative paths -incdir_numpy = include_directories(numpy_include) -incdir_pybind11 = include_directories(pybind11_include) - -# Check if the CODECOV_CI environment variable is set to 'true' -codecov_ci = run_command('sh', ['-c', ''' -if [ "$CODECOV_CI" = "true" ]; then - echo "true" -else - echo "false" -fi -'''], check: true).stdout().strip() - -# Conditionally set install_dir based on the CODECOV_CI variable -if codecov_ci == 'true' - install_dir = run_command('sh', ['-c', ''' - if [ -z "$PYTHONPATH" ]; then - python -c "import sysconfig; print(sysconfig.get_path('purelib'))" - else - echo $PYTHONPATH - fi - '''], check: true).stdout().strip() - - # Ensure the install_dir is not empty - if install_dir == '' - error('Could not determine install_dir from PYTHONPATH or sysconfig.get_path().') - endif -else - install_dir = py.get_install_dir(pure: false) -endif - -message('Installing to: ' + install_dir) -install_subdir('dlup', install_dir : install_dir) - -_background = py.extension_module('_background', - 'dlup/_background.pyx', - include_directories : [incdir_numpy], - install : true, - install_dir : install_dir / 'dlup', - cpp_args : ['-O3', '-march=native', '-ffast-math']) - -# Define the base dependencies and compiler arguments -base_deps = [libtiff_dep] -base_cpp_args = ['-std=c++17', '-O3', '-march=native', '-ffast-math'] - -# Add ZSTD support if available -if have_zstd - base_deps += [zstd_dep] - base_cpp_args += ['-DHAVE_ZSTD'] -endif - -# pybind11 extension -_libtiff_tiff_writer = py.extension_module('_libtiff_tiff_writer', - 'src/libtiff_tiff_writer.cpp', - include_directories : [incdir_pybind11], - install : true, - install_dir : install_dir / 'dlup', - cpp_args : base_cpp_args, - dependencies : base_deps) +# Include subdirectories for building +install_subdir('dlup', install_dir : py.get_install_dir()) diff --git a/pyproject.toml b/pyproject.toml index e48d8941..b20e483e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ dependencies = [ "shapely>=2.0.4", "packaging>=24.0", "pybind11>=2.8.0", + "xsdata>=24.7", ] [project.optional-dependencies] @@ -75,8 +76,11 @@ package = 'dlup' "Build" = [ ".spin/cmds.py:build", ".spin/cmds.py:test", + ".spin/cmds.py:coverage", ".spin/cmds.py:mypy", ".spin/cmds.py:lint", + ".spin/cmds.py:format", + ".spin/cmds.py:precommit", ] "Environments" = [ "spin.cmds.meson.run", @@ -112,6 +116,12 @@ exclude = ''' profile = "black" line_length = 120 +[tool.pylint] +disable = [ + "possibly-used-before-assignment", + "import-error", +] + [tool.pylint.format] max-line-length = "120" @@ -127,3 +137,10 @@ max-line-length = 120 [tool.pytest.ini_options] addopts = "--ignore=libvips" + +[tool.coverage.run] +branch = true +parallel = false # To see if there are threading issues + +[tool.coverage.html] +directory = "htmlcov" diff --git a/src/geometry.cpp b/src/geometry.cpp new file mode 100644 index 00000000..9749847b --- /dev/null +++ b/src/geometry.cpp @@ -0,0 +1,41 @@ +#include +#include +#include + +#include "geometry/base.h" +#include "geometry/box.h" +#include "geometry/collection.h" +#include "geometry/exceptions.h" +#include "geometry/factory.h" +#include "geometry/lazy_array.h" +#include "geometry/point.h" +#include "geometry/polygon.h" +#include "geometry/region.h" +namespace py = pybind11; + +template class FactoryManager; +template class FactoryManager; +template class FactoryManager; + +PYBIND11_MODULE(_geometry, m) { + declare_base_geometry(m); + declare_polygon(m); + declare_box(m); + declare_point(m); + + m.def("set_polygon_factory", &FactoryManager::setFactory, "Set the factory function for Polygons"); + m.def("set_box_factory", &FactoryManager::setFactory, "Set the factory function for Boxes"); + m.def("set_point_factory", &FactoryManager::setFactory, "Set the factory function for Points"); + + declare_pybind_collection(m); + declare_lazy_array(m, "LazyArrayInt"); + declare_pybind_polygon_collection(m); + declare_pybind_region(m); + + py::register_exception(m, "GeometryError"); + py::register_exception(m, "GeometryIntersectionError"); + py::register_exception(m, "GeometryTransformationError"); + py::register_exception(m, "GeometryFactoryFunctionError"); + py::register_exception(m, "GeometryNotFoundError"); + py::register_exception(m, "GeometryCoordinatesError"); +} diff --git a/src/geometry/base.h b/src/geometry/base.h new file mode 100644 index 00000000..79b85e47 --- /dev/null +++ b/src/geometry/base.h @@ -0,0 +1,65 @@ +#ifndef DLUP_GEOMETRY_BASE_H +#define DLUP_GEOMETRY_BASE_H +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bg = boost::geometry; +namespace py = pybind11; + +using BoostPoint = bg::model::d2::point_xy; +using BoostPolygon = bg::model::polygon; +using BoostRing = bg::model::ring; + +class BaseGeometry { + public: + virtual ~BaseGeometry() = default; + std::unordered_map parameters_; + + virtual void setField(const std::string &name, py::object value) { parameters_[name] = value; } + + std::optional getField(const std::string &name) const { + if (auto it = parameters_.find(name); it != parameters_.end()) { + return it->second; + } + return std::nullopt; + } + + auto getFields() const { + std::vector field_names_; + field_names_.reserve(parameters_.size()); + std::transform(parameters_.begin(), parameters_.end(), std::back_inserter(field_names_), + [](const auto ¶m) { return param.first; }); + return field_names_; + } + + std::uintptr_t getPointerId() const { return reinterpret_cast(this); } + virtual std::string toWkt() const = 0; // Force derived classes to provide the WKT + + template + std::string convertToWkt(const GeometryType &geometry) const { + std::stringstream ss; + ss << boost::geometry::wkt(geometry); + return ss.str(); + } + + protected: +}; + +inline void declare_base_geometry(py::module &m) { + py::class_>(m, "BaseGeometry") + .def("set_field", &BaseGeometry::setField) + .def("get_field", &BaseGeometry::getField) + .def_property_readonly("fields", &BaseGeometry::getFields) + .def_property_readonly("pointer_id", &BaseGeometry::getPointerId); +} + +#endif // DLUP_GEOMETRY_BASE_H diff --git a/src/geometry/box.h b/src/geometry/box.h new file mode 100644 index 00000000..889afa9d --- /dev/null +++ b/src/geometry/box.h @@ -0,0 +1,105 @@ +#ifndef DLUP_GEOMETRY_BOX_H +#define DLUP_GEOMETRY_BOX_H +#pragma once + +#include "exceptions.h" +#include "factory.h" +#include "polygon.h" +#include "utilities.h" +#include +#include + +namespace bg = boost::geometry; + +using BoostPoint = bg::model::d2::point_xy; +using BoostBox = bg::model::box; + +class Box : public BaseGeometry { + public: + ~Box() override = default; + std::shared_ptr box_; + + Box() : box_(std::make_shared()) {} + Box(const BoostBox &p) : box_(std::make_shared(p)) {} + Box(std::shared_ptr p) : box_(p) {} + + // TODO: This create a list, not a tuple. + Box(const std::array &coordinates, const std::array &size) + : box_(std::make_shared()) { + setBoxParameters(std::move(coordinates), std::move(size)); + } + + void setBoxParameters(const std::array &coordinates, const std::array &size) { + bg::set(*box_, coordinates[0]); + bg::set(*box_, coordinates[1]); + bg::set(*box_, coordinates[0] + size[0]); + bg::set(*box_, coordinates[1] + size[1]); + } + + inline std::array getCoordinates() const { + return {bg::get(*box_), bg::get(*box_)}; + } + + inline std::array getSize() const { + auto x1 = bg::get(*box_); + auto y1 = bg::get(*box_); + auto x2 = bg::get(*box_); + auto y2 = bg::get(*box_); + + return {x2 - x1, y2 - y1}; + } + + inline double getArea() const { + std::array size = getSize(); + return size[0] * size[1]; + } + std::shared_ptr asPolygon() const { + BoostPolygon poly; + bg::convert(*box_, poly); + std::shared_ptr polygon = std::make_shared(poly); + for (const auto ¶m : parameters_) { + polygon->setField(param.first, param.second); + } + + return polygon; + } + + std::vector> getExterior() const { return asPolygon()->getExterior(); } + + inline py::object asPolygonPyObject() const { return FactoryManager::callFactoryFunction(asPolygon()); } + + void scale(double scaling) { utilities::AffineTransform(*box_, {0.0, 0.0}, scaling); } + + // Factory function for creating boxes from Python + static std::shared_ptr create(std::array coordinates, std::array size) { + return std::make_shared(coordinates, size); + } + std::string toWkt() const override { return convertToWkt(*box_); } +}; + +inline void declare_box(py::module &m) { + py::class_>(m, "Box") + .def(py::init<>()) + .def(py::init()) + .def(py::init &, const std::array &>()) + .def(py::init([](const std::shared_ptr &p) { + // Share the same C++ object, not creating a new one + return p; + })) + .def(py::init([](const Box &other) { + // Explicitly copy parameters when copying the Box + auto newBox = std::make_shared(*other.box_); + newBox->parameters_ = other.parameters_; // Copy the parameters + return newBox; + })) + .def("as_polygon", &Box::asPolygonPyObject, "Convert the box to a polygon") + .def("scale", &Box::scale, py::arg("scaling"), "Scale the box in-place by a factor") + + .def_property_readonly("coordinates", &Box::getCoordinates, + "Get the top-left coordinates of the box as an (x, y) tuple") + .def_property_readonly("size", &Box::getSize, "Get the size of the box as an (h, w) tuple") + .def_property_readonly("area", &Box::getArea) + .def_property_readonly("wkt", &Box::toWkt, "Get the WKT representation of the box"); +} + +#endif // DLUP_GEOMETRY_BOX_H \ No newline at end of file diff --git a/src/geometry/collection.h b/src/geometry/collection.h new file mode 100644 index 00000000..8870c14d --- /dev/null +++ b/src/geometry/collection.h @@ -0,0 +1,524 @@ +#ifndef DLUP_GEOMETRY_COLLECTION_H +#define DLUP_GEOMETRY_COLLECTION_H +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../opencv.h" +#include "base.h" +#include "box.h" +#include "exceptions.h" +#include "factory.h" +#include "point.h" +#include "polygon.h" +#include "region.h" +#include "rtree.h" +#include "utilities.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bg = boost::geometry; +namespace bgi = boost::geometry::index; +namespace py = pybind11; + +using BoostPoint = bg::model::d2::point_xy; +using BoostPolygon = bg::model::polygon; +using BoostBox = bg::model::box; +using BoostRing = bg::model::ring; +using BoostLineString = bg::model::linestring; +using BoostMultiPolygon = bg::model::multi_polygon; + +namespace py = pybind11; + +class GeometryCollection; // Forward declaration of GeometryCollection + +class RTreeWrapper : public RTreeBase { + public: + explicit RTreeWrapper(GeometryCollection *geometryCollection) : geometryCollection(geometryCollection) {} + + void rebuild() override; + + private: + GeometryCollection *geometryCollection; // Pointer to GeometryCollection +}; + +class GeometryCollection { + public: + GeometryCollection(); + // Whatever any LLM says this has to be a shared pointer as we share it with the python interpreter + using PolygonPtr = std::shared_ptr; + using PointPtr = std::shared_ptr; + using BoxPtr = std::shared_ptr; + + void addPolygon(const PolygonPtr &p); + void addRoi(const PolygonPtr &p); + void addPoint(const PointPtr &p); + void addBox(const BoxPtr &p); + + py::list getPolygons(); + py::list getRois(); + py::list getPoints(); + py::list getBoxes(); + std::pair, std::pair> computeBoundingBox() const; + void sortPolygons(const py::function &keyFunc, bool reverse); + + void removePolygon(const PolygonPtr &p); + void removePolygon(size_t index); + void removeRoi(const PolygonPtr &p); + void removeRoi(size_t index); + void removePoint(const PointPtr &p); + void removePoint(size_t index); + void removeBox(const BoxPtr &p); + void removeBox(size_t index); + + void scale(double scaling); + void setOffset(std::pair offset); + void rebuildRTree() { rtree_wrapper_.rebuild(); } + void simplifyPolygons(double tolerance) { + std::lock_guard lock(collection_mutex_); + for (auto &polygon : polygons_) { + polygon->simplifyPolygon(tolerance); + } + } + + int size() const { return polygons_.size() + rois_.size() + points_.size() + boxes_.size(); } + + std::uintptr_t getPointerId() const { return reinterpret_cast(this); } + + bool isRTreeInvalidated() const { return rtree_wrapper_.isInvalidated(); } + + AnnotationRegion readRegion(const std::pair &coordinates, double scaling, + const std::pair &size); + + // TODO: Rethink the need for this function. + void reindexPolygons(const std::map &indexMap); + + private: + friend class RTreeWrapper; + std::vector polygons_; + std::vector rois_; + std::vector points_; + std::vector boxes_; + RTreeWrapper rtree_wrapper_; + mutable std::mutex collection_mutex_; +}; + +GeometryCollection::GeometryCollection() : rtree_wrapper_(this) {} + +std::pair, std::pair> GeometryCollection::computeBoundingBox() const { + std::lock_guard lock(collection_mutex_); + BoostBox overall_bounding_box_; + bool is_first_ = true; + + // Iterate over all polygons and compute their bounding boxes + for (const auto &polygon : polygons_) { + BoostBox polygon_box; + bg::envelope(*(polygon->polygon_), polygon_box); + + if (is_first_) { + overall_bounding_box_ = polygon_box; + is_first_ = false; + } else { + bg::expand(overall_bounding_box_, polygon_box); + } + } + + // Iterate over the ROIs and compute their bounding boxes + for (const auto &roi : rois_) { + BoostBox roi_box; + bg::envelope(*(roi->polygon_), roi_box); + + if (is_first_) { + overall_bounding_box_ = roi_box; + is_first_ = false; + } else { + bg::expand(overall_bounding_box_, roi_box); + } + } + + // Iterate over all boxes and compute their bounding boxes + for (const auto &box : boxes_) { + if (is_first_) { + overall_bounding_box_ = *(box->box_); + is_first_ = false; + } else { + bg::expand(overall_bounding_box_, *(box->box_)); + } + } + + // Iterate over all points and compute their bounding boxes + for (const auto &point : points_) { + BoostBox point_box(*(point->point_), *(point->point_)); + + if (is_first_) { + overall_bounding_box_ = point_box; + is_first_ = false; + } else { + bg::expand(overall_bounding_box_, point_box); + } + } + + // Extract min and max points + const auto &min_corner = overall_bounding_box_.min_corner(); + const auto &max_corner = overall_bounding_box_.max_corner(); + + double min_x = bg::get<0>(min_corner); + double min_y = bg::get<1>(min_corner); + double max_x = bg::get<0>(max_corner); + double max_y = bg::get<1>(max_corner); + + double width = max_x - min_x; + double height = max_y - min_y; + + return std::make_pair(std::make_pair(min_x, min_y), std::make_pair(width, height)); +} + +void GeometryCollection::reindexPolygons(const std::map &indexMap) { + std::lock_guard lock(collection_mutex_); + for (auto &polygon : polygons_) { + std::optional label_opt = polygon->getField("label"); + + if (label_opt.has_value()) { + std::string label = label_opt->cast(); + auto it = indexMap.find(label); + if (it != indexMap.end()) { + polygon->setField("index", py::int_(it->second)); + } else { + throw std::invalid_argument("Label '" + label + "' not found in indexMap"); + } + } else { + throw std::invalid_argument("Polygon does not have a value for the 'label' field"); + } + } +} + +void RTreeWrapper::rebuild() { + // Mutex is handled in the wrapper + clear(); // Clear the existing R-tree + + // Rebuild the tree using polygons, boxes, and points from GeometryCollection + + // First insert polygons + const auto &polygons = geometryCollection->polygons_; + for (size_t i = 0; i < polygons.size(); ++i) { + BoostBox box; + bg::envelope(*(polygons[i]->polygon_), box); + insert(box, i); + } + + // Next insert ROIs + const auto &rois = geometryCollection->rois_; + for (size_t i = 0; i < rois.size(); ++i) { + BoostBox box; + bg::envelope(*(rois[i]->polygon_), box); + insert(box, polygons.size() + i); + } + + // Next insert boxes + const auto &boxes = geometryCollection->boxes_; + for (size_t i = 0; i < boxes.size(); ++i) { + insert(*(boxes[i]->box_), polygons.size() + rois.size() + i); + } + + // Finally, insert points + const auto &points = geometryCollection->points_; + for (size_t i = 0; i < points.size(); ++i) { + BoostBox box(*(points[i]->point_), *(points[i]->point_)); + insert(box, polygons.size() + rois.size() + boxes.size() + i); + } + + rtree_invalidated_ = false; +} + +void GeometryCollection::addPolygon(const PolygonPtr &p) { + std::lock_guard lock(collection_mutex_); + polygons_.emplace_back(p); + rtree_wrapper_.invalidate(); +} + +void GeometryCollection::addRoi(const PolygonPtr &p) { + std::lock_guard lock(collection_mutex_); + rois_.emplace_back(p); + rtree_wrapper_.invalidate(); +} + +void GeometryCollection::addPoint(const PointPtr &p) { + std::lock_guard lock(collection_mutex_); + points_.emplace_back(p); + rtree_wrapper_.invalidate(); +} + +void GeometryCollection::addBox(const BoxPtr &p) { + std::lock_guard lock(collection_mutex_); + boxes_.emplace_back(p); + rtree_wrapper_.invalidate(); +} + +py::list GeometryCollection::getPolygons() { + std::lock_guard lock(collection_mutex_); + py::list py_polygons; + for (const auto &polygon : polygons_) { + py::object processed_polygon = FactoryManager::callFactoryFunction(polygon); + py_polygons.append(processed_polygon); + } + return py_polygons; +} + +py::list GeometryCollection::getRois() { + std::lock_guard lock(collection_mutex_); + py::list py_rois; + for (const auto &roi : rois_) { + py::object processed_roi = FactoryManager::callFactoryFunction(roi); + py_rois.append(processed_roi); + } + return py_rois; +} + +py::list GeometryCollection::getPoints() { + std::lock_guard lock(collection_mutex_); + py::list py_points; + for (const auto &point : points_) { + py_points.append(FactoryManager::callFactoryFunction(point)); + } + return py_points; +} + +py::list GeometryCollection::getBoxes() { + std::lock_guard lock(collection_mutex_); + py::list py_boxes; + for (const auto &box : boxes_) { + py_boxes.append(FactoryManager::callFactoryFunction(box)); + } + return py_boxes; +} + +void GeometryCollection::sortPolygons(const py::function &key_func, bool reverse) { + std::lock_guard lock(collection_mutex_); + std::sort(polygons_.begin(), polygons_.end(), [&key_func, reverse](const PolygonPtr &a, const PolygonPtr &b) { + py::object key_a = key_func(a); + py::object key_b = key_func(b); + + if (py::isinstance(key_a) && py::isinstance(key_b)) { + return reverse ? (key_a.cast() > key_b.cast()) + : (key_a.cast() < key_b.cast()); + } else if (py::isinstance(key_a) && py::isinstance(key_b)) { + return reverse ? (key_a.cast() > key_b.cast()) : (key_a.cast() < key_b.cast()); + } else if (py::isinstance(key_a) && py::isinstance(key_b)) { + return reverse ? (key_a.cast() > key_b.cast()) : (key_a.cast() < key_b.cast()); + } else if (py::isinstance(key_a) && py::isinstance(key_b)) { + return false; + } else { + throw std::invalid_argument("Unsupported key type for sorting."); + } + }); + rtree_wrapper_.invalidate(); +} + +void GeometryCollection::scale(double scaling) { + std::lock_guard lock(collection_mutex_); + for (auto &point : points_) { + point->scale(scaling); + } + for (auto &polygon : polygons_) { + polygon->scale(scaling); + } + + for (auto &roi : rois_) { + roi->scale(scaling); + } + + for (auto &box : boxes_) { + box->scale(scaling); + } + rtree_wrapper_.invalidate(); +} + +void GeometryCollection::setOffset(std::pair offset) { + std::lock_guard lock(collection_mutex_); + for (auto &point : points_) { + utilities::AffineTransform(*point->point_, {-offset.first, -offset.second}, 1.0); + } + for (auto &polygon : polygons_) { + utilities::AffineTransform(*polygon->polygon_, {-offset.first, -offset.second}, 1.0); + } + for (auto &roi : rois_) { + utilities::AffineTransform(*roi->polygon_, {-offset.first, -offset.second}, 1.0); + } + for (auto &box : boxes_) { + utilities::AffineTransform(*box->box_, {-offset.first, -offset.second}, 1.0); + } + + rtree_wrapper_.invalidate(); +} + +void GeometryCollection::removePolygon(const PolygonPtr &p) { + std::lock_guard lock(collection_mutex_); + auto it = std::find(polygons_.begin(), polygons_.end(), p); + if (it != polygons_.end()) { + polygons_.erase(it); + rtree_wrapper_.invalidate(); + } else { + throw GeometryNotFoundError("Polygon not found"); + } +} + +void GeometryCollection::removePolygon(size_t index) { + std::lock_guard lock(collection_mutex_); + if (index >= polygons_.size()) { + throw std::out_of_range("Polygon index out of range"); + } + + polygons_.erase(polygons_.begin() + index); + rtree_wrapper_.invalidate(); +} + +void GeometryCollection::removeRoi(const PolygonPtr &p) { + std::lock_guard lock(collection_mutex_); + auto it = std::find(rois_.begin(), rois_.end(), p); + if (it != rois_.end()) { + rois_.erase(it); + rtree_wrapper_.invalidate(); + } else { + throw GeometryNotFoundError("ROI not found"); + } +} + +void GeometryCollection::removeRoi(size_t index) { + std::lock_guard lock(collection_mutex_); + if (index >= rois_.size()) { + throw std::out_of_range("ROI index out of range"); + } + + rois_.erase(rois_.begin() + index); + rtree_wrapper_.invalidate(); +} + +void GeometryCollection::removePoint(const PointPtr &p) { + std::lock_guard lock(collection_mutex_); + auto it = std::find(points_.begin(), points_.end(), p); + if (it != points_.end()) { + points_.erase(it); + rtree_wrapper_.invalidate(); + } else { + throw GeometryNotFoundError("Point not found"); + } +} + +void GeometryCollection::removePoint(size_t index) { + std::lock_guard lock(collection_mutex_); + if (index >= points_.size()) { + throw std::out_of_range("Point index out of range"); + } + + points_.erase(points_.begin() + index); + rtree_wrapper_.invalidate(); +} + +AnnotationRegion GeometryCollection::readRegion(const std::pair &coordinates, double scaling, + const std::pair &size) { + return AnnotationRegion([=, this]() { + std::lock_guard lock(collection_mutex_); + if (rtree_wrapper_.isInvalidated()) { + rtree_wrapper_.rebuild(); + } + + BoostPoint top_left(coordinates.first / scaling, coordinates.second / scaling); + BoostPoint bottom_right((coordinates.first + size.first) / scaling, (coordinates.second + size.second) / scaling); + BoostBox query_box(top_left, bottom_right); + + BoostPolygon intersection_polygon; + bg::convert(query_box, intersection_polygon); + std::vector> results; + rtree_wrapper_.query(bgi::intersects(query_box), std::back_inserter(results)); + + std::sort(results.begin(), results.end(), [](const auto &a, const auto &b) { return a.second < b.second; }); + + std::vector> intersected_polygons; + std::vector> intersected_rois; + std::vector> current_points; + std::vector> current_boxes; + + for (const auto &result : results) { + size_t index = result.second; + if (index < polygons_.size()) { + auto &polygon = polygons_[index]; + auto intersections = polygon->intersection(intersection_polygon); + for (const auto &intersected_polygon : intersections) { + utilities::AffineTransform(*intersected_polygon->polygon_, coordinates, scaling); + intersected_polygons.push_back(intersected_polygon); + } + } else if (index < polygons_.size() + boxes_.size()) { + auto &roi = rois_[index - polygons_.size()]; + auto intersection = roi->intersection(intersection_polygon); + for (const auto &intersected_roi : intersection) { + utilities::AffineTransform(*intersected_roi->polygon_, coordinates, scaling); + intersected_rois.push_back(intersected_roi); + } + } else if (index < polygons_.size() + rois_.size() + boxes_.size()) { + auto &box = boxes_[index - polygons_.size() - rois_.size()]; + auto transformed_box = std::make_shared(*box); + utilities::AffineTransform(*transformed_box->box_, coordinates, scaling); + current_boxes.push_back(transformed_box); + } else { + auto &point = points_[index - polygons_.size() - rois_.size() - boxes_.size()]; + auto transformed_point = std::make_shared(*point); + utilities::AffineTransform(*transformed_point->point_, coordinates, scaling); + current_points.push_back(transformed_point); + } + } + + return AnnotationRegion(std::move(intersected_polygons), std::move(intersected_rois), std::move(current_boxes), + std::move(current_points), std::make_tuple(size.first, size.second)); + }); +} + +void declare_pybind_collection(py::module &m) { + py::class_>(m, "GeometryCollection") + .def(py::init<>()) + .def("add_polygon", &GeometryCollection::addPolygon) + .def("add_roi", &GeometryCollection::addRoi) + .def("add_point", &GeometryCollection::addPoint) + .def("add_box", &GeometryCollection::addBox) + + // Overload remove_polygon to handle both object and index + .def("remove_polygon", py::overload_cast &>(&GeometryCollection::removePolygon), + "Remove a polygon by passing the Polygon object") + .def("remove_polygon", py::overload_cast(&GeometryCollection::removePolygon), + "Remove a polygon by its index") + .def("remove_roi", py::overload_cast &>(&GeometryCollection::removeRoi), + "Remove an ROI by passing the ROI object") + .def("remove_roi", py::overload_cast(&GeometryCollection::removeRoi), "Remove an ROI by its index") + .def("reindex_polygons", &GeometryCollection::reindexPolygons) + .def("sort_polygons", &GeometryCollection::sortPolygons, "Sort polygons by a custom key function") + .def("simplify_polygons", &GeometryCollection::simplifyPolygons) + .def("size", &GeometryCollection::size) + + // Overload remove_point to handle both object and index + .def("remove_point", py::overload_cast &>(&GeometryCollection::removePoint), + "Remove a point by passing the Point object") + .def("remove_point", py::overload_cast(&GeometryCollection::removePoint), "Remove a point by its index") + .def("read_region", &GeometryCollection::readRegion) + .def("rebuild_rtree", &GeometryCollection::rebuildRTree, "Rebuild the R-tree index manually") + .def("scale", &GeometryCollection::scale, "Scale all geometries by a factor") + .def("set_offset", &GeometryCollection::setOffset, "Set an offset for all geometries") + .def_property_readonly("rtree_invalidated", &GeometryCollection::isRTreeInvalidated) + .def_property_readonly("pointer_id", &GeometryCollection::getPointerId) + .def_property_readonly("bounding_box", &GeometryCollection::computeBoundingBox) + .def_property_readonly("polygons", &GeometryCollection::getPolygons) + .def_property_readonly("rois", &GeometryCollection::getRois) + .def_property_readonly("boxes", &GeometryCollection::getBoxes) + .def_property_readonly("points", &GeometryCollection::getPoints); +} + +#endif // DLUP_GEOMETRY_COLLECTION_H diff --git a/src/geometry/exceptions.h b/src/geometry/exceptions.h new file mode 100644 index 00000000..41b2e814 --- /dev/null +++ b/src/geometry/exceptions.h @@ -0,0 +1,42 @@ +#ifndef DLUP_GEOMETRY_EXCEPTIONS_H +#define DLUP_GEOMETRY_EXCEPTIONS_H + +#include +#include + +class GeometryError : public std::runtime_error { + public: + explicit GeometryError(const std::string &message) : std::runtime_error(message) {} +}; + +class GeometryNotFoundError : public GeometryError { + public: + explicit GeometryNotFoundError(const std::string &message) : GeometryError(message) {} +}; + +class GeometryCoordinatesError : public GeometryError { + public: + explicit GeometryCoordinatesError(const std::string &message) : GeometryError(message) {} +}; + +class GeometryIntersectionError : public GeometryError { + public: + explicit GeometryIntersectionError(const std::string &message) : GeometryError(message) {} +}; + +class GeometryTransformationError : public GeometryError { + public: + explicit GeometryTransformationError(const std::string &message) : GeometryError(message) {} +}; + +class GeometryFactoryFunctionError : public GeometryError { + public: + explicit GeometryFactoryFunctionError(const std::string &message) : GeometryError(message) {} +}; + +class GeometryInvalidPolygonError : public GeometryError { + public: + explicit GeometryInvalidPolygonError(const std::string &message) : GeometryError(message) {} +}; + +#endif // DLUP_GEOMETRY_EXCEPTIONS_H diff --git a/src/geometry/factory.h b/src/geometry/factory.h new file mode 100644 index 00000000..50ec4c0d --- /dev/null +++ b/src/geometry/factory.h @@ -0,0 +1,69 @@ +#ifndef DLUP_GEOMETRY_FACTORY_H +#define DLUP_GEOMETRY_FACTORY_H +#pragma once + +#include +#include +#include + +// FactoryGuard class definition +class FactoryGuard { + public: + FactoryGuard(py::function &factory_ref, py::function new_factory) + : factory_ref_(factory_ref), original_factory_(factory_ref) { + factory_ref_ = new_factory; + } + + ~FactoryGuard() { factory_ref_ = original_factory_; } + + private: + py::function &factory_ref_; + py::function original_factory_; +}; + +// Template class to manage factory functions +template +class FactoryManager { + public: + static void setFactory(py::function factory) { factoryFunction() = std::move(factory); } + + static py::object callFactoryFunction(const std::shared_ptr &object) { + return invokeFactoryFunction(factoryFunction(), object); + } + + static FactoryGuard createFactoryGuard(py::function factory) { return FactoryGuard(factoryFunction(), factory); } + + // New method to streamline setting factories and creating guards + template + static void setAndCreateFactoryGuard(py::function factory) { + setFactory(factory); + createFactoryGuard(factory); + } + + private: + static py::function &factoryFunction() { + static py::function instance; + return instance; + } + + static py::object invokeFactoryFunction(py::function factoryFunction, const std::shared_ptr &object) { + if (!factoryFunction || !PyCallable_Check(factoryFunction.ptr())) { + return py::cast(object); + } + + try { + py::object result = factoryFunction(object); + if (!result.is_none()) { + return result; + } else { + throw std::runtime_error("Factory function returned null object"); + } + } catch (const std::exception &e) { + throw std::runtime_error(std::string("Exception in factory function: ") + e.what()); + } catch (...) { + throw std::runtime_error("Unknown exception in factory function"); + } + } +}; + +#endif // DLUP_GEOMETRY_FACTORY_H diff --git a/src/geometry/lazy_array.h b/src/geometry/lazy_array.h new file mode 100644 index 00000000..cb30d78c --- /dev/null +++ b/src/geometry/lazy_array.h @@ -0,0 +1,91 @@ +#ifndef DLUP_GEOMETRY_LAZY_ARRAY_H +#define DLUP_GEOMETRY_LAZY_ARRAY_H + +#include +#include +#include +#include +#include + +namespace py = pybind11; + +template +class LazyArray { + public: + using ComputeFunction = std::function()>; + + LazyArray(ComputeFunction compute_func, std::vector shape) + : compute_func_(std::move(compute_func)), computed_(false), shape_(std::move(shape)) {} + + py::array_t numpy() const { + if (!computed_) { + data_ = compute_func_(); + computed_ = true; + } + return data_; + } + + py::array_t operator*() const { return numpy(); } + + py::array_t py_numpy() const { return numpy(); } + + std::vector shape() const { return shape_; } + + LazyArray transpose(const std::vector &axes = {}) const { + std::vector new_shape(shape_); + if (axes.empty()) { + std::reverse(new_shape.begin(), new_shape.end()); + } else { + for (size_t i = 0; i < axes.size(); ++i) { + new_shape[i] = shape_[axes[i]]; + } + } + + return LazyArray( + [this, axes]() { return this->numpy().attr("transpose")(axes).template cast>(); }, new_shape); + } + + LazyArray reshape(const std::vector &new_shape) const { + return LazyArray([this, new_shape]() { return this->numpy().reshape(new_shape); }, new_shape); + } + + LazyArray operator+(const LazyArray &other) const { + return LazyArray([this, &other]() { return this->numpy() + other.numpy(); }, shape_); + } + + LazyArray operator-(const LazyArray &other) const { + return LazyArray([this, &other]() { return this->numpy() - other.numpy(); }, shape_); + } + + LazyArray multiply(const LazyArray &other) const { + return LazyArray([this, &other]() { return this->numpy() * other.numpy(); }, shape_); + } + + LazyArray operator/(const LazyArray &other) const { + return LazyArray([this, &other]() { return this->numpy() / other.numpy(); }, shape_); + } + + private: + ComputeFunction compute_func_; + mutable py::array_t data_; + mutable bool computed_; + std::vector shape_; +}; + +template +void declare_lazy_array(py::module &m, const std::string &type_name) { + py::class_>(m, type_name.c_str()) + .def(py::init::ComputeFunction, std::vector>()) + .def("numpy", &LazyArray::py_numpy) + .def("shape", &LazyArray::shape) + .def("transpose", &LazyArray::transpose, py::arg("axes") = std::vector()) + .def("reshape", &LazyArray::reshape) + .def("__array__", &LazyArray::py_numpy) + .def("__repr__", [](const LazyArray &) { return ""; }) + .def("__add__", &LazyArray::operator+) + .def("__sub__", &LazyArray::operator-) + .def("__mul__", &LazyArray::multiply) + .def("__truediv__", &LazyArray::operator/); +} + +#endif // DLUP_GEOMETRY_LAZY_ARRAY_H diff --git a/src/geometry/point.h b/src/geometry/point.h new file mode 100644 index 00000000..f1f32475 --- /dev/null +++ b/src/geometry/point.h @@ -0,0 +1,74 @@ +#ifndef DLUP_GEOMETRY_POINT_H +#define DLUP_GEOMETRY_POINT_H +#pragma once + +#include + +namespace bg = boost::geometry; + +using BoostPoint = bg::model::d2::point_xy; + +class Point : public BaseGeometry { + public: + ~Point() override = default; + std::shared_ptr point_; + + Point() : point_(std::make_shared()) {} + Point(const BoostPoint &p) : point_(std::make_shared(p)) {} + Point(std::shared_ptr p) : point_(p) {} + Point(double x, double y) : point_(std::make_shared(x, y)) {} + + Point(const Point &other) : BaseGeometry(other), point_(std::make_shared(*other.point_)) { + parameters_ = other.parameters_; // Copy parameters + } + + // Factory function for creating points from Python + static std::shared_ptr create(double x, double y) { return std::make_shared(x, y); } + std::pair getCoordinates() const { return std::make_pair(bg::get<0>(*point_), bg::get<1>(*point_)); } + std::string toWkt() const override { return convertToWkt(*point_); } + + inline double getX() const { return bg::get<0>(*point_); } + inline double getY() const { return bg::get<1>(*point_); } + double distanceTo(const Point &other) const { return bg::distance(*point_, *(other.point_)); } + bool equals(const Point &other) const { + bool pointEqual = bg::equals(*point_, *(other.point_)); + return parameters_ == other.parameters_ && pointEqual; + } + bool within(const Polygon &polygon) const { return bg::within(*point_, *(polygon.polygon_)); } + + void scale(double scaling) { setCoordinates(getX() * scaling, getY() * scaling); } + + private: + void setCoordinates(double x, double y) { + bg::set<0>(*point_, x); + bg::set<1>(*point_, y); + } +}; + +inline void declare_point(py::module &m) { + py::class_>(m, "Point") + .def(py::init<>()) + .def(py::init()) + .def(py::init()) + .def(py::init([](const std::shared_ptr &p) { + // Share the same C++ object, not creating a new one + return p; + })) + .def(py::init([](const Point &other) { + // Explicitly copy parameters when copying the polygon + auto newPoint = std::make_shared(*other.point_); + newPoint->parameters_ = other.parameters_; // Copy the parameters + return newPoint; + })) + .def_property_readonly("coordinates", &Point::getCoordinates, + "Get the coordinates of the point as an (x, y) tuple") + .def_property_readonly("x", &Point::getX, "Get the X coordinate") + .def_property_readonly("y", &Point::getY, "Get the Y coordinate") + .def("distance_to", &Point::distanceTo, py::arg("other"), "Calculate the distance to another point") + .def("equals", &Point::equals, py::arg("other"), "Check if the point is equal to another point") + .def("within", &Point::within, py::arg("polygon"), "Check if the point is within a polygon") + .def("scale", &Point::scale, py::arg("scaling"), "Scale the point in-place point by a factor") + .def_property_readonly("wkt", &Point::toWkt, "Get the WKT representation of the point"); +} + +#endif // DLUP_GEOMETRY_POINT_H \ No newline at end of file diff --git a/src/geometry/polygon.h b/src/geometry/polygon.h new file mode 100644 index 00000000..ffd43e91 --- /dev/null +++ b/src/geometry/polygon.h @@ -0,0 +1,216 @@ + +#ifndef DLUP_GEOMETRY_POLYGON_H +#define DLUP_GEOMETRY_POLYGON_H +#pragma once + +#include "utilities.h" +#include +#include +#include + +namespace bg = boost::geometry; + +using BoostPoint = bg::model::d2::point_xy; +using BoostPolygon = bg::model::polygon; +using BoostRing = bg::model::ring; + +class Polygon : public BaseGeometry { + public: + using ExteriorRing = std::vector &; + using InteriorRings = std::vector &; + + ~Polygon() override = default; + std::shared_ptr polygon_; + + Polygon() : polygon_(std::make_shared()) {} + Polygon(const BoostPolygon &p) : polygon_(std::make_shared(p)) {} + // This doesn't work, but is probably + // Polygon(BoostPolygon &&p) : polygon(std::make_shared(std::move(p))) {} + Polygon(std::shared_ptr p) : polygon_(p) {} + + Polygon(const std::vector> &exterior, + const std::vector>> &interiors = {}) + : polygon_(std::make_shared()) { + setExterior(std::move(exterior)); + setInteriors(std::move(interiors)); + } + + bool equals(const Polygon &other) const { + bool polygon_is_equal = bg::equals(*polygon_, *(other.polygon_)); + return parameters_ == other.parameters_ && polygon_is_equal; + } + + // TODO: Box is probably sufficient. + std::vector> intersection(const BoostPolygon &otherPolygon) const; + + std::string toWkt() const override { return convertToWkt(*polygon_); } + + std::vector> getExterior() const; + std::vector>> getInteriors() const; + + bool contains(const Polygon &other) const { return bg::within(*(other.polygon_), *polygon_); } + bool isValid() const { return bg::is_valid(*polygon_); } + + void makeValid() { *polygon_ = utilities::MakeValid(*polygon_); } + + ExteriorRing getExteriorAsIterator() { return bg::exterior_ring(*polygon_); } + InteriorRings getInteriorAsIterator() { return polygon_->inners(); } + + double getArea() const { + // Shapely reorients the polygon in memory if it is not oriented correctly, but keeps the coordinates + // So we need to make a copy here to avoid modifying the original polygon + if (!is_corrected_) { + // Make a copy of the current polygon + BoostPolygon new_polygon = *polygon_; + bg::correct(new_polygon); // Correct the copied polygon + return bg::area(new_polygon); + } + + return bg::area(*polygon_); + } + + void setExterior(const std::vector> &coordinates); + void setInteriors(const std::vector>> &interiors); + void correctIfNeeded() const; + void scale(double scaling); + void simplifyPolygon(double tolerance); + + private: + mutable bool is_corrected_ = false; // mutable allows modification in const methods +}; + +void Polygon::scale(double scaling) { utilities::AffineTransform(*polygon_, {0.0, 0.0}, scaling); } +void Polygon::setInteriors(const std::vector>> &interiors) { + bg::interior_rings(*polygon_).clear(); + polygon_->inners().resize(interiors.size()); + + for (size_t i = 0; i < interiors.size(); ++i) { + const auto &interior_coords = interiors[i]; + auto &inner = polygon_->inners()[i]; + inner.clear(); + + for (const auto &coord : interior_coords) { + bg::append(inner, BoostPoint(coord.first, coord.second)); + } + + // Close the ring if it's not already closed + if (interior_coords.front() != interior_coords.back()) { + bg::append(inner, BoostPoint(interior_coords.front().first, interior_coords.front().second)); + } + } + + is_corrected_ = false; // Mark as not corrected. Correction reorients and closes +} +std::vector> Polygon::intersection(const BoostPolygon &otherPolygon) const { + // correctIfNeeded(); + // Make the polygon valid if needed before performing the intersection + // TODO: This simplifies the polygon!! + BoostPolygon validPolygon = utilities::MakeValid(*polygon_); + + std::vector intersectionResult; + bg::intersection(validPolygon, otherPolygon, intersectionResult); + + std::vector> result; + for (const auto &intersectedBoostPolygon : intersectionResult) { + auto intersectedPolygon = std::make_shared(intersectedBoostPolygon); + // Copy the parameters from this polygon to the new one + + for (const auto ¶m : parameters_) { + intersectedPolygon->setField(param.first, param.second); + } + + result.emplace_back(intersectedPolygon); + } + + return result; +} +void Polygon::simplifyPolygon(double tolerance) { bg::simplify(*polygon_, *polygon_, tolerance); } +void Polygon::correctIfNeeded() const { + if (!is_corrected_) { + bg::correct(*polygon_); // Dereference the shared pointer to apply the correction + is_corrected_ = true; + } +} + +std::vector> Polygon::getExterior() const { + std::vector> result; + result.reserve(bg::exterior_ring(*polygon_).size()); + for (const auto &point : bg::exterior_ring(*polygon_)) { + result.emplace_back(bg::get<0>(point), bg::get<1>(point)); + } + return result; +} + +std::vector>> Polygon::getInteriors() const { + // correctIfNeeded(); + std::vector>> result; + result.reserve(polygon_->inners().size()); + for (const auto &inner : polygon_->inners()) { + std::vector> inner_result; + for (const auto &point : inner) { + inner_result.emplace_back(bg::get<0>(point), bg::get<1>(point)); + } + result.emplace_back(inner_result); + } + return result; +} + +void Polygon::setExterior(const std::vector> &coordinates) { + bg::exterior_ring(*polygon_).clear(); + bg::exterior_ring(*polygon_).reserve(coordinates.size()); + for (const auto &coord : coordinates) { + bg::append(*polygon_, BoostPoint(coord.first, coord.second)); + } + + // Close the ring if it's not already closed + // Shapely does this, so we want to keep compatibility. + if (coordinates.front() != coordinates.back()) { + bg::append(*polygon_, BoostPoint(coordinates.front().first, coordinates.front().second)); + } + + is_corrected_ = false; // Mark as not corrected. Correction reorients and closes +} + +inline void declare_polygon(py::module &m) { + py::class_>(m, "Polygon") + .def(py::init<>()) + .def(py::init()) + .def(py::init> &, + const std::vector>> &>()) + .def(py::init([](const std::shared_ptr &p) { + // Share the same C++ object, not creating a new one + return p; + })) + .def(py::init([](const Polygon &other) { + // Explicitly copy parameters when copying the polygon + auto newPolygon = std::make_shared(*other.polygon_); + newPolygon->parameters_ = other.parameters_; // Copy the parameters + return newPolygon; + })) + .def("set_exterior", &Polygon::setExterior) + .def("set_interiors", &Polygon::setInteriors) + .def("get_exterior", &Polygon::getExterior) + .def("get_exterior_iterator", + [](Polygon &self) { + return py::make_iterator(self.getExteriorAsIterator().begin(), self.getExteriorAsIterator().end()); + }) + .def("get_interiors_iterator", + [](Polygon &self) { + return py::make_iterator(self.getInteriorAsIterator().begin(), self.getInteriorAsIterator().end()); + }) + .def("scale", &Polygon::scale, py::arg("scaling")) + .def("get_interiors", &Polygon::getInteriors) + .def("correct_orientation", &Polygon::correctIfNeeded) + .def("simplify", &Polygon::simplifyPolygon) + .def("contains", &Polygon::contains, py::arg("other"), + "Check if the polygon fully contains another polygon. Does not check if the fields are equal") + .def("make_valid", &Polygon::makeValid, + "Make the polygon valid by removing self-intersections and duplicate points") + .def("equals", &Polygon::equals, py::arg("other"), + "Check if the polygon is equal to another polygon. Checks if the fields are equal.") + .def_property_readonly("wkt", &Polygon::toWkt) + .def_property_readonly("is_valid", &Polygon::isValid) + .def_property_readonly("area", &Polygon::getArea); +} + +#endif // DLUP_GEOMETRY_POLYGON_H diff --git a/src/geometry/polygon_collection.h b/src/geometry/polygon_collection.h new file mode 100644 index 00000000..ae8ad5f8 --- /dev/null +++ b/src/geometry/polygon_collection.h @@ -0,0 +1,78 @@ +#ifndef DLUP_POLYGON_COLLECTION_H +#define DLUP_POLYGON_COLLECTION_H +#pragma once + +#include "factory.h" +#include "lazy_array.h" +#include +#include +#include +#include +#include + +class Polygon; + +class PolygonCollection { + public: + PolygonCollection(std::vector> polygons, std::tuple mask_size) + : polygons_(std::move(polygons)), mask_size_(std::move(mask_size)), initialized_(true) {} + + // Lazy initialization constructor + PolygonCollection(std::function>()> initializer, std::tuple mask_size) + : initialized_(false), polygons_(), mask_size_(std::move(mask_size)), initializer_(std::move(initializer)) {} + + std::vector getGeometries() const { + ensureInitialized(); + std::vector py_objects; + py_objects.reserve(polygons_.size()); + for (const auto &polygon : polygons_) { + py_objects.push_back(FactoryManager::callFactoryFunction(polygon)); + } + return py_objects; + } + + auto getMaskSize() const { return mask_size_; } + + LazyArray toMask(int default_value = 0) const { + ensureInitialized(); + // Capture polygons_ and mask_size_ by value + auto polygons_copy = polygons_; + auto mask_size_copy = mask_size_; + return LazyArray( + [polygons_copy, mask_size_copy, default_value]() { + auto mask = generateMaskFromAnnotations(polygons_copy, mask_size_copy, default_value); + int width = std::get<0>(mask_size_copy); + int height = std::get<1>(mask_size_copy); + return py::array_t({height, width}, mask->data()); + }, + {std::get<1>(mask_size_copy), std::get<0>(mask_size_copy)}); + } + + const std::vector> &getPolygonsVector() const { + ensureInitialized(); + return polygons_; + } + + private: + void ensureInitialized() const { + if (!initialized_ && initializer_) { + polygons_ = initializer_(); // Execute the lambda to initialize the polygons + initialized_ = true; + } else { + // Do nothing if already initialized or no initializer provided + } + } + + mutable bool initialized_; + mutable std::vector> polygons_; + std::tuple mask_size_; + std::function>()> initializer_; +}; + +void declare_pybind_polygon_collection(py::module &m) { + py::class_>(m, "PolygonCollection") + .def("get_geometries", &PolygonCollection::getGeometries) + .def("to_mask", &PolygonCollection::toMask, py::arg("default_value") = 0); +}; + +#endif // DLUP_POLYGON_COLLECTION_H diff --git a/src/geometry/region.h b/src/geometry/region.h new file mode 100644 index 00000000..d8d26d1a --- /dev/null +++ b/src/geometry/region.h @@ -0,0 +1,126 @@ +#ifndef DLUP_GEOMETRY_REGION_H +#define DLUP_GEOMETRY_REGION_H +#pragma once + +#include "factory.h" +#include "polygon_collection.h" +#include +#include +#include +#include +#include + +class Polygon; +class Box; +class Point; + +template +class AnnotationRegionBase { + public: + AnnotationRegionBase(std::vector> objects) : objects_(std::move(objects)) {} + + std::vector> getObjectVector() const { return objects_; } + + std::vector getObjects() const { + std::vector py_objects; + py_objects.reserve(objects_.size()); + for (const auto &object : objects_) { + py_objects.push_back(FactoryManager::callFactoryFunction(object)); + } + return py_objects; + } + + private: + std::vector> objects_; +}; + +class AnnotationRegion { + public: + AnnotationRegion(std::function region_generator) + : region_generator_(region_generator), initialized_(false), + polygon_collection_( + std::make_shared(std::vector>(), std::tuple{0, 0})), + roi_collection_( + std::make_shared(std::vector>(), std::tuple{0, 0})), + point_region_({}), box_region_({}) {} + + AnnotationRegion(std::vector> polygons, std::vector> rois, + std::vector> boxes, std::vector> points, + std::tuple mask_size) + : polygon_collection_(std::make_shared(std::move(polygons), std::move(mask_size))), + roi_collection_(std::make_shared(std::move(rois), std::move(mask_size))), + point_region_(std::move(points)), box_region_(std::move(boxes)), initialized_(true) {} + + std::shared_ptr getPolygonsEager() { + ensureInitialized(); + return polygon_collection_; + } + + std::shared_ptr getPolygons() { + ensureInitialized(); + if (!lazy_polygon_collection_) { + lazy_polygon_collection_ = std::make_shared( + [this]() -> std::vector> { + this->ensureInitialized(); + return polygon_collection_->getPolygonsVector(); // Ensure polygons are initialized + }, + polygon_collection_->getMaskSize()); + } + return lazy_polygon_collection_; + } + + std::shared_ptr getRois() { + ensureInitialized(); + if (!lazy_roi_collection_) { + lazy_roi_collection_ = std::make_shared( + [this]() -> std::vector> { + this->ensureInitialized(); + return roi_collection_->getPolygonsVector(); // Ensure rois are initialized + }, + roi_collection_->getMaskSize()); + } + return lazy_roi_collection_; + } + + std::vector getPoints() { + ensureInitialized(); + return point_region_.getObjects(); + } + + std::vector getBoxes() { + ensureInitialized(); + return box_region_.getObjects(); + } + + private: + void ensureInitialized() { + if (!initialized_) { + AnnotationRegion generated_region = region_generator_(); + polygon_collection_ = std::move(generated_region.polygon_collection_); + roi_collection_ = std::move(generated_region.roi_collection_); + point_region_ = std::move(generated_region.point_region_); + box_region_ = std::move(generated_region.box_region_); + initialized_ = true; + } + } + + std::function region_generator_; + bool initialized_; + std::shared_ptr polygon_collection_; + std::shared_ptr roi_collection_; + AnnotationRegionBase point_region_; + AnnotationRegionBase box_region_; + mutable std::shared_ptr lazy_polygon_collection_; + mutable std::shared_ptr lazy_roi_collection_; +}; + +void declare_pybind_region(py::module &m) { + py::class_>(m, "AnnotationRegion") + .def_property_readonly("polygons", &AnnotationRegion::getPolygons) + .def_property_readonly("rois", &AnnotationRegion::getRois) + .def_property_readonly("polygons_eager", &AnnotationRegion::getPolygonsEager) + .def_property_readonly("boxes", &AnnotationRegion::getBoxes) + .def_property_readonly("points", &AnnotationRegion::getPoints); +}; + +#endif // DLUP_GEOMETRY_REGION_H diff --git a/src/geometry/rtree.h b/src/geometry/rtree.h new file mode 100644 index 00000000..68b12696 --- /dev/null +++ b/src/geometry/rtree.h @@ -0,0 +1,63 @@ +#ifndef DLUP_GEOMETRY_RTREE_H +#define DLUP_GEOMETRY_RTREE_H +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace bg = boost::geometry; +namespace bgi = boost::geometry::index; + +using BoostPoint = bg::model::d2::point_xy; +using BoostBox = bg::model::box; + +class RTreeBase { + public: + using RTreeType = bgi::rtree, bgi::quadratic<16>>; + + virtual ~RTreeBase() = default; + + virtual void rebuild() = 0; // Pure virtual function for rebuilding the R-tree + + void insert(const BoostBox &box, size_t index) { + std::lock_guard lock(rtree_mutex_); // Lock the mutex for thread safety + rtree_.insert(std::make_pair(box, index)); + rtree_invalidated_ = false; + } + + template + void query(const QueryType &query, OutputIterator out) { + std::lock_guard lock(rtree_mutex_); // Lock the mutex for thread safety + if (rtree_invalidated_) { + rebuild(); + } + rtree_.query(query, out); + } + + void invalidate() { + std::lock_guard lock(rtree_mutex_); // Lock the mutex for thread safety + rtree_invalidated_ = true; + } + + void clear() { + std::lock_guard lock(rtree_mutex_); // Lock the mutex for thread safety + rtree_.clear(); + rtree_invalidated_ = true; + } + + bool isInvalidated() const { + std::lock_guard lock(rtree_mutex_); // Lock the mutex for thread safety + return rtree_invalidated_; + } + + protected: + RTreeType rtree_; + bool rtree_invalidated_ = true; + mutable std::mutex rtree_mutex_; // Mutex to protect R-tree operations +}; + +#endif // DLUP_GEOMETRY_RTREE_H diff --git a/src/geometry/utilities.h b/src/geometry/utilities.h new file mode 100644 index 00000000..bfde36b5 --- /dev/null +++ b/src/geometry/utilities.h @@ -0,0 +1,78 @@ +#ifndef DLUP_GEOMETRY_UTILITIES_H +#define DLUP_GEOMETRY_UTILITIES_H +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace utilities { + +namespace bg = boost::geometry; + +// Aliases for common types +using BoostPoint = bg::model::d2::point_xy; +using BoostPolygon = bg::model::polygon; +using BoostBox = bg::model::box; + +// Function to make a polygon valid +BoostPolygon MakeValid(const BoostPolygon &polygon) { + BoostPolygon valid_polygon = polygon; + + // Check if the polygon is valid + if (!bg::is_valid(valid_polygon)) { + // Correct the polygon (removing self-intersections and duplicate points) + bg::correct(valid_polygon); + + // If still not valid, simplify it + if (!bg::is_valid(valid_polygon)) { + BoostPolygon simplifiedPolygon; + // TODO: emit a warning + bg::simplify(valid_polygon, simplifiedPolygon, 0.01); // TODO: Adjust tolerance + valid_polygon = simplifiedPolygon; + } + } + + return valid_polygon; +} + +void AffineTransform(BoostPolygon &polygon, const std::pair &origin, double scaling) { + bg::strategy::transform::matrix_transformer transform(scaling, 0, -origin.first, 0, scaling, + -origin.second, 0, 0, 1); + + // TODO: This is a bit weird that we can't just immediately apply this to the polygon + // Apply the transformation to each point of the exterior ring + for (auto &point : bg::exterior_ring(polygon)) { + bg::transform(point, point, transform); + } + + // Apply the transformation to each point of each interior ring + for (auto &ring : bg::interior_rings(polygon)) { + for (auto &point : ring) { + bg::transform(point, point, transform); + } + } +} + +// Function to apply an affine transformation to a point +void AffineTransform(BoostPoint &point, const std::pair &origin, double scaling) { + double x = (bg::get<0>(point) - origin.first) * scaling; + double y = (bg::get<1>(point) - origin.second) * scaling; + bg::set<0>(point, x); + bg::set<1>(point, y); +} + +void AffineTransform(BoostBox &box, const std::pair &origin, double scaling) { + bg::strategy::transform::matrix_transformer transform(scaling, 0, -origin.first, 0, scaling, + -origin.second, 0, 0, 1); + + // Apply the transformation to the min corner + bg::transform(bg::return_envelope(box), box, transform); +} +} // namespace utilities +// namespace geometry_utils + +#endif // DLUP_GEOMETRY_UTILITIES_H diff --git a/src/image.h b/src/image.h index 01541a16..c17763f0 100644 --- a/src/image.h +++ b/src/image.h @@ -11,25 +11,25 @@ namespace image_utils { void downsample2x2(const std::vector &input, uint32_t inputWidth, uint32_t inputHeight, std::vector &output, uint32_t outputWidth, uint32_t outputHeight, int channels) { - for (uint32_t y = 0; y < outputHeight; ++y) { - for (uint32_t x = 0; x < outputWidth; ++x) { - for (int c = 0; c < channels; ++c) { - uint32_t sum = 0; - uint32_t count = 0; - for (uint32_t dy = 0; dy < 2; ++dy) { - for (uint32_t dx = 0; dx < 2; ++dx) { - uint32_t sx = 2 * x + dx; - uint32_t sy = 2 * y + dy; - if (sx < inputWidth && sy < inputHeight) { - sum += std::to_integer(input[(sy * inputWidth + sx) * channels + c]); - ++count; - } - } - } - output[(y * outputWidth + x) * channels + c] = static_cast(sum / count); + for (uint32_t y = 0; y < outputHeight; ++y) { + for (uint32_t x = 0; x < outputWidth; ++x) { + for (int c = 0; c < channels; ++c) { + uint32_t sum = 0; + uint32_t count = 0; + for (uint32_t dy = 0; dy < 2; ++dy) { + for (uint32_t dx = 0; dx < 2; ++dx) { + uint32_t sx = 2 * x + dx; + uint32_t sy = 2 * y + dy; + if (sx < inputWidth && sy < inputHeight) { + sum += std::to_integer(input[(sy * inputWidth + sx) * channels + c]); + ++count; } + } } + output[(y * outputWidth + x) * channels + c] = static_cast(sum / count); + } } + } } } // namespace image_utils diff --git a/src/libtiff_tiff_writer.cpp b/src/libtiff_tiff_writer.cpp index 7ec3bf2b..73a80edb 100644 --- a/src/libtiff_tiff_writer.cpp +++ b/src/libtiff_tiff_writer.cpp @@ -1,5 +1,7 @@ #include "constants.h" #include "image.h" +#include "tiff/exceptions.h" +#include "tiff/writer.h" #include #include #include @@ -15,500 +17,44 @@ #include #include -#ifdef HAVE_ZSTD -#include -#endif - -namespace fs = std::filesystem; -namespace py = pybind11; - -class TiffException : public std::runtime_error { -public: - explicit TiffException(const std::string &message) : std::runtime_error(message) {} -}; - -class TiffCompressionNotSupportedError : public TiffException { -public: - explicit TiffCompressionNotSupportedError(const std::string &message) - : TiffException("Compression not supported: " + message) {} -}; - -class TiffOpenException : public TiffException { -public: - explicit TiffOpenException(const std::string &message) : TiffException("Failed to open TIFF file: " + message) {} -}; - -class TiffWriteException : public TiffException { -public: - explicit TiffWriteException(const std::string &message) : TiffException("Failed to write TIFF data: " + message) {} -}; - -class TiffSetupException : public TiffException { -public: - explicit TiffSetupException(const std::string &message) : TiffException("Failed to setup TIFF: " + message) {} -}; - -class TiffReadException : public TiffException { -public: - explicit TiffReadException(const std::string &message) : TiffException("Failed to read TIFF data: " + message) {} -}; - -enum class CompressionType { NONE, JPEG, LZW, DEFLATE, ZSTD }; - -CompressionType string_to_compression_type(const std::string &compression) { - if (compression == "NONE") - return CompressionType::NONE; - if (compression == "JPEG") - return CompressionType::JPEG; - if (compression == "LZW") - return CompressionType::LZW; - if (compression == "DEFLATE") - return CompressionType::DEFLATE; - if (compression == "ZSTD") - return CompressionType::ZSTD; - throw std::invalid_argument("Invalid compression type: " + compression); -} - -struct TIFFDeleter { - void operator()(TIFF *tif) const noexcept { - if (tif) { - // Disable error reporting temporarily - TIFFErrorHandler oldHandler = TIFFSetErrorHandler(nullptr); - - // Attempt to flush any pending writes - if (TIFFFlush(tif) == 0) { - TIFFError("TIFFDeleter", "Failed to flush TIFF data"); - } - - TIFFClose(tif); - TIFFSetErrorHandler(oldHandler); - } - } -}; - -using TIFFPtr = std::unique_ptr; - -class LibtiffTiffWriter { -public: - LibtiffTiffWriter(fs::path filename, std::array imageSize, std::array mpp, - std::array tileSize, CompressionType compression = CompressionType::JPEG, - int quality = 100) - : filename(std::move(filename)), imageSize(imageSize), mpp(mpp), tileSize(tileSize), compression(compression), - quality(quality), tif(nullptr) { - - validateInputs(); - - TIFF *tiff_ptr = TIFFOpen(this->filename.c_str(), "w"); - if (!tiff_ptr) { - throw TiffOpenException("Unable to create TIFF file"); - } - tif.reset(tiff_ptr); - - setupTIFFDirectory(0); - } - - ~LibtiffTiffWriter(); - void writeTile(py::array_t tile, int row, int col); - void flush(); - void finalize(); - void writePyramid(); - -private: - std::string filename; - std::array imageSize; - std::array mpp; - std::array tileSize; - CompressionType compression; - int quality; - uint32_t tileCounter; - int numLevels = calculateLevels(); - TIFFPtr tif; - - void validateInputs() const; - int calculateLevels(); - std::pair calculateTiles(int level); - uint32_t calculateNumTiles(int level); - void setupTIFFDirectory(int level); - void writeTIFFDirectory(); - void writeDownsampledResolutionPage(int level); - - std::pair getLevelDimensions(int level); - std::vector read2x2TileGroup(TIFF *readTif, uint32_t row, uint32_t col, uint32_t prevWidth, - uint32_t prevHeight); - void setupReadTIFF(TIFF *readTif); -}; - -LibtiffTiffWriter::~LibtiffTiffWriter() { finalize(); } - -void LibtiffTiffWriter::writeTile(py::array_t tile, int row, - int col) { - auto numTiles = calculateNumTiles(0); - if (tileCounter >= numTiles) { - throw TiffWriteException("all tiles have already been written"); - } - auto buf = tile.request(); - if (buf.ndim < 2 || buf.ndim > 3) { - throw TiffWriteException("invalid number of dimensions in tile data. Expected 2 or 3, got " + - std::to_string(buf.ndim)); - } - auto [height, width, channels] = std::tuple{buf.shape[0], buf.shape[1], buf.ndim > 2 ? buf.shape[2] : 1}; - - // Verify dimensions and buffer size - size_t expected_size = static_cast(width) * height * channels; - if (static_cast(buf.size) != expected_size) { - throw TiffWriteException("buffer size does not match expected size. Expected " + std::to_string(expected_size) + - ", got " + std::to_string(buf.size)); - } - - // Check if tile coordinates are within bounds - if (row < 0 || row >= imageSize[0] || col < 0 || col >= imageSize[1]) { - auto [imageWidth, imageHeight] = getLevelDimensions(0); - throw TiffWriteException("tile coordinates out of bounds for row " + std::to_string(row) + ", col " + - std::to_string(col) + ". Image size is " + std::to_string(imageWidth) + "x" + - std::to_string(imageHeight)); - } - - // Write the tile - if (TIFFWriteTile(tif.get(), buf.ptr, col, row, 0, 0) < 0) { - throw TiffWriteException("TIFFWriteTile failed for row " + std::to_string(row) + ", col " + - std::to_string(col)); - } - tileCounter++; - if (tileCounter == numTiles) { - flush(); - } -} - -void LibtiffTiffWriter::validateInputs() const { - // check positivity of image size - if (imageSize[0] <= 0 || imageSize[1] <= 0 || imageSize[2] <= 0) { - throw std::invalid_argument("Invalid size parameters"); - } - - // check positivity of mpp - if (mpp[0] <= 0 || mpp[1] <= 0) { - throw std::invalid_argument("Invalid mpp value"); - } - - // check positivity of tile size - if (tileSize[0] <= 0 || tileSize[1] <= 0) { - throw std::invalid_argument("Invalid tile size"); - } - - // check quality parameter - if (quality < 0 || quality > 100) { - throw std::invalid_argument("Invalid quality value"); - } - - // check if tile size is power of two - if ((tileSize[0] & (tileSize[0] - 1)) != 0 || (tileSize[1] & (tileSize[1] - 1)) != 0) { - throw std::invalid_argument("Tile size must be a power of two"); - } -} - -int LibtiffTiffWriter::calculateLevels() { - int maxDim = std::max(imageSize[0], imageSize[1]); - int minTileDim = std::min(tileSize[0], tileSize[1]); - int numLevels = 1; - while (maxDim > minTileDim * 2) { - maxDim /= 2; - numLevels++; - } - return numLevels; -} - -std::pair LibtiffTiffWriter::calculateTiles(int level) { - auto [currentWidth, currentHeight] = getLevelDimensions(level); - auto [tileWidth, tileHeight] = tileSize; - - uint32_t numTilesX = (currentWidth + tileWidth - 1) / tileWidth; - uint32_t numTilesY = (currentHeight + tileHeight - 1) / tileHeight; - return {numTilesX, numTilesY}; -} - -uint32_t LibtiffTiffWriter::calculateNumTiles(int level) { - auto [numTilesX, numTilesY] = calculateTiles(level); - return numTilesX * numTilesY; -} - -std::pair LibtiffTiffWriter::getLevelDimensions(int level) { - uint32_t levelWidth = std::max(1, imageSize[1] >> level); - uint32_t levelHeight = std::max(1, imageSize[0] >> level); - return {levelWidth, levelHeight}; -} - -void LibtiffTiffWriter::flush() { - if (tif) { - if (TIFFFlush(tif.get()) != 1) { - throw TiffWriteException("failed to flush TIFF file"); - } - } -} - -void LibtiffTiffWriter::finalize() { - if (tif) { - // Only write directory if we haven't written all directories yet - if (TIFFCurrentDirectory(tif.get()) < TIFFNumberOfDirectories(tif.get()) - 1) { - TIFFWriteDirectory(tif.get()); - } - TIFFClose(tif.get()); - tif.release(); - } -} - -void LibtiffTiffWriter::setupReadTIFF(TIFF *readTif) { - auto set_field = [readTif](uint32_t tag, auto... value) { - if (TIFFSetField(readTif, tag, value...) != 1) { - throw TiffSetupException("failed to set TIFF field for reading: " + std::to_string(tag)); - } - }; - - uint16_t compression; - if (TIFFGetField(readTif, TIFFTAG_COMPRESSION, &compression) == 1) { - if (compression == COMPRESSION_JPEG) { - set_field(TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); - } - } -} - -void LibtiffTiffWriter::setupTIFFDirectory(int level) { - auto set_field = [this](uint32_t tag, auto... value) { - if (TIFFSetField(tif.get(), tag, value...) != 1) { - throw TiffSetupException("failed to set TIFF field: " + std::to_string(tag)); - } - }; - - auto [width, height] = getLevelDimensions(level); - int channels = imageSize[2]; - - set_field(TIFFTAG_IMAGEWIDTH, width); - set_field(TIFFTAG_IMAGELENGTH, height); - set_field(TIFFTAG_SAMPLESPERPIXEL, channels); - set_field(TIFFTAG_BITSPERSAMPLE, 8); - set_field(TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); - set_field(TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); - set_field(TIFFTAG_TILEWIDTH, tileSize[1]); - set_field(TIFFTAG_TILELENGTH, tileSize[0]); - - if (channels == 3 || channels == 4) { - if (compression != CompressionType::JPEG) { - set_field(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); - } - } else { - set_field(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); - } - - if (channels == 4) { - uint16_t extra_samples = EXTRASAMPLE_ASSOCALPHA; - set_field(TIFFTAG_EXTRASAMPLES, 1, &extra_samples); - } else if (channels > 4) { - std::vector extra_samples(channels - 3, EXTRASAMPLE_UNSPECIFIED); - set_field(TIFFTAG_EXTRASAMPLES, channels - 3, extra_samples.data()); - } - - switch (compression) { - case CompressionType::NONE: - set_field(TIFFTAG_COMPRESSION, COMPRESSION_NONE); - break; - case CompressionType::JPEG: - set_field(TIFFTAG_COMPRESSION, COMPRESSION_JPEG); - set_field(TIFFTAG_JPEGQUALITY, quality); - set_field(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_YCBCR); - set_field(TIFFTAG_YCBCRSUBSAMPLING, 2, 2); - set_field(TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); - break; - case CompressionType::LZW: - set_field(TIFFTAG_COMPRESSION, COMPRESSION_LZW); - break; - case CompressionType::DEFLATE: - set_field(TIFFTAG_COMPRESSION, COMPRESSION_ADOBE_DEFLATE); - break; - case CompressionType::ZSTD: -#ifdef HAVE_ZSTD - set_field(TIFFTAG_COMPRESSION, COMPRESSION_ZSTD); - set_field(TIFFTAG_ZSTD_LEVEL, 3); // 3 is the default - break; -#else - throw TiffCompressionNotSupportedError("ZSTD"); -#endif - default: - throw TiffSetupException("Unknown compression type"); - } - - // Convert mpp (micrometers per pixel) to pixels per centimeter - double pixels_per_cm_x = 10000.0 / mpp[0]; - double pixels_per_cm_y = 10000.0 / mpp[1]; - - set_field(TIFFTAG_RESOLUTIONUNIT, RESUNIT_CENTIMETER); - set_field(TIFFTAG_XRESOLUTION, pixels_per_cm_x); - set_field(TIFFTAG_YRESOLUTION, pixels_per_cm_y); - - // Set the image description - // TODO: This needs to be configurable - std::string description = "TODO"; - // set_field(TIFFTAG_IMAGEDESCRIPTION, description.c_str()); - - // Set the software tag with version from dlup - std::string software_tag = - "dlup " + std::string(DLUP_VERSION) + " (libtiff " + std::to_string(TIFFLIB_VERSION) + ")"; - set_field(TIFFTAG_SOFTWARE, software_tag.c_str()); - - // Set SubFileType for pyramid levels - if (level == 0) { - set_field(TIFFTAG_SUBFILETYPE, 0); - } else { - set_field(TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE); - } -} - -std::vector LibtiffTiffWriter::read2x2TileGroup(TIFF *readTif, uint32_t row, uint32_t col, - uint32_t prevWidth, uint32_t prevHeight) { - auto [tileWidth, tileHeight] = tileSize; - int channels = imageSize[2]; - uint32_t fullGroupWidth = 2 * tileWidth; - uint32_t fullGroupHeight = 2 * tileHeight; - - // Initialize a zero buffer for the 2x2 group - std::vector groupBuffer(fullGroupWidth * fullGroupHeight * channels, std::byte(0)); - - for (int i = 0; i < 2; ++i) { - for (int j = 0; j < 2; ++j) { - uint32_t tileRow = row + i * tileHeight; - uint32_t tileCol = col + j * tileWidth; - - // Skip if this tile is out of bounds, this can happen when the image dimensions are smaller than 2x2 in - // tileSize - if (tileRow >= prevHeight || tileCol >= prevWidth) { - continue; - } - - std::vector tileBuf(TIFFTileSize(readTif)); - - if (TIFFReadTile(readTif, tileBuf.data(), tileCol, tileRow, 0, 0) < 0) { - throw TiffReadException("failed to read tile at row " + std::to_string(tileRow) + ", col " + - std::to_string(tileCol)); - } - - // Copy tile data to groupBuffer - uint32_t copyWidth = std::min(tileWidth, prevWidth - tileCol); - uint32_t copyHeight = std::min(tileHeight, prevHeight - tileRow); - for (uint32_t y = 0; y < copyHeight; ++y) { - for (uint32_t x = 0; x < copyWidth; ++x) { - for (int c = 0; c < channels; ++c) { - size_t groupIndex = - ((i * tileHeight + y) * fullGroupWidth + (j * tileWidth + x)) * channels + c; - size_t tileIndex = (y * tileWidth + x) * channels + c; - groupBuffer[groupIndex] = static_cast(tileBuf[tileIndex]); - } - } - } - } - } - - return groupBuffer; -} -void LibtiffTiffWriter::writeDownsampledResolutionPage(int level) { - if (level <= 0 || level >= numLevels) { - throw std::invalid_argument("Invalid level for downsampled resolution page"); - } - - auto [prevWidth, prevHeight] = getLevelDimensions(level - 1); - int channels = imageSize[2]; - auto [tileWidth, tileHeight] = tileSize; - - TIFFPtr readTif(TIFFOpen(filename.c_str(), "r")); - if (!readTif) { - throw TiffOpenException("failed to open TIFF file for reading"); - } - - if (!TIFFSetDirectory(readTif.get(), level - 1)) { - throw TiffReadException("failed to set directory to level " + std::to_string(level - 1)); - } - setupReadTIFF(readTif.get()); - - if (!TIFFSetDirectory(tif.get(), level - 1)) { - throw TiffReadException("failed to set directory to level " + std::to_string(level - 1)); - } - - if (!TIFFWriteDirectory(tif.get())) { - throw TiffWriteException("failed to create new directory for downsampled image"); - } - - setupTIFFDirectory(level); - - auto [numTilesX, numTilesY] = calculateTiles(level); - - for (uint32_t tileY = 0; tileY < numTilesY; ++tileY) { - for (uint32_t tileX = 0; tileX < numTilesX; ++tileX) { - uint32_t row = tileY * tileHeight * 2; - uint32_t col = tileX * tileWidth * 2; - - std::vector groupBuffer = read2x2TileGroup(readTif.get(), row, col, prevWidth, prevHeight); - std::vector downsampledBuffer(tileHeight * tileWidth * channels); - - image_utils::downsample2x2(groupBuffer, 2 * tileWidth, 2 * tileHeight, downsampledBuffer, tileWidth, - tileHeight, channels); - - if (TIFFWriteTile(tif.get(), reinterpret_cast(downsampledBuffer.data()), tileX * tileWidth, - tileY * tileHeight, 0, 0) < 0) { - throw TiffWriteException("failed to write downsampled tile at level " + std::to_string(level) + - ", row " + std::to_string(tileY) + ", col " + std::to_string(tileX)); - } - } - } - - readTif.reset(); - flush(); -} - -void LibtiffTiffWriter::writePyramid() { - numLevels = calculateLevels(); - - // The base level (level 0) is already written, so we start from level 1 - for (int level = 1; level < numLevels; ++level) { - writeDownsampledResolutionPage(level); - flush(); - } -} - PYBIND11_MODULE(_libtiff_tiff_writer, m) { - py::class_(m, "LibtiffTiffWriter") - .def(py::init([](py::object path, std::array size, std::array mpp, - std::array tileSize, py::object compression, int quality) { - fs::path cpp_path; - if (py::isinstance(path)) { - cpp_path = fs::path(path.cast()); - } else if (py::hasattr(path, "__fspath__")) { - cpp_path = fs::path(path.attr("__fspath__")().cast()); - } else { - throw py::type_error("Expected str or os.PathLike object"); - } - - CompressionType comp_type; - if (py::isinstance(compression)) { - comp_type = string_to_compression_type(compression.cast()); - } else if (py::isinstance(compression)) { - comp_type = compression.cast(); - } else { - throw py::type_error("Expected str or CompressionType for compression"); - } - - return new LibtiffTiffWriter(std::move(cpp_path), size, mpp, tileSize, comp_type, quality); - })) - .def("write_tile", &LibtiffTiffWriter::writeTile) - .def("write_pyramid", &LibtiffTiffWriter::writePyramid) - .def("finalize", &LibtiffTiffWriter::finalize); - - py::enum_(m, "CompressionType") - .value("NONE", CompressionType::NONE) - .value("JPEG", CompressionType::JPEG) - .value("LZW", CompressionType::LZW) - .value("DEFLATE", CompressionType::DEFLATE); - - py::register_exception(m, "TiffException"); - py::register_exception(m, "TiffOpenException"); - py::register_exception(m, "TiffReadException"); - py::register_exception(m, "TiffWriteException"); - py::register_exception(m, "TiffSetupException"); - py::register_exception(m, "TiffCompressionNotSupportedError"); + py::class_(m, "LibtiffTiffWriter") + .def(py::init([](py::object path, std::array Size, std::array mpp, std::array tileSize, + py::object compression, int quality) { + fs::path cpp_path; + if (py::isinstance(path)) { + cpp_path = fs::path(path.cast()); + } else if (py::hasattr(path, "__fspath__")) { + cpp_path = fs::path(path.attr("__fspath__")().cast()); + } else { + throw py::type_error("Expected str or os.PathLike object"); + } + + CompressionType comp_type; + if (py::isinstance(compression)) { + comp_type = string_to_compression_type(compression.cast()); + } else if (py::isinstance(compression)) { + comp_type = compression.cast(); + } else { + throw py::type_error("Expected str or CompressionType for compression"); + } + + return new LibtiffTiffWriter(std::move(cpp_path), Size, mpp, tileSize, comp_type, quality); + })) + .def("write_tile", &LibtiffTiffWriter::writeTile) + .def("write_pyramid", &LibtiffTiffWriter::writePyramid) + .def("finalize", &LibtiffTiffWriter::finalize); + + py::enum_(m, "CompressionType") + .value("NONE", CompressionType::NONE) + .value("JPEG", CompressionType::JPEG) + .value("LZW", CompressionType::LZW) + .value("DEFLATE", CompressionType::DEFLATE); + + py::register_exception(m, "TiffException"); + py::register_exception(m, "TiffOpenException"); + py::register_exception(m, "TiffReadException"); + py::register_exception(m, "TiffWriteException"); + py::register_exception(m, "TiffSetupException"); + py::register_exception(m, "TiffCompressionNotSupportedError"); } diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 00000000..07c9f793 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,27 @@ +# Include the dependencies module +third_party_dir = include_directories('..') + +# Importing necessary variables from the dependencies build file +incdir_pybind11 = get_variable('incdir_pybind11', []) +base_deps = get_variable('base_deps', []) +boost_dep = dependency('boost', modules : ['system', 'serialization']) +opencv_dep = dependency('opencv4') + +# Pybind11 modules +libtiff_tiff_writer = py.extension_module('_libtiff_tiff_writer', + 'libtiff_tiff_writer.cpp', + install : true, + subdir : 'dlup', # This is crucial + include_directories : [third_party_dir, incdir_pybind11], + cpp_args : cpp_args, + link_args : link_args, + dependencies : base_deps) + +geometry = py.extension_module('_geometry', + 'geometry.cpp', + install : true, + subdir : 'dlup', # This is crucial + include_directories : [third_party_dir, incdir_pybind11], + cpp_args : cpp_args, + link_args : link_args, + dependencies : base_deps + [boost_dep, opencv_dep]) diff --git a/src/opencv.h b/src/opencv.h new file mode 100644 index 00000000..ae624351 --- /dev/null +++ b/src/opencv.h @@ -0,0 +1,74 @@ +#ifndef DLUP_OPENCV_H +#define DLUP_OPENCV_H + +#include "geometry/polygon.h" +#include +#include +#include +#include +#include +#include +#include + +std::shared_ptr> generateMaskFromAnnotations(const std::vector> &annotations, + const std::tuple &mask_size, + int default_value) { + + int width = std::get<0>(mask_size); + int height = std::get<1>(mask_size); + + // Use a shared_ptr to manage the mask's lifetime + auto mask = std::make_shared>(width * height, default_value); + + cv::Mat mask_view(height, width, CV_32S, mask->data()); + + std::vector exterior_cv_points; + std::vector> interiors_cv_points; + + for (const auto &annotation : annotations) { + auto index_value_field = annotation->getField("index"); + if (!index_value_field) { + auto label = annotation->getField("label"); + throw std::runtime_error("Annotation with label '" + label->cast() + "' does not have an index."); + } + int index_value = index_value_field->cast(); + + // Convert exterior points + exterior_cv_points.clear(); + const auto &exterior = annotation->getExterior(); + exterior_cv_points.reserve(exterior.size()); + for (const auto &[x, y] : exterior) { + exterior_cv_points.emplace_back(static_cast(std::round(x)), static_cast(std::round(y))); + } + + // Convert interior points + interiors_cv_points.clear(); + const auto &interiors = annotation->getInteriors(); + interiors_cv_points.reserve(interiors.size()); + for (const auto &interior : interiors) { + interiors_cv_points.emplace_back(); // Create a new vector in place + interiors_cv_points.back().reserve(interior.size()); + for (const auto &[x, y] : interior) { + interiors_cv_points.back().emplace_back(static_cast(std::round(x)), static_cast(std::round(y))); + } + } + + if (!interiors_cv_points.empty()) { + // Create a mask for holes + cv::Mat holes_mask = cv::Mat::zeros(height, width, CV_8U); + cv::fillPoly(holes_mask, interiors_cv_points, cv::Scalar(1)); + + // Apply exterior mask first, then restore original values in holes + cv::Mat original_values = mask_view.clone(); + cv::fillPoly(mask_view, std::vector>{exterior_cv_points}, cv::Scalar(index_value)); + original_values.copyTo(mask_view, holes_mask); + } else { + // Directly fill the exterior mask if no interiors exist + cv::fillPoly(mask_view, std::vector>{exterior_cv_points}, cv::Scalar(index_value)); + } + } + + return mask; +} + +#endif // DLUP_OPENCV_H diff --git a/src/tiff/exceptions.h b/src/tiff/exceptions.h new file mode 100644 index 00000000..863cffff --- /dev/null +++ b/src/tiff/exceptions.h @@ -0,0 +1,40 @@ +#ifndef DLUP_TIFF_EXCEPTIONS_H +#define DLUP_TIFF_EXCEPTIONS_H +#pragma once + +#include +#include +#include + +class TiffException : public std::runtime_error { + public: + explicit TiffException(const std::string &message) : std::runtime_error(message) {} +}; + +class TiffCompressionNotSupportedError : public TiffException { + public: + explicit TiffCompressionNotSupportedError(const std::string &message) + : TiffException("Compression not supported: " + message) {} +}; + +class TiffOpenException : public TiffException { + public: + explicit TiffOpenException(const std::string &message) : TiffException("Failed to open TIFF file: " + message) {} +}; + +class TiffWriteException : public TiffException { + public: + explicit TiffWriteException(const std::string &message) : TiffException("Failed to write TIFF data: " + message) {} +}; + +class TiffSetupException : public TiffException { + public: + explicit TiffSetupException(const std::string &message) : TiffException("Failed to setup TIFF: " + message) {} +}; + +class TiffReadException : public TiffException { + public: + explicit TiffReadException(const std::string &message) : TiffException("Failed to read TIFF data: " + message) {} +}; + +#endif // DLUP_TIFF_EXCEPTIONS_H diff --git a/src/tiff/writer.h b/src/tiff/writer.h new file mode 100644 index 00000000..f53e038c --- /dev/null +++ b/src/tiff/writer.h @@ -0,0 +1,444 @@ +#ifndef DLUP_TIFF_WRITER_H +#define DLUP_TIFF_WRITER_H +#pragma once + +#include "../constants.h" +#include "../image.h" +#include "exceptions.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_ZSTD +#include +#endif + +namespace fs = std::filesystem; +namespace py = pybind11; + +enum class CompressionType { NONE, JPEG, LZW, DEFLATE, ZSTD }; + +CompressionType string_to_compression_type(const std::string &compression) { + if (compression == "NONE") + return CompressionType::NONE; + if (compression == "JPEG") + return CompressionType::JPEG; + if (compression == "LZW") + return CompressionType::LZW; + if (compression == "DEFLATE") + return CompressionType::DEFLATE; + if (compression == "ZSTD") + return CompressionType::ZSTD; + throw std::invalid_argument("Invalid compression type: " + compression); +} + +struct TIFFDeleter { + void operator()(TIFF *tif) const noexcept { + if (tif) { + // Disable error reporting temporarily + TIFFErrorHandler oldHandler = TIFFSetErrorHandler(nullptr); + + // Attempt to flush any pending writes + if (TIFFFlush(tif) == 0) { + TIFFError("TIFFDeleter", "Failed to flush TIFF data"); + } + + TIFFClose(tif); + TIFFSetErrorHandler(oldHandler); + } + } +}; + +using TIFFPtr = std::unique_ptr; + +class LibtiffTiffWriter { + public: + LibtiffTiffWriter(fs::path filename, std::array imageSize, std::array mpp, + std::array tileSize, CompressionType compression = CompressionType::JPEG, int quality = 100) + : filename(std::move(filename)), imageSize(imageSize), mpp(mpp), tileSize(tileSize), compression(compression), + quality(quality), tif(nullptr) { + + validateInputs(); + + TIFF *tiff_ptr = TIFFOpen(this->filename.c_str(), "w"); + if (!tiff_ptr) { + throw TiffOpenException("Unable to create TIFF file"); + } + tif.reset(tiff_ptr); + + setupTIFFDirectory(0); + } + + ~LibtiffTiffWriter(); + void writeTile(py::array_t tile, int row, int col); + void flush(); + void finalize(); + void writePyramid(); + + private: + std::string filename; + std::array imageSize; + std::array mpp; + std::array tileSize; + CompressionType compression; + int quality; + uint32_t tileCounter; + int numLevels = calculateLevels(); + TIFFPtr tif; + + void validateInputs() const; + int calculateLevels(); + std::pair calculateTiles(int level); + uint32_t CalculateNumTiles(int level); + void setupTIFFDirectory(int level); + void writeTIFFDirectory(); + void writeDownsampledResolutionPage(int level); + + std::pair getLevelDimensions(int level); + std::vector read2x2TileGroup(TIFF *readTif, uint32_t row, uint32_t col, uint32_t prevWidth, + uint32_t prevHeight); + void setupReadTIFF(TIFF *readTif); +}; + +LibtiffTiffWriter::~LibtiffTiffWriter() { finalize(); } + +void LibtiffTiffWriter::writeTile(py::array_t tile, int row, + int col) { + auto num_tiles = CalculateNumTiles(0); + if (tileCounter >= num_tiles) { + throw TiffWriteException("all tiles have already been written"); + } + auto buf = tile.request(); + if (buf.ndim < 2 || buf.ndim > 3) { + throw TiffWriteException("invalid number of dimensions in tile data. Expected 2 or 3, got " + + std::to_string(buf.ndim)); + } + auto [height, width, channels] = std::tuple{buf.shape[0], buf.shape[1], buf.ndim > 2 ? buf.shape[2] : 1}; + + // Verify dimensions and buffer Size + size_t expected_size = static_cast(width) * height * channels; + if (static_cast(buf.size) != expected_size) { + throw TiffWriteException("buffer Size does not match expected Size. Expected " + std::to_string(expected_size) + + ", got " + std::to_string(buf.size)); + } + + // Check if tile coordinates are within bounds + if (row < 0 || row >= imageSize[0] || col < 0 || col >= imageSize[1]) { + auto [imageWidth, imageHeight] = getLevelDimensions(0); + throw TiffWriteException("tile coordinates out of bounds for row " + std::to_string(row) + ", col " + + std::to_string(col) + ". Image Size is " + std::to_string(imageWidth) + "x" + + std::to_string(imageHeight)); + } + + // Write the tile + if (TIFFWriteTile(tif.get(), buf.ptr, col, row, 0, 0) < 0) { + throw TiffWriteException("TIFFWriteTile failed for row " + std::to_string(row) + ", col " + std::to_string(col)); + } + tileCounter++; + if (tileCounter == num_tiles) { + flush(); + } +} + +void LibtiffTiffWriter::validateInputs() const { + // check positivity of image Size + if (imageSize[0] <= 0 || imageSize[1] <= 0 || imageSize[2] <= 0) { + throw std::invalid_argument("Invalid Size parameters"); + } + + // check positivity of mpp + if (mpp[0] <= 0 || mpp[1] <= 0) { + throw std::invalid_argument("Invalid mpp value"); + } + + // check positivity of tile Size + if (tileSize[0] <= 0 || tileSize[1] <= 0) { + throw std::invalid_argument("Invalid tile Size"); + } + + // check quality parameter + if (quality < 0 || quality > 100) { + throw std::invalid_argument("Invalid quality value"); + } + + // check if tile Size is power of two + if ((tileSize[0] & (tileSize[0] - 1)) != 0 || (tileSize[1] & (tileSize[1] - 1)) != 0) { + throw std::invalid_argument("Tile Size must be a power of two"); + } +} + +int LibtiffTiffWriter::calculateLevels() { + int maxDim = std::max(imageSize[0], imageSize[1]); + int minTileDim = std::min(tileSize[0], tileSize[1]); + int numLevels = 1; + while (maxDim > minTileDim * 2) { + maxDim /= 2; + numLevels++; + } + return numLevels; +} + +std::pair LibtiffTiffWriter::calculateTiles(int level) { + auto [currentWidth, currentHeight] = getLevelDimensions(level); + auto [tileWidth, tileHeight] = tileSize; + + uint32_t numTilesX = (currentWidth + tileWidth - 1) / tileWidth; + uint32_t numTilesY = (currentHeight + tileHeight - 1) / tileHeight; + return {numTilesX, numTilesY}; +} + +uint32_t LibtiffTiffWriter::CalculateNumTiles(int level) { + auto [numTilesX, numTilesY] = calculateTiles(level); + return numTilesX * numTilesY; +} + +std::pair LibtiffTiffWriter::getLevelDimensions(int level) { + uint32_t levelWidth = std::max(1, imageSize[1] >> level); + uint32_t levelHeight = std::max(1, imageSize[0] >> level); + return {levelWidth, levelHeight}; +} + +void LibtiffTiffWriter::flush() { + if (tif) { + if (TIFFFlush(tif.get()) != 1) { + throw TiffWriteException("failed to flush TIFF file"); + } + } +} + +void LibtiffTiffWriter::finalize() { + if (tif) { + // Only write directory if we haven't written all directories yet + if (TIFFCurrentDirectory(tif.get()) < TIFFNumberOfDirectories(tif.get()) - 1) { + TIFFWriteDirectory(tif.get()); + } + TIFFClose(tif.get()); + tif.release(); + } +} + +void LibtiffTiffWriter::setupReadTIFF(TIFF *readTif) { + auto set_field = [readTif](uint32_t tag, auto... value) { + if (TIFFSetField(readTif, tag, value...) != 1) { + throw TiffSetupException("failed to set TIFF field for reading: " + std::to_string(tag)); + } + }; + + uint16_t compression; + if (TIFFGetField(readTif, TIFFTAG_COMPRESSION, &compression) == 1) { + if (compression == COMPRESSION_JPEG) { + set_field(TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); + } + } +} + +void LibtiffTiffWriter::setupTIFFDirectory(int level) { + auto set_field = [this](uint32_t tag, auto... value) { + if (TIFFSetField(tif.get(), tag, value...) != 1) { + throw TiffSetupException("failed to set TIFF field: " + std::to_string(tag)); + } + }; + + auto [width, height] = getLevelDimensions(level); + int channels = imageSize[2]; + + set_field(TIFFTAG_IMAGEWIDTH, width); + set_field(TIFFTAG_IMAGELENGTH, height); + set_field(TIFFTAG_SAMPLESPERPIXEL, channels); + set_field(TIFFTAG_BITSPERSAMPLE, 8); + set_field(TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); + set_field(TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + set_field(TIFFTAG_TILEWIDTH, tileSize[1]); + set_field(TIFFTAG_TILELENGTH, tileSize[0]); + + if (channels == 3 || channels == 4) { + if (compression != CompressionType::JPEG) { + set_field(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + } + } else { + set_field(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK); + } + + if (channels == 4) { + uint16_t extra_samples = EXTRASAMPLE_ASSOCALPHA; + set_field(TIFFTAG_EXTRASAMPLES, 1, &extra_samples); + } else if (channels > 4) { + std::vector extra_samples(channels - 3, EXTRASAMPLE_UNSPECIFIED); + set_field(TIFFTAG_EXTRASAMPLES, channels - 3, extra_samples.data()); + } + + switch (compression) { + case CompressionType::NONE: + set_field(TIFFTAG_COMPRESSION, COMPRESSION_NONE); + break; + case CompressionType::JPEG: + set_field(TIFFTAG_COMPRESSION, COMPRESSION_JPEG); + set_field(TIFFTAG_JPEGQUALITY, quality); + set_field(TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_YCBCR); + set_field(TIFFTAG_YCBCRSUBSAMPLING, 2, 2); + set_field(TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); + break; + case CompressionType::LZW: + set_field(TIFFTAG_COMPRESSION, COMPRESSION_LZW); + break; + case CompressionType::DEFLATE: + set_field(TIFFTAG_COMPRESSION, COMPRESSION_ADOBE_DEFLATE); + break; + case CompressionType::ZSTD: +#ifdef HAVE_ZSTD + set_field(TIFFTAG_COMPRESSION, COMPRESSION_ZSTD); + set_field(TIFFTAG_ZSTD_LEVEL, 3); // 3 is the default + break; +#else + throw TiffCompressionNotSupportedError("ZSTD"); +#endif + default: + throw TiffSetupException("Unknown compression type"); + } + + // Convert mpp (micrometers per pixel) to pixels per centimeter + double pixels_per_cm_x = 10000.0 / mpp[0]; + double pixels_per_cm_y = 10000.0 / mpp[1]; + + set_field(TIFFTAG_RESOLUTIONUNIT, RESUNIT_CENTIMETER); + set_field(TIFFTAG_XRESOLUTION, pixels_per_cm_x); + set_field(TIFFTAG_YRESOLUTION, pixels_per_cm_y); + + // Set the image description + // TODO: This needs to be configurable + std::string description = "TODO"; + // set_field(TIFFTAG_IMAGEDESCRIPTION, description.c_str()); + + // Set the software tag with version from dlup + std::string software_tag = "dlup " + std::string(DLUP_VERSION) + " (libtiff " + std::to_string(TIFFLIB_VERSION) + ")"; + set_field(TIFFTAG_SOFTWARE, software_tag.c_str()); + + // Set SubFileType for pyramid levels + if (level == 0) { + set_field(TIFFTAG_SUBFILETYPE, 0); + } else { + set_field(TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE); + } +} + +std::vector LibtiffTiffWriter::read2x2TileGroup(TIFF *readTif, uint32_t row, uint32_t col, + uint32_t prevWidth, uint32_t prevHeight) { + auto [tileWidth, tileHeight] = tileSize; + int channels = imageSize[2]; + uint32_t fullGroupWidth = 2 * tileWidth; + uint32_t fullGroupHeight = 2 * tileHeight; + + // Initialize a zero buffer for the 2x2 group + std::vector groupBuffer(fullGroupWidth * fullGroupHeight * channels, std::byte(0)); + + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 2; ++j) { + uint32_t tileRow = row + i * tileHeight; + uint32_t tileCol = col + j * tileWidth; + + // Skip if this tile is out of bounds, this can happen when the image dimensions are smaller than 2x2 in + // tileSize + if (tileRow >= prevHeight || tileCol >= prevWidth) { + continue; + } + + std::vector tileBuf(TIFFTileSize(readTif)); + + if (TIFFReadTile(readTif, tileBuf.data(), tileCol, tileRow, 0, 0) < 0) { + throw TiffReadException("failed to read tile at row " + std::to_string(tileRow) + ", col " + + std::to_string(tileCol)); + } + + // Copy tile data to groupBuffer + uint32_t copyWidth = std::min(tileWidth, prevWidth - tileCol); + uint32_t copyHeight = std::min(tileHeight, prevHeight - tileRow); + for (uint32_t y = 0; y < copyHeight; ++y) { + for (uint32_t x = 0; x < copyWidth; ++x) { + for (int c = 0; c < channels; ++c) { + size_t groupIndex = ((i * tileHeight + y) * fullGroupWidth + (j * tileWidth + x)) * channels + c; + size_t tileIndex = (y * tileWidth + x) * channels + c; + groupBuffer[groupIndex] = static_cast(tileBuf[tileIndex]); + } + } + } + } + } + + return groupBuffer; +} +void LibtiffTiffWriter::writeDownsampledResolutionPage(int level) { + if (level <= 0 || level >= numLevels) { + throw std::invalid_argument("Invalid level for downsampled resolution page"); + } + + auto [prevWidth, prevHeight] = getLevelDimensions(level - 1); + int channels = imageSize[2]; + auto [tileWidth, tileHeight] = tileSize; + + TIFFPtr readTif(TIFFOpen(filename.c_str(), "r")); + if (!readTif) { + throw TiffOpenException("failed to open TIFF file for reading"); + } + + if (!TIFFSetDirectory(readTif.get(), level - 1)) { + throw TiffReadException("failed to set directory to level " + std::to_string(level - 1)); + } + setupReadTIFF(readTif.get()); + + if (!TIFFSetDirectory(tif.get(), level - 1)) { + throw TiffReadException("failed to set directory to level " + std::to_string(level - 1)); + } + + if (!TIFFWriteDirectory(tif.get())) { + throw TiffWriteException("failed to create new directory for downsampled image"); + } + + setupTIFFDirectory(level); + + auto [numTilesX, numTilesY] = calculateTiles(level); + + for (uint32_t tileY = 0; tileY < numTilesY; ++tileY) { + for (uint32_t tileX = 0; tileX < numTilesX; ++tileX) { + uint32_t row = tileY * tileHeight * 2; + uint32_t col = tileX * tileWidth * 2; + + std::vector groupBuffer = read2x2TileGroup(readTif.get(), row, col, prevWidth, prevHeight); + std::vector downsampledBuffer(tileHeight * tileWidth * channels); + + image_utils::downsample2x2(groupBuffer, 2 * tileWidth, 2 * tileHeight, downsampledBuffer, tileWidth, tileHeight, + channels); + + if (TIFFWriteTile(tif.get(), reinterpret_cast(downsampledBuffer.data()), tileX * tileWidth, + tileY * tileHeight, 0, 0) < 0) { + throw TiffWriteException("failed to write downsampled tile at level " + std::to_string(level) + ", row " + + std::to_string(tileY) + ", col " + std::to_string(tileX)); + } + } + } + + readTif.reset(); + flush(); +} + +void LibtiffTiffWriter::writePyramid() { + numLevels = calculateLevels(); + + // The base level (level 0) is already written, so we start from level 1 + for (int level = 1; level < numLevels; ++level) { + writeDownsampledResolutionPage(level); + flush(); + } +} + +#endif diff --git a/tests/files/dlup_xml_example.xml b/tests/files/dlup_xml_example.xml new file mode 100644 index 00000000..7658e63d --- /dev/null +++ b/tests/files/dlup_xml_example.xml @@ -0,0 +1,63 @@ + + + example_image_01 + 1.0 + 2024-09-03 + ExampleSoftware + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/files/halo_holes.annotations b/tests/files/halo_holes.annotations new file mode 100644 index 00000000..86029e44 --- /dev/null +++ b/tests/files/halo_holes.annotations @@ -0,0 +1,371 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/files/test_different_types_halo.annotations b/tests/files/test_different_types_halo.annotations new file mode 100644 index 00000000..3b2c924a --- /dev/null +++ b/tests/files/test_different_types_halo.annotations @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/infer_ann.py b/tests/infer_ann.py deleted file mode 100644 index e7be73c0..00000000 --- a/tests/infer_ann.py +++ /dev/null @@ -1,3 +0,0 @@ -from dlup.annotations import WsiAnnotations - -ann = WsiAnnotations.from_geojson("/Users/jteuwen/annotations.json") diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 4e055afe..118c906c 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -9,7 +9,14 @@ import pytest import shapely.geometry -from dlup.annotations import AnnotationClass, AnnotationType, Point, Polygon, WsiAnnotations, shape +from dlup.annotations import ( + AnnotationClass, + AnnotationType, + DlupShapelyPoint, + DlupShapelyPolygon, + WsiAnnotations, + shape, +) from dlup.utils.imports import DARWIN_SDK_AVAILABLE ASAP_XML_EXAMPLE = b""" @@ -54,12 +61,12 @@ class TestAnnotations: _v7_raster_annotations = None additinoaL_point_a_cls = AnnotationClass(label="example", annotation_type=AnnotationType.POINT, color=(255, 0, 0)) - additional_point = Point((1, 2), a_cls=additinoaL_point_a_cls) + additional_point = DlupShapelyPoint((1, 2), a_cls=additinoaL_point_a_cls) additional_polygon_a_cls = AnnotationClass( label="example", annotation_type=AnnotationType.POLYGON, color=(255, 0, 0), z_index=1 ) - additional_polygon = Polygon([(0, 0), (4, 0), (4, 4), (0, 4)], a_cls=additional_polygon_a_cls) + additional_polygon = DlupShapelyPolygon([(0, 0), (4, 0), (4, 4), (0, 4)], a_cls=additional_polygon_a_cls) @property def v7_annotations(self): @@ -109,7 +116,7 @@ def test_asap_to_geojson(self): shape1 = shape(elem1["geometry"], label="") assert len(set([_.label for _ in shape0])) == 1 assert len(set([_.label for _ in shape1])) == 1 - if isinstance(shape0[0], Polygon): + if isinstance(shape0[0], DlupShapelyPolygon): complete_shape0 = shapely.geometry.MultiPolygon(shape0) complete_shape1 = shapely.geometry.MultiPolygon(shape1) else: @@ -126,7 +133,7 @@ def test_read_region(self, region): assert len(region) == 1 assert region[0].area == area assert region[0].label == "healthy glands" - assert isinstance(region[0], Polygon) + assert isinstance(region[0], DlupShapelyPolygon) if not area: assert region == [] @@ -201,8 +208,8 @@ def test_polygon_pickling(self): exterior = [(0, 0), (4, 0), (4, 4), (0, 4)] hole1 = [(1, 1), (2, 1), (2, 2), (1, 2)] hole2 = [(3, 3), (3, 3.5), (3.5, 3.5), (3.5, 3)] - dlup_polygon_with_holes = Polygon(exterior, [hole1, hole2], a_cls=annotation_class) - dlup_solid_polygon = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)], a_cls=annotation_class) + dlup_polygon_with_holes = DlupShapelyPolygon(exterior, [hole1, hole2], a_cls=annotation_class) + dlup_solid_polygon = DlupShapelyPolygon([(0, 0), (1, 0), (1, 1), (0, 1)], a_cls=annotation_class) with tempfile.NamedTemporaryFile(suffix=".pkl", mode="w+b") as pickled_polygon_file: pickle.dump(dlup_solid_polygon, pickled_polygon_file) pickled_polygon_file.flush() @@ -221,7 +228,7 @@ def test_point_pickling(self): annotation_class = AnnotationClass( label="example", annotation_type=AnnotationType.POINT, color=(255, 0, 0), z_index=None ) - dlup_point = Point([(1, 2)], a_cls=annotation_class) + dlup_point = DlupShapelyPoint([(1, 2)], a_cls=annotation_class) with tempfile.NamedTemporaryFile(suffix=".pkl", mode="w+b") as pickled_point_file: pickle.dump(dlup_point, pickled_point_file) pickled_point_file.flush() diff --git a/tests/test_background.py b/tests/test_background.py index 2da62f88..34b34598 100644 --- a/tests/test_background.py +++ b/tests/test_background.py @@ -6,7 +6,7 @@ from dlup import AnnotationType, SlideImage, WsiAnnotations from dlup._exceptions import DlupError -from dlup.annotations import AnnotationClass, Polygon +from dlup.annotations import AnnotationClass, DlupShapelyPolygon from dlup.background import compute_masked_indices from dlup.data.dataset import _coords_to_region from dlup.tiling import Grid @@ -59,8 +59,10 @@ def test_wsiannotations(self, dlup_wsi, threshold): # Let's make a shapely polygon thats equal to # background_mask[14:20, 10:20] = True # background_mask[85:100, 50:80] = True - polygon0 = Polygon(box(100, 140, 200, 200), AnnotationClass(annotation_type=AnnotationType.POLYGON, label="bg")) - polygon1 = Polygon( + polygon0 = DlupShapelyPolygon( + box(100, 140, 200, 200), AnnotationClass(annotation_type=AnnotationType.POLYGON, label="bg") + ) + polygon1 = DlupShapelyPolygon( box(500, 850, 800, 1000), AnnotationClass(annotation_type=AnnotationType.POLYGON, label="bg") ) diff --git a/tests/test_geometry.py b/tests/test_geometry.py new file mode 100644 index 00000000..3c30753f --- /dev/null +++ b/tests/test_geometry.py @@ -0,0 +1,538 @@ +# Copyright (c) dlup contributors + +"""Test the geometry classes.""" + +import copy +import pickle +import tempfile + +import pytest +import shapely.geometry +from shapely.geometry import Point as ShapelyPoint +from shapely.geometry import Polygon as ShapelyPolygon + +import dlup._geometry as dg +from dlup.geometry import ( + Box, + GeometryCollection, + Point, + Polygon, + _BaseGeometry, + _box_factory, + _point_factory, + _polygon_factory, +) + +polygons = [ + Polygon(dg.Polygon([(0, 0), (0, 3), (3, 3), (3, 0)], [])), + Polygon(dg.Polygon([(2, 2), (2, 5), (5, 5), (5, 2)], [])), + Polygon(dg.Polygon([(4, 2), (4, 7), (7, 7), (7, 4)], [])), + Polygon(dg.Polygon([(6, 6), (6, 9), (9, 9), (9, 6)], [])), +] + +points = [Point(1, 1, label="label0"), Point(4, 4, index=1), Point(6, 6), Point(8, 8)] + + +class TestGeometry: + def test_base_geometry(self): + _BaseGeometry() + with pytest.raises(NotImplementedError): + _BaseGeometry().from_shapely(None) + + with pytest.raises(NotImplementedError): + _BaseGeometry().set_field("name", "test") + + with pytest.raises(NotImplementedError): + _BaseGeometry().get_field("name") + + def test_try_to_set_incorrect_field_type(self): + base = Polygon() + with pytest.raises(ValueError): + base.label = True + with pytest.raises(ValueError): + base.color = "red" + with pytest.raises(ValueError): + base.index = "1" + + def test_point_factory(self): + c_point = dg.Point(1, 1) + point = _point_factory(c_point) + assert point == Point(1, 1) + + def test_polygon_factory(self): + c_polygon = dg.Polygon([(0, 0), (0, 3), (3, 3), (3, 0)], []) + polygon = _polygon_factory(c_polygon) + assert polygon == Polygon([(0, 0), (0, 3), (3, 3), (3, 0)]) + + def test_box_factory(self): + c_box = dg.Box((1, 1), (2, 2)) + box = _box_factory(c_box) + assert box == Box((1, 1), (2, 2)) + + def test_box_area(self): + box = Box((1, 1), (2, 2)) + box.area == 4 + box.as_polygon().area == box.area + + def box_to_polygon(self): + box = Box((1, 1), (2, 2)) + polygon = box.as_polygon() + assert isinstance(box, Box) + assert isinstance(polygon, Polygon) + assert polygon == Polygon([(1, 1), (1, 2), (2, 2), (2, 1)]) + + @pytest.mark.parametrize( + "exterior,interiors,expected_area", + [ + ( + [(0, 0), (0, 3), (3, 3), (3, 0)], + [[(1, 1), (1, 2), (2, 2), (2, 1)], [(1.5, 1.5), (1.5, 2.5), (2.5, 2.5), (2.5, 1.5)]], + 7.0, + ) + ], + ) + def test_if_area_is_correct(self, exterior, interiors, expected_area): + shapely_polygon = shapely.geometry.Polygon(exterior, interiors) + dlup_polygon = Polygon(exterior, interiors) + assert dlup_polygon.area == dlup_polygon.to_shapely().area == shapely_polygon.area == expected_area + + @pytest.mark.parametrize( + "field_name,field_value", + [ + ("arbitrary field", [1, 2, 3, 4]), + ("random", None), + ], + ) + def test_set_arbitrary_field(self, field_name, field_value): + polygon = Polygon([(0, 0), (0, 3), (3, 3), (3, 0)]) + polygon.set_field(field_name, field_value) + assert polygon.get_field(field_name) == field_value + + @pytest.mark.parametrize("object_to_pickle", [polygons[0], points[0]]) + def test_pickle_objects(self, object_to_pickle): + object_to_pickle = copy.deepcopy(object_to_pickle) + object_to_pickle.set_field("random", True) + with tempfile.NamedTemporaryFile() as f: + pickle.dump(object_to_pickle, f) + f.seek(0) + new_object = pickle.load(f) + assert new_object == object_to_pickle + + def test_repr(self): + polygon = Polygon([(1, 1), (2, 3), (3, 4), (0, 0)], label="label", index=1, color=(1, 1, 1)) + polygon.set_field("random", True) + assert ( + repr(polygon) + == "" + ) + + point = Point(1, 1, label="label", index=1, color=(1, 1, 1)) + assert repr(point) == "" + + polygon = Polygon([(1, 1) for _ in range(100)]) + assert repr(polygon) == "" + + @pytest.mark.parametrize("original_object", polygons + points) + def test_deep_copy(self, original_object): + copied_object = copy.deepcopy(original_object) + assert original_object is not copied_object + assert original_object == copied_object + + def test_copy_polygon(self): + polygon = polygons[0] + polygon_copy = copy.copy(polygon) + + # TODO: Figure out way to not make a copy. Creating an InteriorRing object might be an option. + assert polygon.get_interiors() == polygon_copy.get_interiors() + assert polygon.get_exterior() == polygon_copy.get_exterior() + + def test_copy_point(self): + point = points[0] + point_copy = copy.copy(point) + + assert point == point_copy + + def test_collection_add_object(self): + collection = GeometryCollection() + collection.add_polygon(polygons[0]) + collection.add_polygon(polygons[1]) + assert collection.polygons == polygons[:2] + + collection.add_point(points[0]) + collection.add_point(points[1]) + assert collection.points == points[:2] + + def test_if_keeps_reference(self): + collection = GeometryCollection() + for polygon in polygons: + collection.add_polygon(polygon) + + for point in points: + collection.add_point(point) + + for idx, polygon in enumerate(collection.polygons): + assert polygon == polygons[idx] + assert polygon.pointer_id == polygons[idx].pointer_id + + for idx, point in enumerate(collection.points): + assert point == points[idx] + assert point.pointer_id == points[idx].pointer_id + + def test_pointers(self): + pointers = [poly.pointer_id for poly in polygons] + point_pointers = [point.pointer_id for point in points] + + collection = GeometryCollection() + for poly in polygons: + collection.add_polygon(poly) + + for point in points: + collection.add_point(point) + + for idx, poly in enumerate(collection.polygons): + assert poly.pointer_id == pointers[idx] + + for idx, point in enumerate(collection.points): + assert point.pointer_id == point_pointers[idx] + + def test_remove_geometry_from_collection(self): + collection = GeometryCollection() + for poly in polygons: + collection.add_polygon(poly) + + for point in points: + collection.add_point(point) + + assert collection.rtree_invalidated + + assert len(collection.polygons) == 4 + assert len(collection.points) == 4 + + collection.remove_polygon(polygons[0]) + assert collection.polygons == polygons[1:] + assert len(collection.polygons) == 3 + + collection.remove_point(points[0]) + assert len(collection.points) == 3 + + collection.remove_polygon(0) + assert len(collection.polygons) == 2 + + collection.remove_point(0) + assert len(collection.points) == 2 + + assert collection.rtree_invalidated + collection.rebuild_rtree() + assert not collection.rtree_invalidated + + def test_wkt(self): + polygon = Polygon([(0, 0), (0, 3), (3, 3), (3, 0)], [[(1, 1), (1, 2), (2, 2), (2, 1)]]) + assert polygon.wkt == "POLYGON((0 0,0 3,3 3,3 0,0 0),(1 1,1 2,2 2,2 1,1 1))" + + @pytest.mark.parametrize("object_type", [Polygon, Point]) + def test_setting_properties(self, object_type): + obj = object_type() + obj.label = "test" + obj.color = (1, 1, 1) + + if isinstance(obj, Polygon): + obj.index = 1 + assert obj.index == 1 + + assert obj.label == "test" + assert obj.color == (1, 1, 1) + + def test_color_lut(self): + collection = GeometryCollection() + for idx, polygon in enumerate(polygons): + collection.add_polygon(polygon) + polygon.set_field("label", f"label {idx}") + polygon.set_field("color", (idx + 1, idx + 1, idx + 1)) + polygon.set_field("index", idx + 1) + + # Add expected color LUT test here + + def test_close_loop(self): + polygon = Polygon([(0, 0), (0, 3), (3, 3), (3, 0)], [[(1, 1), (1, 2), (2, 2), (2, 1)]]) + + assert polygon.get_exterior() == [(0, 0), (0, 3), (3, 3), (3, 0), (0, 0)] + assert polygon.get_interiors() == [[(1, 1), (1, 2), (2, 2), (2, 1), (1, 1)]] + + def test_from_shapely_polygon(self): + exterior = [(0, 0), (0, 3), (3, 3), (3, 0)] + interiors = [[(1, 1), (1, 2), (2, 2), (2, 1)]] + + shapely_polygon = ShapelyPolygon(exterior, interiors) + polygon_converted = Polygon.from_shapely(shapely_polygon) + polygon_direct = Polygon(exterior, interiors) + + polygon_shapely_2 = Polygon(shapely_polygon) + assert polygon_shapely_2 == polygon_converted + + assert ( + polygon_converted.get_exterior() == list(shapely_polygon.exterior.coords) == polygon_direct.get_exterior() + ) + assert ( + polygon_converted.get_interiors() + == [list(_.coords) for _ in shapely_polygon.interiors] + == polygon_direct.get_interiors() + ) + assert shapely_polygon == polygon_direct.to_shapely() + + def test_from_shapely_point(self): + dlup_point = Point(1, 1) + shapely_point = ShapelyPoint(1, 1) + dlup_point2 = Point(shapely_point) + + assert dlup_point2 == dlup_point + + assert dlup_point == Point.from_shapely(shapely_point) + assert dlup_point.to_shapely() == shapely_point + + @pytest.mark.parametrize("object_type", [Point, Polygon]) + def test_shapely_wrong_type(self, object_type): + with pytest.raises(ValueError): + object_type.from_shapely([]) + + def test_sort_polygon(self): + collection = GeometryCollection() + for poly in polygons: + collection.add_polygon(poly) + + assert [_.area for _ in collection.polygons] == [9.0, 9.0, 12.0, 9.0] + + collection.sort_polygons(lambda x: x.area, True) + + assert [_.area for _ in collection.polygons] == [12.0, 9.0, 9.0, 9.0] + assert collection.polygons[0] == polygons[2] + + collection.sort_polygons(lambda x: x.area, False) + assert [_.area for _ in collection.polygons] == [9.0, 9.0, 9.0, 12.0] + assert collection.polygons[0] == polygons[0] + + @pytest.mark.parametrize("object_type", [Point, Polygon]) + def test_to_shapely_missing(self, object_type, monkeypatch): + monkeypatch.setattr("dlup.geometry.SHAPELY_AVAILABLE", False) + with pytest.raises(ImportError): + object_type().to_shapely() + + @pytest.mark.parametrize("object_type", [ShapelyPoint, ShapelyPolygon]) + def test_from_shapely_missing(self, object_type, monkeypatch): + monkeypatch.setattr("dlup.geometry.SHAPELY_AVAILABLE", False) + with pytest.raises(ImportError): + if object_type == ShapelyPoint: + Point.from_shapely(object_type()) + else: + Polygon.from_shapely(object_type()) + + def test_point_scaling(self): + point = Point(1, 1) + pointer_id = point.pointer_id + point.scale(2) + + assert point == Point(2, 2) + assert point.pointer_id == pointer_id + + def test_polygon_scaling(self): + polygon = Polygon([(0, 0), (0, 3), (3, 3), (3, 0)], [[(1, 1), (1, 2), (2, 2), (2, 1)]]) + pointer_id = polygon.pointer_id + polygon.scale(2) + + assert polygon == Polygon([(0, 0), (0, 6), (6, 6), (6, 0)], [[(2, 2), (2, 4), (4, 4), (4, 2)]]) + assert polygon.pointer_id == pointer_id + + @pytest.mark.parametrize("scaling", [1.0, 2.0]) + def test_read_region(self, scaling): + collection = GeometryCollection() + for poly in polygons: + collection.add_polygon(poly) + + for idx, poly in enumerate(polygons): + poly.set_field("label", f"label {idx}") + + assert collection.rtree_invalidated + region = collection.read_region((2, 2), scaling, (10, 10)) + # It's still invalid because of the lazy evaluation! + assert collection.rtree_invalidated + region.polygons # Call to ensure that the polygons are obtained + assert not collection.rtree_invalidated + + # TODO: Add more elaborate tests for regions + + @pytest.mark.parametrize("shapely_available", [True, False]) + def test_importerror_for_from_shapely(self, shapely_available, monkeypatch): + def mock_shapely_available(): + return shapely_available + + monkeypatch.setattr("dlup.geometry.SHAPELY_AVAILABLE", shapely_available) + + if shapely_available: + shapely_polygon = ShapelyPolygon([(0, 0), (0, 3), (3, 3), (3, 0)]) + Polygon.from_shapely(shapely_polygon) + Polygon(shapely_polygon) + else: + with pytest.raises(ImportError): + Polygon.from_shapely(None) + + def test_compare_mismatch(self): + point = Point(1, 1) + polygon = Polygon([(0, 0), (0, 3), (3, 3), (3, 0)], [[(1, 1), (1, 2), (2, 2), (2, 1)]]) + + assert point != polygon + + def test_compare_incorrect_fields(self): + point0 = Point(1, 1, label="label0") + point1 = Point(1, 1, label="label1") + + assert point0 != point1 + + polygon0 = copy.deepcopy(polygons[0]) + polygon1 = copy.deepcopy(polygons[1]) + polygon1.set_field("random", False) + + assert polygon0 != polygon1 + + def test_cannot_add_geometries(self): + with pytest.raises(TypeError): + polygons[0] += points[0] + + with pytest.raises(TypeError): + polygons[0] += polygons[0] + + def test_cannot_subtract_geometries(self): + with pytest.raises(TypeError): + polygons[0] -= points[0] + + with pytest.raises(TypeError): + polygons[0] -= polygons[0] + + def test_inequality(self): + polygon0 = Polygon([(0, 0), (0, 3), (3, 3), (3, 0)], []) + polygon1 = Polygon([(0, 0), (1, 3), (3, 3), (3, 0)], []) + + polygon0.label = "test" + polygon1.label = "test" + + assert polygon0 != polygon1 + + point0 = Point(0, 1) + point1 = Point(1, 1) + + point0.color = (1, 2, 3) + point1.color = (1, 2, 3) + + assert polygon0 != point1 + assert point0 != point1 + + def test_geometry_collection_lut(self): + collection = GeometryCollection() + for idx, polygon in enumerate(polygons[:3]): + collection.add_polygon(polygon) + polygon.set_field("label", f"label {idx}") + polygon.set_field("color", (idx + 1, idx + 1, idx + 1)) + polygon.set_field("index", idx + 1) + + assert collection.color_lut.tolist() == [[0, 0, 0], [1, 1, 1], [2, 2, 2], [3, 3, 3]] + + def test_geometry_collection_lut_exceptions(self): + collection = GeometryCollection() + polygon = Polygon([(0, 0), (0, 3), (3, 3), (3, 0)], []) + collection.add_polygon(polygon) + with pytest.raises(ValueError): + collection.color_lut + + polygon.index = 1 + with pytest.raises(ValueError): + collection.color_lut + + def test_geometry_collection_pickle(self): + collection = GeometryCollection() + for polygon in polygons: + collection.add_polygon(polygon) + + for point in points: + collection.add_point(point) + + with tempfile.NamedTemporaryFile() as f: + pickle.dump(collection, f) + f.seek(0) + new_collection = pickle.load(f) + assert new_collection == collection + + def test_geometry_collection_length(self): + collection = GeometryCollection() + for polygon in polygons: + collection.add_polygon(polygon) + + assert len(collection) == 4 + + for point in points: + collection.add_point(point) + + assert len(collection) == 8 + + def test_geometry_equality_different_type_and_length(self): + collection0 = GeometryCollection() + assert collection0 is not None + collection1 = GeometryCollection() + + assert collection0 == collection1 + collection0.add_polygon(polygons[0]) + collection1.add_polygon(polygons[1]) + assert collection0 != collection1 + + collection0.remove_polygon(polygons[0]) + collection1.remove_polygon(polygons[1]) + + collection0.add_point(points[0]) + collection1.add_point(points[1]) + assert collection0 != collection1 + + def test_geometry_read_region(self): + collection = GeometryCollection() + + # Let's make a nice polygon that's a square + polygon = Polygon([(0, 0), (0, 8), (8, 8), (8, 0)], []) + + collection.add_polygon(polygon) + + for point in points: + collection.add_point(point) + + regions = collection.read_region((2, 2), 1.0, (5, 5)) + + assert len(regions.points) == 2 + assert len(regions.polygons.get_geometries()) == 1 + assert regions.points == [Point(2, 2, index=1), Point(4, 4)] + assert regions.polygons.get_geometries() == [Polygon([(0, 0), (0, 5), (5, 5), (5, 0)], [])] + + def test_geometry_scaling(self): + collection = GeometryCollection() + for polygon in polygons: + collection.add_polygon(polygon) + + for point in points: + collection.add_point(point) + + collection.scale(2) + + polygon0 = collection.polygons[0] + points0 = collection.points[0] + + assert points0 == Point(2, 2, label="label0") + assert polygon0.get_exterior() == [(0, 0), (0, 6), (6, 6), (6, 0), (0, 0)] + assert polygon0.get_interiors() == [] + + assert polygon0 == Polygon( + [(0, 0), (0, 6), (6, 6), (6, 0), (0, 0)], [], color=(1, 1, 1), index=1, label="label 0" + ) + + def test_mask(self): + collection = GeometryCollection() + polygon = Box((1, 1), (4, 4)).as_polygon() + polygon.index = 2 + collection.add_polygon(polygon) + + region = collection.read_region((0, 0), 1.0, (5, 5)) + mask = region.polygons.to_mask().numpy() + assert mask.sum() == 16 * 2 diff --git a/tests/test_slide_annotations.py b/tests/test_slide_annotations.py new file mode 100644 index 00000000..0f9922dc --- /dev/null +++ b/tests/test_slide_annotations.py @@ -0,0 +1,552 @@ +# Copyright (c) dlup contributors + +"""Test the annotation facilities.""" +import copy +import json +import os +import pathlib +import pickle +import tempfile + +import numpy as np +import pytest + +from dlup.annotations_experimental import GeometryCollection, SlideAnnotations, geojson_to_dlup, get_v7_metadata +from dlup.geometry import Point as Point +from dlup.geometry import Polygon as Polygon +from dlup.utils.imports import DARWIN_SDK_AVAILABLE + +ASAP_XML_EXAMPLE = b""" + + + + + + + + + + + + + + + + + + + + + +""" + +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# + + +DLUP_XML_EXAMPLE = b""" + + IMG_12345 + Sample annotations with polygons, multipolygons, points, and boxes. + 1.0 + + Jane Doe + John Smith + + 2024-08-19 + dlup v0.8.0 + + + + + Attribute 1 + Attribute 2 + This is the single text field for this tag. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +polygons = [ + Polygon([(0, 0), (0, 3), (3, 3), (3, 0)], []), + Polygon([(2, 2), (2, 5), (5, 5), (5, 2)], []), + Polygon([(4, 2), (4, 7), (7, 7), (7, 4)], []), + Polygon([(6, 6), (6, 9), (9, 9), (9, 6)], []), +] + + +class TestAnnotations: + with tempfile.NamedTemporaryFile(suffix=".xml") as asap_file: + asap_file.write(ASAP_XML_EXAMPLE) + asap_file.flush() + asap_annotations = SlideAnnotations.from_asap_xml(pathlib.Path(asap_file.name)) + asap_annotations.rebuild_rtree() + + with tempfile.NamedTemporaryFile(suffix=".xml") as dlup_file: + dlup_file.write(DLUP_XML_EXAMPLE) + dlup_file.flush() + dlup_annotations = SlideAnnotations.from_dlup_xml(pathlib.Path(dlup_file.name)) + + with tempfile.NamedTemporaryFile(suffix=".json") as geojson_out: + asap_geojson = asap_annotations.as_geojson() + geojson_out.write(json.dumps(asap_geojson).encode("utf-8")) + geojson_out.flush() + + geojson_annotations = SlideAnnotations.from_geojson(pathlib.Path(geojson_out.name)) + + _v7_annotations = None + _v7_raster_annotations = None + + _halo_annotations = None + + additional_point = Point(*(1, 2), label="example", color=(255, 0, 0)) + additional_polygon = Polygon([(0, 0), (4, 0), (4, 4), (0, 4)], label="example", color=(255, 0, 0)) + additional_polygon.set_field("z_index", 1) + + @property + def v7_annotations(self): + if self._v7_annotations is None: + assert pathlib.Path(pathlib.Path(__file__).parent / "files/103S.json").exists() + self._v7_annotations = SlideAnnotations.from_darwin_json(pathlib.Path(__file__).parent / "files/103S.json") + return self._v7_annotations + + @property + def halo_annotations(self): + if self._halo_annotations is None: + assert pathlib.Path(pathlib.Path(__file__).parent / "files/halo_holes.annotations").exists() + self._halo_annotations = SlideAnnotations.from_halo_xml( + pathlib.Path(__file__).parent / "files/halo_holes.annotations", + box_as_polygon=False, + ) + return self._halo_annotations + + def test_raster_annotations(self): + if self._v7_raster_annotations is None: + assert pathlib.Path(pathlib.Path(__file__).parent / "files/raster.json").exists() + with pytest.raises(NotImplementedError): + SlideAnnotations.from_darwin_json(pathlib.Path(__file__).parent / "files/raster.json") + + def test_conversion_geojson_v7(self): + # We need to read the asap annotations and compare them to the geojson annotations + with tempfile.NamedTemporaryFile(suffix=".json") as geojson_out: + geojson_out.write(json.dumps(self.v7_annotations.as_geojson()).encode("utf-8")) + geojson_out.flush() + annotations = SlideAnnotations.from_geojson(pathlib.Path(geojson_out.name), sorting="NONE") + + assert self.v7_annotations.num_points == annotations.num_points + assert self.v7_annotations.num_polygons == annotations.num_polygons + + assert self.v7_annotations.layers.polygons == annotations.layers.polygons + assert self.v7_annotations.layers.points == annotations.layers.points + + self.v7_annotations.rebuild_rtree() + annotations.rebuild_rtree() + + v7_region = self.v7_annotations.read_region((15300, 19000), 1.0, (2500.0, 2500.0)) + geojson_region = annotations.read_region((15300, 19000), 1.0, (2500.0, 2500.0)) + + assert len(v7_region.polygons.get_geometries()) == len(geojson_region.polygons.get_geometries()) + + for elem0, elem1 in zip(v7_region.polygons.get_geometries(), geojson_region.polygons.get_geometries()): + assert elem0.wkt == elem1.wkt + assert elem0.label == elem1.label + elem0.index = 1 + elem1.index = 1 + + assert np.allclose(v7_region.polygons.to_mask(), geojson_region.polygons.to_mask()) + + for elem0, elem1 in zip(v7_region.points, geojson_region.points): + assert elem0.wkt == elem1.wkt + assert elem0.label == elem1.label + + def test_conversion_halo_geojson(self): + # We read the halo annotations and compare them to the geojson annotations + with tempfile.NamedTemporaryFile(suffix=".json") as geojson_out: + geojson_out.write(json.dumps(self.halo_annotations.as_geojson()).encode("utf-8")) + geojson_out.flush() + geojson_annotations = SlideAnnotations.from_geojson(pathlib.Path(geojson_out.name), sorting="NONE") + geojson_annotations.offset_function = self.halo_annotations.offset_function + + assert self.halo_annotations.num_points == geojson_annotations.num_points + assert self.halo_annotations.num_polygons == geojson_annotations.num_polygons + assert self.halo_annotations.layers.polygons == geojson_annotations.layers.polygons + assert self.halo_annotations.layers.points == geojson_annotations.layers.points + # This won't work because we have no boxes in GeoJSON + assert self.halo_annotations.layers.boxes != geojson_annotations.layers.boxes + # assert self.halo_annotations.__eq__(geojson_annotations) + + def test_halo_annotations(self): + halo_annotations = self.halo_annotations.copy() + bounding_box = halo_annotations.bounding_box + assert bounding_box[0] == (-29349.0, 50000.55808864343) + halo_annotations.set_offset((29349.0, -50000.55808864343)) + assert halo_annotations.bounding_box[0] == (0, 0) + + for polygon in halo_annotations.layers.polygons: + polygon.index = 1 + + new_bbox = halo_annotations.bounding_box_at_scaling(0.01) + region = halo_annotations.read_region(new_bbox[0], 0.01, new_bbox[1]) + output_color_mask = halo_annotations.color_lut[region.polygons.to_mask().numpy()] + assert output_color_mask.sum() == 51485183 + + def test_reexpert_dlup_xml(self): + with tempfile.NamedTemporaryFile(suffix=".xml") as dlup_file: + with open(dlup_file.name, "w") as f: + f.write(self.dlup_annotations.as_dlup_xml()) + + annotations = SlideAnnotations.from_dlup_xml(dlup_file.name) + assert self.dlup_annotations._layers == annotations._layers + assert self.dlup_annotations.tags == annotations.tags + assert self.dlup_annotations.sorting == annotations.sorting + assert self.dlup_annotations.offset_function == annotations.offset_function + assert self.dlup_annotations == annotations + + def test_reading_qupath05_geojson_export(self): + annotations = SlideAnnotations.from_geojson(pathlib.Path("tests/files/qupath05.geojson")) + assert len(annotations.available_classes) == 2 + + @pytest.mark.parametrize("class_method", ["from_geojson", "from_halo_xml", "from_dlup_xml", "from_asap_xml"]) + def test_missing_file_constructor(self, class_method): + constructor = getattr(SlideAnnotations, class_method) + with pytest.raises(FileNotFoundError): + constructor("doesnotexist.xml.json") + + def test_asap_to_geojson(self): + # TODO: Make sure that the annotations hit the border of the region. + asap_geojson = self.asap_annotations.as_geojson() + geojson_geojson = self.geojson_annotations.as_geojson() + assert len(asap_geojson) == len(geojson_geojson) + + # TODO: Collect the geometries together per name and compare + for elem0, elem1 in zip(asap_geojson["features"], geojson_geojson["features"]): + assert elem0["type"] == elem1["type"] + assert elem0["properties"] == elem1["properties"] + assert elem0["id"] == elem1["id"] + + # Now we need to compare the geometries, given the sorting they could become different + shape0 = geojson_to_dlup(elem0["geometry"], label="") + shape1 = geojson_to_dlup(elem1["geometry"], label="") + assert len(set([_.label for _ in shape0])) == 1 + assert len(set([_.label for _ in shape1])) == 1 + if isinstance(shape0[0], Polygon): + pass + else: + raise NotImplementedError("Different shape types not implemented yet.") + + for p0, p1 in zip(shape0, shape1): + # The shapes should be equal + assert p0 == p1 + + @pytest.mark.parametrize("region", [((10000, 10000), (5000, 5000), 3756.0), ((0, 0), (5000, 5000), None)]) + def test_read_region(self, region): + coordinates, size, area = region + region = self.asap_annotations.read_region(coordinates, 1.0, size) + + polygons = region.polygons.get_geometries() + + if area and area > 0: + assert len(polygons) == 1 + assert polygons[0].area == area + assert polygons[0].label == "healthy glands" + assert isinstance(polygons[0], Polygon) + + if not area: + assert region.polygons.get_geometries() == [] + assert region.points == [] + + def test_copy(self): + copied_annotations = copy.copy(self.asap_annotations) + + copied_annotations.tags == self.asap_annotations.tags + copied_annotations._layers = self.asap_annotations._layers + + def test_pickle(self): + with tempfile.NamedTemporaryFile(suffix=".pkl") as pkl_file: + pickle.dump(self.asap_annotations, pkl_file) + pkl_file.flush() + + with open(pkl_file.name, "rb") as pkl_file: + annotations = pickle.load(pkl_file) + + assert annotations.tags == self.asap_annotations.tags + assert annotations._layers == self.asap_annotations._layers + + def test_reindex_polygons(self): + ann = self.dlup_annotations.copy() + ann.reindex_polygons({"Polygon1": 7}) + for polygon in ann._layers.polygons: + assert polygon.index == 7 + + def test_relabel_polygons(self): + ann = self.dlup_annotations.copy() + ann.relabel_polygons({"Polygon1": "Polygon2"}) + for polygon in ann._layers.polygons: + assert polygon.label == "Polygon2" + + @pytest.mark.parametrize("scaling", [0.5, 0.3, 1.0]) + def test_bounding_box(self, scaling): + assert self.v7_annotations.bounding_box == ( + (15291.49, 18094.48), + (5122.9400000000005, 4597.509999999998), + ) + + assert self.v7_annotations.bounding_box_at_scaling(scaling) == ( + (15291.49 * scaling, 18094.48 * scaling), + (5122.9400000000005 * scaling, 4597.509999999998 * scaling), + ) + + def test_read_darwin_v7(self): + if not DARWIN_SDK_AVAILABLE: + return None + + assert len(self.v7_annotations.available_classes) == 5 + + assert "lymphocyte (cell)" in self.v7_annotations + assert "ROI (segmentation)" in self.v7_annotations + assert "stroma (area)" in self.v7_annotations + assert "tumor (cell)" in self.v7_annotations + assert "tumor (area)" in self.v7_annotations + + assert self.v7_annotations.bounding_box == ( + (15291.49, 18094.48), + (5122.9400000000005, 4597.509999999998), + ) + + region = self.v7_annotations.read_region((15300, 19000), 1.0, (2500.0, 2500.0)) + + expected_output_polygon = [ + (6250000.0, "ROI (segmentation)"), + (1616768.0657540853, "stroma (area)"), + (398284.54274999996, "stroma (area)"), + (5124.669949999994, "stroma (area)"), + (103262.97951705182, "stroma (area)"), + (141.48809999997553, "tumor (cell)"), + (171.60999999998563, "tumor (cell)"), + (181.86480000002044, "tumor (cell)"), + (100.99830000001506, "tumor (cell)"), + (132.57199999999582, "tumor (cell)"), + (0.5479999999621504, "tumor (cell)"), + (7705.718799999958, "tumor (area)"), + (10985.104649999948, "tumor (area)"), + (585.8433000000018, "tumor (cell)"), + ] + for x, y in zip(region.polygons.get_geometries(), expected_output_polygon): + if os.environ.get("GITHUB_ACTIONS", False): + if x.area <= 1: + assert np.allclose(x.area, y[0], atol=1e-3) + else: + assert np.allclose(x.area, y[0]) + else: + assert [(_.area, _.label) for _ in region.polygons.get_geometries()] == expected_output_polygon + assert x.label == y[1] + assert len(region.points) == 3 + + def test_annotation_filter(self): + annotations = self.asap_annotations.copy() + annotations.filter(["healthy glands"]) + assert "healthy glands" in annotations + + annotations.filter_polygons(["non-existing"]) + assert len(annotations._layers.polygons) == 1 + + def test_length(self): + annotations = self.geojson_annotations + assert len(annotations._layers) == len(annotations) == 1 + + def test_dunder_add_methods_with_point(self): + annotations = self.geojson_annotations.copy() + initial_annotations_id = id(annotations) + initial_length = len(annotations) + + # __add__ + new_annotations = annotations + self.additional_point + assert initial_annotations_id != id(new_annotations) + assert initial_length + 1 == len(new_annotations) + assert self.additional_point in new_annotations + + # __radd__ + new_annotations = self.additional_point + annotations + assert initial_annotations_id != id(new_annotations) + assert initial_length + 1 == len(new_annotations) + assert self.additional_point in new_annotations + with pytest.raises(TypeError): + self.additional_point += annotations + + # __iadd__ + annotations += self.additional_point + assert initial_annotations_id == id(annotations) + assert initial_length + 1 == len(annotations) + assert self.additional_point in annotations + + def test_add_with_polygon(self): + annotations = self.geojson_annotations.copy() + initial_annotations_id = id(annotations) + initial_length = len(annotations) + + # __add__ + new_annotations = annotations + self.additional_polygon + assert initial_annotations_id != id(new_annotations) + assert initial_length + 1 == len(new_annotations) + assert self.additional_polygon in new_annotations + + # __radd__ + new_annotations = self.additional_polygon + annotations + assert initial_annotations_id != id(new_annotations) + assert initial_length + 1 == len(new_annotations) + assert self.additional_polygon in new_annotations + with pytest.raises(TypeError): + self.additional_polygon += annotations + + # __iadd__ + annotations += self.additional_polygon + assert initial_annotations_id == id(annotations) + assert initial_length + 1 == len(annotations) + assert self.additional_polygon in annotations + + def test_add_with_list(self): + annotations = self.geojson_annotations.copy() + initial_annotations_id = id(annotations) + initial_length = len(annotations) + + # __add__ + new_annotations = annotations + [self.additional_point, self.additional_polygon] + assert initial_annotations_id != id(new_annotations) + assert initial_length + 2 == len(new_annotations) + assert self.additional_polygon in new_annotations + assert self.additional_point in new_annotations + + # __radd__ + _annotations_list = [self.additional_point, self.additional_polygon] + with pytest.raises(TypeError): + new_annotations = _annotations_list + annotations + + _annotations_list = [self.additional_point, self.additional_polygon] + with pytest.raises(TypeError): + _annotations_list += annotations + + # __iadd__ + annotations += [self.additional_point, self.additional_polygon] + assert initial_annotations_id == id(annotations) + assert initial_length + 2 == len(annotations) + assert all(ann in new_annotations for ann in annotations) + + def test_add_with_wsi_annotations(self): + annotations = self.geojson_annotations.copy() + other_annotations = self.geojson_annotations.copy() + initial_annotations_id = id(annotations) + initial_length = len(annotations) + + # __add__ + new_annotations = annotations + other_annotations + assert initial_annotations_id != id(new_annotations) + assert len(annotations) + len(other_annotations) == len(new_annotations) + assert all(ann in new_annotations for ann in annotations) + + # __iadd__ + annotations += other_annotations + assert initial_annotations_id == id(annotations) + assert initial_length + len(other_annotations) == len(annotations) + assert all(ann in annotations for ann in other_annotations) + + def test_add_with_invalid_type(self): + annotations = self.geojson_annotations.copy() + with pytest.raises(TypeError): + _ = annotations + "invalid type" + with pytest.raises(TypeError): + annotations += "invalid type" + with pytest.raises(TypeError): + _ = "invalid type" + annotations + + def test_v7_metadata(self, monkeypatch): + with pytest.raises(ValueError): + get_v7_metadata(pathlib.Path("../tests")) + + monkeypatch.setattr("dlup.annotations_experimental.DARWIN_SDK_AVAILABLE", False) + with pytest.raises(ImportError): + get_v7_metadata(pathlib.Path(".")) + + @pytest.mark.parametrize("sorting_type", ["NONE", "REVERSE", "AREA", "Z_INDEX", "NON_EXISTENT"]) + def test_sorting(self, sorting_type): + collection = GeometryCollection() + for polygon in polygons: + collection.add_polygon(polygon) + + if sorting_type == "NONE": + curr_collection = collection.__copy__() + SlideAnnotations._in_place_sort_and_scale(curr_collection, scaling=1.0, sorting=sorting_type) + assert curr_collection == collection + + if sorting_type == "REVERSE": + with pytest.raises(NotImplementedError): + curr_collection = collection.__copy__() + SlideAnnotations._in_place_sort_and_scale(curr_collection, scaling=1.0, sorting=sorting_type) + # Needs fixing + # assert curr_collection.polygons == collection.polygons[::-1] + + if sorting_type == "Z_INDEX": + curr_collection = collection.__copy__() + for idx, polygon in enumerate(curr_collection.polygons): + polygon.set_field("z_index", len(curr_collection.polygons) - idx) + SlideAnnotations._in_place_sort_and_scale(curr_collection, scaling=1.0, sorting=sorting_type) + assert curr_collection.polygons == collection.polygons[::-1] + + if sorting_type == "NON_EXISTENT": + with pytest.raises(KeyError): + SlideAnnotations._in_place_sort_and_scale(collection, scaling=1.0, sorting=sorting_type) + + def test_halo_annotations_with_pins(self): + assert pathlib.Path(pathlib.Path(__file__).parent / "files/test_different_types_halo.annotations").exists() + annotations = SlideAnnotations.from_halo_xml( + pathlib.Path(__file__).parent / "files/test_different_types_halo.annotations" + ) + assert len(annotations.layers.polygons) == 5 # 2 ellipses, 3 polygons + assert len(annotations.layers.points) == 1 diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 9f382a64..b81b6af8 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -5,7 +5,7 @@ from shapely.geometry import Polygon as ShapelyPolygon # TODO: Our Polygon should support holes as well from dlup._exceptions import AnnotationError -from dlup.annotations import Point, Polygon +from dlup.annotations import DlupShapelyPoint, DlupShapelyPolygon from dlup.data.transforms import ( AnnotationClass, AnnotationType, @@ -16,7 +16,7 @@ def test_convert_annotations_points_only(): - point = Point((5, 5), a_cls=AnnotationClass(label="point1", annotation_type=AnnotationType.POINT)) + point = DlupShapelyPoint((5, 5), a_cls=AnnotationClass(label="point1", annotation_type=AnnotationType.POINT)) points, mask, roi_mask = convert_annotations([point], (10, 10), {"point1": 1}) assert mask.sum() == 0 @@ -34,7 +34,7 @@ def test_convert_annotations_default_value(): def test_convert_annotations_polygons_only(): - polygon = Polygon( + polygon = DlupShapelyPolygon( [(2, 2), (2, 8), (8, 8), (8, 2)], a_cls=AnnotationClass(label="polygon1", annotation_type=AnnotationType.POLYGON), ) @@ -49,7 +49,7 @@ def test_convert_annotations_polygons_only(): @pytest.mark.parametrize("top_add", [0.0, 0.1, 0.49, 0.51, 0.9]) @pytest.mark.parametrize("bottom_add", [0.0, 0.1, 0.49, 0.51, 0.9]) def test_convert_annotations_polygons_with_floats(top_add, bottom_add): - polygon = Polygon( + polygon = DlupShapelyPolygon( [ (2 + top_add, 2 + top_add), (2 + top_add, 8 + bottom_add), @@ -77,7 +77,7 @@ def test_convert_annotations_polygons_with_floats(top_add, bottom_add): def test_convert_annotations_label_not_present(): - polygon = Polygon( + polygon = DlupShapelyPolygon( [(1, 1), (1, 7), (7, 7), (7, 1)], a_cls=AnnotationClass(label="polygon", annotation_type=AnnotationType.POLYGON), ) @@ -86,7 +86,7 @@ def test_convert_annotations_label_not_present(): def test_roi_exception(): - box = Polygon( + box = DlupShapelyPolygon( [(1, 1), (1, 7), (7, 7), (7, 1)], a_cls=AnnotationClass(label="polygon", annotation_type=AnnotationType.BOX) ) @@ -98,15 +98,19 @@ def _create_complex_polygons(): shell0 = [(1, 1), (1, 7), (7, 7), (7, 1)] holes0 = [[(2, 2), (2, 4), (4, 4), (4, 1)]] spolygon = ShapelyPolygon(shell0, holes=holes0) - polygon0 = Polygon(spolygon, a_cls=AnnotationClass(label="polygon1", annotation_type=AnnotationType.POLYGON)) + polygon0 = DlupShapelyPolygon( + spolygon, a_cls=AnnotationClass(label="polygon1", annotation_type=AnnotationType.POLYGON) + ) shell1 = [(4, 4), (4, 9), (9, 9), (9, 4)] holes1 = [[(5, 5), (5, 7), (7, 7), (7, 5)], [(7, 7), (5, 9), (5, 9), (9, 9)]] spolygon = ShapelyPolygon(shell1, holes=holes1) - polygon1 = Polygon(spolygon, a_cls=AnnotationClass(label="polygon2", annotation_type=AnnotationType.POLYGON)) + polygon1 = DlupShapelyPolygon( + spolygon, a_cls=AnnotationClass(label="polygon2", annotation_type=AnnotationType.POLYGON) + ) shell_roi = [(3, 3), (3, 6), (6, 6), (6, 3)] - roi = Polygon(shell_roi, a_cls=AnnotationClass(label="roi", annotation_type=AnnotationType.POLYGON)) + roi = DlupShapelyPolygon(shell_roi, a_cls=AnnotationClass(label="roi", annotation_type=AnnotationType.POLYGON)) target = np.asarray( [ @@ -140,7 +144,7 @@ def test_convert_annotations_multiple_polygons_and_holes(): def test_convert_annotations_out_of_bounds(): - polygon = Polygon( + polygon = DlupShapelyPolygon( [(2, 2), (2, 11), (11, 11), (11, 2)], a_cls=AnnotationClass(label="polygon1", annotation_type=AnnotationType.POLYGON), ) @@ -192,7 +196,7 @@ def transformer1(self): return RenameLabels(remap_labels={"old_name": "new_name", "some_point": "some_point2", "some_box": "some_box"}) def test_no_remap(self, transformer0): - old_annotation = Polygon( + old_annotation = DlupShapelyPolygon( [(2, 2), (2, 8), (8, 8), (8, 2)], a_cls=AnnotationClass(label="unchanged_name", annotation_type=AnnotationType.POLYGON), ) @@ -201,24 +205,26 @@ def test_no_remap(self, transformer0): assert transformed_sample["annotations"][0].label == "unchanged_name" def test_remap_polygon(self, transformer1): - old_annotation = Polygon( + old_annotation = DlupShapelyPolygon( [(2, 2), (2, 8), (8, 8), (8, 2)], a_cls=AnnotationClass(label="old_name", annotation_type=AnnotationType.POLYGON), ) - random_box = Polygon( + random_box = DlupShapelyPolygon( [(2, 2), (2, 8), (8, 8), (8, 2)], a_cls=AnnotationClass(label="some_box", annotation_type=AnnotationType.BOX), ) - random_point = Point((1, 1), a_cls=AnnotationClass(label="some_point", annotation_type=AnnotationType.POINT)) + random_point = DlupShapelyPoint( + (1, 1), a_cls=AnnotationClass(label="some_point", annotation_type=AnnotationType.POINT) + ) sample = {"annotations": [old_annotation, random_box, random_point]} transformed_sample = transformer1(sample) assert transformed_sample["annotations"][0].label == "new_name" assert transformed_sample["annotations"][1].label == "some_box" assert transformed_sample["annotations"][2].label == "some_point2" - assert isinstance(transformed_sample["annotations"][0], Polygon) + assert isinstance(transformed_sample["annotations"][0], DlupShapelyPolygon) assert transformed_sample["annotations"][0].annotation_type == AnnotationType.POLYGON assert transformed_sample["annotations"][1].annotation_type == AnnotationType.BOX assert transformed_sample["annotations"][2].annotation_type == AnnotationType.POINT diff --git a/tests/utils/test_annotation_utils.py b/tests/utils/test_annotation_utils.py new file mode 100644 index 00000000..b2d4966d --- /dev/null +++ b/tests/utils/test_annotation_utils.py @@ -0,0 +1,48 @@ +# Copyright (c) dlup contributors +import pytest + +from dlup.utils.annotations_utils import hex_to_rgb, rgb_to_hex + + +@pytest.mark.parametrize("rgb", [(0, 0, 0), (255, 10, 255), (255, 127, 0), (0, 28, 0), (0, 0, 255)]) +def test_rgb_to_hex_to_rgb(rgb): + hex_repr = rgb_to_hex(*rgb) + rgb2 = hex_to_rgb(hex_repr) + assert rgb == rgb2 + + +def test_fixed_colors(): + assert hex_to_rgb("black") == (0, 0, 0) + + +def test_exceptions(): + with pytest.raises(ValueError): + rgb_to_hex(256, 0, 0) + with pytest.raises(ValueError): + rgb_to_hex(0, 256, 0) + with pytest.raises(ValueError): + rgb_to_hex(0, 0, 256) + with pytest.raises(ValueError): + rgb_to_hex(-1, 0, 0) + with pytest.raises(ValueError): + rgb_to_hex(0, -1, 0) + with pytest.raises(ValueError): + rgb_to_hex(0, 0, -1) + with pytest.raises(ValueError): + hex_to_rgb("1234567") + with pytest.raises(ValueError): + hex_to_rgb("#1234567") + with pytest.raises(ValueError): + hex_to_rgb("#12345") + with pytest.raises(ValueError): + hex_to_rgb("#1234") + with pytest.raises(ValueError): + hex_to_rgb("#123") + with pytest.raises(ValueError): + hex_to_rgb("#12") + with pytest.raises(ValueError): + hex_to_rgb("#1") + with pytest.raises(ValueError): + hex_to_rgb("#") + with pytest.raises(ValueError): + hex_to_rgb("") diff --git a/third_party/meson.build b/third_party/meson.build new file mode 100644 index 00000000..293b5ab6 --- /dev/null +++ b/third_party/meson.build @@ -0,0 +1,66 @@ +### third_party/meson.build ### + +py_mod = import('python') +py = py_mod.find_installation(pure: false) +py_dep = py.dependency() + +python_path = run_command(py, ['-c', 'import sys; print(sys.executable)'], check: true).stdout().strip() +message('Using Python: ' + python_path) + +# LibTIFF +libtiff_dep = dependency('libtiff-4', required : false) +if not libtiff_dep.found() + libtiff_dep = dependency('tiff', required : false) +endif +if not libtiff_dep.found() + libtiff_dep = cc.find_library('tiff', required : false) +endif +if not libtiff_dep.found() + error('libtiff not found. Please install libtiff development files.') +endif + +# ZSTD +zstd_dep = dependency('libzstd', required : false) +have_zstd = zstd_dep.found() +if have_zstd + message('ZSTD support enabled') +else + message('ZSTD support disabled') +endif + +# Numpy and PYBIND11 +numpy_include = run_command(py, ['-c', ''' +import os +import numpy +print(os.path.relpath(numpy.get_include(), os.getcwd())) +'''], check: true).stdout().strip() + +pybind11_include = run_command(py, ['-c', ''' +import os +import pybind11 +print(os.path.relpath(pybind11.get_include(), os.getcwd())) +'''], check: true).stdout().strip() + +incdir_numpy = include_directories(numpy_include) +incdir_pybind11 = include_directories(pybind11_include) + +# Find Boost with necessary components +boost_modules = ['system', 'serialization'] +boost_dep = dependency('boost', modules : boost_modules, required : true) + +# OpenCV +opencv_dep = dependency('opencv4', required : true) + +# Shared dependencies +base_deps = [libtiff_dep] +if have_zstd + base_deps += [zstd_dep] + cpp_args += ['-DHAVE_ZSTD'] +endif + +# Export dependencies and includes +incdir_numpy = incdir_numpy +incdir_pybind11 = incdir_pybind11 +base_deps = base_deps +boost_dep = boost_dep +opencv_dep = opencv_dep diff --git a/tox.ini b/tox.ini index 554cf1f1..ba9f2262 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,11 @@ [tox] -envlist = py310, py311 +envlist = py311 isolated_build = True [testenv] +envdir = {toxworkdir}/env +setenv = + GITHUB_ACTIONS = 1 deps = meson meson-python>=0.15.0 @@ -11,12 +14,10 @@ deps = spin pybind11 build + pyhaloxml extras = dev,darwin commands = - sh -c 'meson setup builddir' - sh -c 'meson compile -C builddir' - sh -c 'cp builddir/*.so dlup' - pytest + pytest --maxfail=1 --disable-warnings allowlist_externals = sh pytest