diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 395ba2b9..0a72442e 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -11,6 +11,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build the Docker image run: docker build . --file Dockerfile --tag ${{ github.repository }}:$(date +%s) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index af5d1211..f0028c37 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -13,7 +13,7 @@ jobs: contents: read steps: - name: Check out the repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Log in to Docker Hub uses: docker/login-action@v2 diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index ea431b78..2ebc2d83 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -7,12 +7,12 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Install GDAL run: | python -m pip install --upgrade pip @@ -26,11 +26,6 @@ jobs: pip install --no-cache-dir Cython pip install -r requirements.txt -r requirements_dev.txt -r requirements_docs.txt pip install . - - name: Discover typos with codespell - run: codespell --skip="*.csv,*.geojson,*.json,*.js,*.html,*cff,*.pdf" --ignore-words-list="aci,acount,acounts,fallow,hart,hist,nd,ned,ois,wqs,pres" - # - name: PKG-TEST - # run: | - # python -m unittest discover tests/ - run: mkdocs build - name: Deploy to Netlify uses: nwtgck/actions-netlify@v2.0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 87f9cd4a..0750f827 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,10 +7,10 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/draft-pdf.yml b/.github/workflows/draft-pdf.yml index ac29ec02..ade8a486 100644 --- a/.github/workflows/draft-pdf.yml +++ b/.github/workflows/draft-pdf.yml @@ -6,7 +6,7 @@ jobs: name: Paper Draft steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build draft PDF uses: openjournals/openjournals-draft-action@master with: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index d0cfe19d..ca67f825 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -15,14 +15,14 @@ jobs: fail-fast: false matrix: config: - - { os: macOS-latest, py: "3.10" } + - { os: macOS-latest, py: "3.11" } env: SDKROOT: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk steps: - name: CHECKOUT CODE - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: SETUP PYTHON - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.config.py }} # - name: Install GDAL diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 2bbb0c08..ae450659 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -12,9 +12,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install dependencies diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 6e5dcd7f..b17609d8 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -24,9 +24,9 @@ jobs: SDKROOT: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk steps: - name: CHECKOUT CODE - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: SETUP PYTHON - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.config.py }} - name: Install GDAL diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index a01f3b15..dc67168a 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -11,9 +11,9 @@ jobs: test-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install miniconda - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v3 with: auto-activate-base: true python-version: "3.10" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..de6cf879 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + types: [python] + - id: trailing-whitespace + - id: requirements-txt-fixer + - id: check-added-large-files + args: ["--maxkb=500"] + + - repo: https://github.com/psf/black + rev: 24.4.2 + hooks: + - id: black-jupyter + language_version: python3.11 + + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + args: [--toml, pyproject-codespell.precommit-toml] + + - repo: https://github.com/kynan/nbstripout + rev: 0.7.1 + hooks: + - id: nbstripout diff --git a/Dockerfile b/Dockerfile index 0371ddd5..bc1542c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ LABEL repo="https://github.com/opengeos/segment-geospatial" RUN mamba install -c conda-forge leafmap localtileserver segment-geospatial -y && \ pip install -U segment-geospatial jupyter-server-proxy && \ - jupyter serverextension enable --sys-prefix jupyter_server_proxy && \ + jupyter server extension enable --sys-prefix jupyter_server_proxy && \ fix-permissions "${CONDA_DIR}" && \ fix-permissions "/home/${NB_USER}" @@ -18,6 +18,5 @@ ARG LOCALTILESERVER_CLIENT_PREFIX='proxy/{port}' ENV LOCALTILESERVER_CLIENT_PREFIX=$LOCALTILESERVER_CLIENT_PREFIX USER root -RUN apt update; apt install -y libgl1 RUN chown -R ${NB_UID} ${HOME} USER ${NB_USER} diff --git a/README.md b/README.md index 146779e1..a77eb565 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ The **segment-geospatial** package draws its inspiration from [segment-anything- ## Citations -- Wu, Q., & Osco, L. (2023). samgeo: A Python package for segmenting geospatial data with the Segment Anything Model (SAM). _Journal of Open Source Software_, 8(89), 5663, +- Wu, Q., & Osco, L. (2023). samgeo: A Python package for segmenting geospatial data with the Segment Anything Model (SAM). _Journal of Open Source Software_, 8(89), 5663. +- Osco, L. P., Wu, Q., de Lemos, E. L., Gonçalves, W. N., Ramos, A. P. M., Li, J., & Junior, J. M. (2023). The Segment Anything Model (SAM) for remote sensing applications: From zero to one shot. _International Journal of Applied Earth Observation and Geoinformation_, 124, 103540. ## Features @@ -56,16 +57,16 @@ conda install -c conda-forge mamba mamba install -c conda-forge segment-geospatial ``` -Samgeo-geospatial has some optional dependencies that are not included in the default conda environment. To install these dependencies, run the following command: +If your system has a GPU, but the above commands do not install the GPU version of pytorch, you can force the installation of the GPU version of pytorch using the following command: ```bash -mamba install -c conda-forge groundingdino-py segment-anything-fast +mamba install -c conda-forge segment-geospatial "pytorch=*=cuda*" ``` -As of July 9th, 2023 Linux systems have also required that `libgl1` be installed for segment-geospatial to work. The following command will install that dependency +Samgeo-geospatial has some optional dependencies that are not included in the default conda environment. To install these dependencies, run the following command: ```bash -apt update; apt install -y libgl1 +mamba install -c conda-forge groundingdino-py segment-anything-fast ``` ## Examples diff --git a/docs/examples/arcgis.ipynb b/docs/examples/arcgis.ipynb index b40533af..96fe8c0d 100644 --- a/docs/examples/arcgis.ipynb +++ b/docs/examples/arcgis.ipynb @@ -231,9 +231,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "sam.show_anns(axis=\"off\", alpha=1, output=\"ag_annotations.tif\")" @@ -431,7 +429,7 @@ "metadata": {}, "outputs": [], "source": [ - "sam.generate('agriculture.tif', output=\"ag_masks2.tif\", foreground=True)" + "sam.generate(\"agriculture.tif\", output=\"ag_masks2.tif\", foreground=True)" ] }, { diff --git a/docs/examples/automatic_mask_generator_hq.ipynb b/docs/examples/automatic_mask_generator_hq.ipynb index a510a0aa..4f3c4a91 100644 --- a/docs/examples/automatic_mask_generator_hq.ipynb +++ b/docs/examples/automatic_mask_generator_hq.ipynb @@ -39,7 +39,13 @@ "source": [ "import os\n", "import leafmap\n", - "from samgeo.hq_sam import SamGeo, show_image, download_file, overlay_images, tms_to_geotiff" + "from samgeo.hq_sam import (\n", + " SamGeo,\n", + " show_image,\n", + " download_file,\n", + " overlay_images,\n", + " tms_to_geotiff,\n", + ")" ] }, { @@ -147,7 +153,7 @@ "outputs": [], "source": [ "sam = SamGeo(\n", - " model_type=\"vit_h\", # can be vit_h, vit_b, vit_l, vit_tiny\n", + " model_type=\"vit_h\", # can be vit_h, vit_b, vit_l, vit_tiny\n", " sam_kwargs=None,\n", ")" ] @@ -288,7 +294,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ diff --git a/docs/examples/box_prompts.ipynb b/docs/examples/box_prompts.ipynb index 5858943e..80fd433f 100644 --- a/docs/examples/box_prompts.ipynb +++ b/docs/examples/box_prompts.ipynb @@ -241,7 +241,7 @@ "metadata": {}, "outputs": [], "source": [ - "m.add_raster('mask.tif', cmap='viridis', nodata=0, layer_name='Mask')\n", + "m.add_raster(\"mask.tif\", cmap=\"viridis\", nodata=0, layer_name=\"Mask\")\n", "m" ] }, @@ -260,7 +260,7 @@ "metadata": {}, "outputs": [], "source": [ - "url = 'https://opengeos.github.io/data/sam/tree_boxes.geojson'\n", + "url = \"https://opengeos.github.io/data/sam/tree_boxes.geojson\"\n", "geojson = \"tree_boxes.geojson\"\n", "leafmap.download_file(url, geojson)" ] @@ -350,8 +350,7 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.8" - }, - "orig_nbformat": 4 + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/docs/examples/fast_sam.ipynb b/docs/examples/fast_sam.ipynb index 153595a8..14a98fa5 100644 --- a/docs/examples/fast_sam.ipynb +++ b/docs/examples/fast_sam.ipynb @@ -142,6 +142,7 @@ "outputs": [], "source": [ "from samgeo.fast_sam import SamGeo\n", + "\n", "sam = SamGeo(model=\"FastSAM-x.pt\")" ] }, @@ -259,8 +260,7 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.16" - }, - "orig_nbformat": 4 + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/docs/examples/input_prompts.ipynb b/docs/examples/input_prompts.ipynb index d9c4b879..cf036e6b 100644 --- a/docs/examples/input_prompts.ipynb +++ b/docs/examples/input_prompts.ipynb @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -147,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -260,8 +260,7 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.8" - }, - "orig_nbformat": 4 + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/docs/examples/input_prompts_hq.ipynb b/docs/examples/input_prompts_hq.ipynb index 11329aa4..40c5d61d 100644 --- a/docs/examples/input_prompts_hq.ipynb +++ b/docs/examples/input_prompts_hq.ipynb @@ -258,8 +258,7 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.16" - }, - "orig_nbformat": 4 + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/docs/examples/maxar_open_data.ipynb b/docs/examples/maxar_open_data.ipynb index f6faaad1..2c64e89a 100644 --- a/docs/examples/maxar_open_data.ipynb +++ b/docs/examples/maxar_open_data.ipynb @@ -63,7 +63,7 @@ }, "outputs": [], "source": [ - "url = 'https://drive.google.com/file/d/1jIIC5hvSPeJEC0fbDhtxVWk2XV9AxsQD/view?usp=sharing'" + "url = \"https://github.com/opengeos/datasets/releases/download/raster/Derna_sample.tif\"" ] }, { @@ -74,7 +74,7 @@ }, "outputs": [], "source": [ - "leafmap.download_file(url, output='image.tif')" + "leafmap.download_file(url, output=\"image.tif\")" ] }, { @@ -94,7 +94,7 @@ "source": [ "m = leafmap.Map(height=\"600px\")\n", "m.add_basemap(\"SATELLITE\")\n", - "m.add_raster('image.tif', layer_name=\"Image\")\n", + "m.add_raster(\"image.tif\", layer_name=\"Image\")\n", "m.add_layer_manager()\n", "m" ] @@ -160,7 +160,7 @@ }, "outputs": [], "source": [ - "sam.generate('image.tif', output=\"mask.tif\", foreground=True)" + "sam.generate(\"image.tif\", output=\"mask.tif\", foreground=True)" ] }, { @@ -178,7 +178,7 @@ }, "outputs": [], "source": [ - "raster_to_vector('mask.tif', output='mask.shp')" + "raster_to_vector(\"mask.tif\", output=\"mask.shp\")" ] }, { @@ -235,7 +235,7 @@ "outputs": [], "source": [ "leafmap.image_comparison(\n", - " 'image.tif',\n", + " \"image.tif\",\n", " \"annotation.tif\",\n", " label1=\"Image\",\n", " label2=\"Segmentation\",\n", @@ -257,7 +257,7 @@ }, "outputs": [], "source": [ - "overlay_images('image.tif', \"annotation.tif\", backend=\"TkAgg\")" + "overlay_images(\"image.tif\", \"annotation.tif\", backend=\"TkAgg\")" ] }, { @@ -273,8 +273,8 @@ "metadata": {}, "outputs": [], "source": [ - "m.add_raster('mask.tif', layer_name='Mask', nodata=0)\n", - "m.add_raster('annotation.tif', layer_name='Annotation')\n", + "m.add_raster(\"mask.tif\", layer_name=\"Mask\", nodata=0)\n", + "m.add_raster(\"annotation.tif\", layer_name=\"Annotation\")\n", "m" ] }, @@ -286,7 +286,7 @@ }, "outputs": [], "source": [ - "m.add_vector('mask.shp', layer_name='Vector', info_mode=None)" + "m.add_vector(\"mask.shp\", layer_name=\"Vector\", info_mode=None)" ] }, { diff --git a/docs/examples/swimming_pools.ipynb b/docs/examples/swimming_pools.ipynb index d49e3498..d301e53c 100644 --- a/docs/examples/swimming_pools.ipynb +++ b/docs/examples/swimming_pools.ipynb @@ -200,9 +200,9 @@ "outputs": [], "source": [ "sam.show_anns(\n", - " cmap='Blues',\n", - " box_color='red',\n", - " title='Automatic Segmentation of Swimming Pools',\n", + " cmap=\"Blues\",\n", + " box_color=\"red\",\n", + " title=\"Automatic Segmentation of Swimming Pools\",\n", " blend=True,\n", ")" ] @@ -228,10 +228,10 @@ "outputs": [], "source": [ "sam.show_anns(\n", - " cmap='Blues',\n", + " cmap=\"Blues\",\n", " add_boxes=False,\n", " alpha=0.5,\n", - " title='Automatic Segmentation of Swimming Pools',\n", + " title=\"Automatic Segmentation of Swimming Pools\",\n", ")" ] }, @@ -256,12 +256,12 @@ "outputs": [], "source": [ "sam.show_anns(\n", - " cmap='Greys_r',\n", + " cmap=\"Greys_r\",\n", " add_boxes=False,\n", " alpha=1,\n", - " title='Automatic Segmentation of Swimming Pools',\n", + " title=\"Automatic Segmentation of Swimming Pools\",\n", " blend=False,\n", - " output='pools.tif',\n", + " output=\"pools.tif\",\n", ")" ] }, @@ -360,8 +360,7 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.16" - }, - "orig_nbformat": 4 + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/docs/examples/text_prompts.ipynb b/docs/examples/text_prompts.ipynb index 19de7f55..11d2719e 100644 --- a/docs/examples/text_prompts.ipynb +++ b/docs/examples/text_prompts.ipynb @@ -200,9 +200,9 @@ "outputs": [], "source": [ "sam.show_anns(\n", - " cmap='Greens',\n", - " box_color='red',\n", - " title='Automatic Segmentation of Trees',\n", + " cmap=\"Greens\",\n", + " box_color=\"red\",\n", + " title=\"Automatic Segmentation of Trees\",\n", " blend=True,\n", ")" ] @@ -228,10 +228,10 @@ "outputs": [], "source": [ "sam.show_anns(\n", - " cmap='Greens',\n", + " cmap=\"Greens\",\n", " add_boxes=False,\n", " alpha=0.5,\n", - " title='Automatic Segmentation of Trees',\n", + " title=\"Automatic Segmentation of Trees\",\n", ")" ] }, @@ -256,12 +256,12 @@ "outputs": [], "source": [ "sam.show_anns(\n", - " cmap='Greys_r',\n", + " cmap=\"Greys_r\",\n", " add_boxes=False,\n", " alpha=1,\n", - " title='Automatic Segmentation of Trees',\n", + " title=\"Automatic Segmentation of Trees\",\n", " blend=False,\n", - " output='trees.tif',\n", + " output=\"trees.tif\",\n", ")" ] }, @@ -353,8 +353,7 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.16" - }, - "orig_nbformat": 4 + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/docs/examples/text_prompts_batch.ipynb b/docs/examples/text_prompts_batch.ipynb index 7f5c160c..cbb100eb 100644 --- a/docs/examples/text_prompts_batch.ipynb +++ b/docs/examples/text_prompts_batch.ipynb @@ -198,13 +198,13 @@ "outputs": [], "source": [ "sam.predict_batch(\n", - " images='tiles',\n", - " out_dir='masks',\n", + " images=\"tiles\",\n", + " out_dir=\"masks\",\n", " text_prompt=text_prompt,\n", " box_threshold=0.24,\n", " text_threshold=0.24,\n", " mask_multiplier=255,\n", - " dtype='uint8',\n", + " dtype=\"uint8\",\n", " merge=True,\n", " verbose=True,\n", ")" @@ -223,7 +223,7 @@ "metadata": {}, "outputs": [], "source": [ - "m.add_raster('masks/merged.tif', cmap='viridis', nodata=0, layer_name='Mask')\n", + "m.add_raster(\"masks/merged.tif\", cmap=\"viridis\", nodata=0, layer_name=\"Mask\")\n", "m.add_layer_manager()\n", "m" ] @@ -253,8 +253,7 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.16" - }, - "orig_nbformat": 4 + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/docs/index.md b/docs/index.md index 5e2a1a1a..9457acef 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,7 +21,8 @@ The **segment-geospatial** package draws its inspiration from [segment-anything- ## Citations -- Wu, Q., & Osco, L. (2023). samgeo: A Python package for segmenting geospatial data with the Segment Anything Model (SAM). _Journal of Open Source Software_, 8(89), 5663, +- Wu, Q., & Osco, L. (2023). samgeo: A Python package for segmenting geospatial data with the Segment Anything Model (SAM). _Journal of Open Source Software_, 8(89), 5663. +- Osco, L. P., Wu, Q., de Lemos, E. L., Gonçalves, W. N., Ramos, A. P. M., Li, J., & Junior, J. M. (2023). The Segment Anything Model (SAM) for remote sensing applications: From zero to one shot. _International Journal of Applied Earth Observation and Geoinformation_, 124, 103540. ## Features diff --git a/docs/installation.md b/docs/installation.md index a02e9b63..f3338060 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -20,16 +20,16 @@ conda install -c conda-forge mamba mamba install -c conda-forge segment-geospatial ``` -Samgeo-geospatial has some optional dependencies that are not included in the default conda environment. To install these dependencies, run the following command: +If your system has a GPU, but the above commands do not install the GPU version of pytorch, you can force the installation of the GPU version of pytorch using the following command: ```bash -mamba install -c conda-forge groundingdino-py segment-anything-fast +mamba install -c conda-forge segment-geospatial "pytorch=*=cuda*" ``` -As of July 9th, 2023 Linux systems have also required that `libgl1` be installed for segment-geospatial to work. The following command will install that dependency +Samgeo-geospatial has some optional dependencies that are not included in the default conda environment. To install these dependencies, run the following command: ```bash -apt update; apt install -y libgl1 +mamba install -c conda-forge groundingdino-py segment-anything-fast ``` ## Install from GitHub diff --git a/docs/samgeo.md b/docs/samgeo.md index 4efc0605..744c6cb7 100644 --- a/docs/samgeo.md +++ b/docs/samgeo.md @@ -1,4 +1,4 @@ - + # samgeo module ::: samgeo.samgeo \ No newline at end of file diff --git a/docs/workshops/cn_workshop.ipynb b/docs/workshops/cn_workshop.ipynb new file mode 100644 index 00000000..95f133e5 --- /dev/null +++ b/docs/workshops/cn_workshop.ipynb @@ -0,0 +1,1116 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# SamGeo Workshop\n", + "\n", + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/segment-geospatial/blob/main/docs/workshops/cn_workshop.ipynb)\n", + "\n", + "This notebook is for the workshop presented at the 第七届地球空间大数据与云计算前沿会议与集中学习.\n", + "\n", + "## Install dependencies\n", + "\n", + "Uncomment and run the following cell to install the required dependencies." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install segment-geospatial groundingdino-py leafmap localtileserver" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import leafmap\n", + "from samgeo import SamGeo\n", + "from samgeo.text_sam import LangSAM" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## Download sample data\n", + "\n", + "### Create an interactive map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m = leafmap.Map(center=[40.427495, -86.913638], zoom=18, height=700)\n", + "m.add_basemap(\"SATELLITE\")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "Pan and zoom the map to select the area of interest. Use the draw tools to draw a polygon or rectangle on the map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "if m.user_roi_bounds() is not None:\n", + " bbox = m.user_roi_bounds()\n", + "else:\n", + " bbox = [-86.9167, 40.4262, -86.9105, 40.4289]" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "### Download map tiles\n", + "\n", + "Download maps tiles and mosaic them into a single GeoTIFF file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image = \"image.tif\"" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "Specify the basemap as the source." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "leafmap.map_tiles_to_geotiff(\n", + " output=image, bbox=bbox, zoom=18, source=\"Satellite\", overwrite=True\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "You can also use your own image. Uncomment and run the following cell to use your own image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "# image = '/path/to/your/own/image.tif'" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "Display the downloaded image on the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m.layers[-1].visible = False # turn off the basemap\n", + "m.add_raster(image, layer_name=\"Image\")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/YHwrpS2.png)" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "## Automatic mask generation\n", + "\n", + "### Initialize SAM class" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam = SamGeo(\n", + " model_type=\"vit_h\",\n", + " sam_kwargs=None,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "### Automatic mask generation\n", + "\n", + "Segment the image and save the results to a GeoTIFF file. Set `unique=True` to assign a unique ID to each object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.generate(image, output=\"masks.tif\", foreground=True, unique=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.show_masks(cmap=\"binary_r\")" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/kWqLVuL.png)" + ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "Show the object annotations (objects with random color) on the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.show_anns(axis=\"off\", alpha=1, output=\"annotations.tif\")" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/J6Ie0Zj.png)" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "Compare images with a slider." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "leafmap.image_comparison(\n", + " \"image.tif\",\n", + " \"annotations.tif\",\n", + " label1=\"Satellite Image\",\n", + " label2=\"Image Segmentation\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/cm4QyaR.png)" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "Add image to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m.add_raster(\"annotations.tif\", opacity=0.5, layer_name=\"Masks\")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/Y6EaGVN.png)" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "Convert the object annotations to vector format, such as GeoPackage, Shapefile, or GeoJSON." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.raster_to_vector(\"masks.tif\", \"masks.shp\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m.add_vector(\"masks.shp\", layer_name=\"Masks vector\")" + ] + }, + { + "cell_type": "markdown", + "id": "35", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/N0xVt9S.png)" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "### Automatic mask generation options\n", + "\n", + "There are several tunable parameters in automatic mask generation that control how densely points are sampled and what the thresholds are for removing low quality or duplicate masks. Additionally, generation can be automatically run on crops of the image to get improved performance on smaller objects, and post-processing can remove stray pixels and holes. Here is an example configuration that samples more masks:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam_kwargs = {\n", + " \"points_per_side\": 32,\n", + " \"pred_iou_thresh\": 0.86,\n", + " \"stability_score_thresh\": 0.92,\n", + " \"crop_n_layers\": 1,\n", + " \"crop_n_points_downscale_factor\": 2,\n", + " \"min_mask_region_area\": 100,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam = SamGeo(\n", + " model_type=\"vit_h\",\n", + " sam_kwargs=sam_kwargs,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.generate(image, output=\"masks2.tif\", foreground=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.show_masks(cmap=\"binary_r\")" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/S2LYen8.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.show_anns(axis=\"off\", opacity=1, output=\"annotations2.tif\")" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/opEKsUu.png)" + ] + }, + { + "cell_type": "markdown", + "id": "44", + "metadata": {}, + "source": [ + "Compare images with a slider." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "leafmap.image_comparison(\n", + " image,\n", + " \"annotations.tif\",\n", + " label1=\"Image\",\n", + " label2=\"Image Segmentation\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "## Use points as input prompts\n", + "\n", + "### Initialize SAM class" + ] + }, + { + "cell_type": "markdown", + "id": "47", + "metadata": {}, + "source": [ + "Set `automatic=False` to disable the `SamAutomaticMaskGenerator` and enable the `SamPredictor`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m = leafmap.Map(center=[40.427495, -86.913638], zoom=18, height=700)\n", + "image = \"image.tif\"\n", + "m.add_raster(image, layer_name=\"Image\")\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam = SamGeo(\n", + " model_type=\"vit_h\",\n", + " automatic=False,\n", + " sam_kwargs=None,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "Specify the image to segment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.set_image(image)" + ] + }, + { + "cell_type": "markdown", + "id": "52", + "metadata": {}, + "source": [ + "### Image segmentation with input points\n", + "\n", + "A single point can be used to segment an object. The point can be specified as a tuple of (x, y), such as (col, row) or (lon, lat). The points can also be specified as a file path to a vector dataset. For non (col, row) input points, specify the `point_crs` parameter, which will automatically transform the points to the image column and row coordinates.\n", + "\n", + "Try a single point input:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "point_coords = [[-86.913162, 40.427157]]\n", + "sam.predict(point_coords, point_labels=1, point_crs=\"EPSG:4326\", output=\"mask1.tif\")\n", + "m.add_raster(\"mask1.tif\", layer_name=\"Mask1\", nodata=0, cmap=\"Blues\", opacity=1)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "54", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/zUMLUsn.png)" + ] + }, + { + "cell_type": "markdown", + "id": "55", + "metadata": {}, + "source": [ + "Try multiple points input:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "point_coords = [\n", + " [-86.913162, 40.427157],\n", + " [-86.913425, 40.427157],\n", + " [-86.91343, 40.427721],\n", + " [-86.913012, 40.427741],\n", + "]\n", + "sam.predict(point_coords, point_labels=1, point_crs=\"EPSG:4326\", output=\"mask2.tif\")\n", + "m.add_raster(\"mask2.tif\", layer_name=\"Mask2\", nodata=0, cmap=\"Greens\", opacity=1)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "57", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/zUMLUsn.png)" + ] + }, + { + "cell_type": "markdown", + "id": "58", + "metadata": {}, + "source": [ + "### Interactive segmentation\n", + "\n", + "Display the interactive map and use the marker tool to draw points on the map. Then click on the `Segment` button to segment the objects. The results will be added to the map automatically. Click on the `Reset` button to clear the points and the results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m = sam.show_map()\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "60", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/3W7JGqP.png)" + ] + }, + { + "cell_type": "markdown", + "id": "61", + "metadata": {}, + "source": [ + "## Bounding box input prompts" + ] + }, + { + "cell_type": "markdown", + "id": "62", + "metadata": {}, + "source": [ + "### Create an interactive map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m = leafmap.Map(center=[40.427495, -86.913638], zoom=18, height=700)\n", + "image = \"image.tif\"\n", + "m.add_raster(image, layer_name=\"Image\")\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam = SamGeo(\n", + " model_type=\"vit_h\",\n", + " automatic=False,\n", + " sam_kwargs=None,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "65", + "metadata": {}, + "source": [ + "Specify the image to segment. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "66", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.set_image(image)" + ] + }, + { + "cell_type": "markdown", + "id": "67", + "metadata": {}, + "source": [ + "### Create bounding boxes\n", + "\n", + "If no rectangles are drawn, the default bounding boxes will be used as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "if m.user_rois is not None:\n", + " boxes = m.user_rois\n", + "else:\n", + " boxes = [\n", + " [-86.913654, 40.426967, -86.912774, 40.427881],\n", + " [-86.914780, 40.426256, -86.913997, 40.426852],\n", + " [-86.913632, 40.426215, -86.912581, 40.426820],\n", + " ]" + ] + }, + { + "cell_type": "markdown", + "id": "69", + "metadata": {}, + "source": [ + "## Segment the image\n", + "\n", + "Use the `predict()` method to segment the image with specified bounding boxes. The `boxes` parameter accepts a list of bounding box coordinates in the format of [[left, bottom, right, top], [left, bottom, right, top], ...], a GeoJSON dictionary, or a file path to a GeoJSON file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.predict(boxes=boxes, point_crs=\"EPSG:4326\", output=\"mask.tif\", dtype=\"uint8\")" + ] + }, + { + "cell_type": "markdown", + "id": "71", + "metadata": {}, + "source": [ + "## Display the result\n", + "\n", + "Add the segmented image to the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m.add_raster(\"mask.tif\", cmap=\"viridis\", nodata=0, opacity=0.6, layer_name=\"Mask\")\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "73", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/9y31xUH.png)" + ] + }, + { + "cell_type": "markdown", + "id": "74", + "metadata": {}, + "source": [ + "## Text promots\n", + "\n", + "### Initialize LangSAM class\n", + "\n", + "The initialization of the LangSAM class might take a few minutes. The initialization downloads the model weights and sets up the model for inference." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m = leafmap.Map(center=[40.427495, -86.913638], zoom=18, height=700)\n", + "image = \"image.tif\"\n", + "m.add_raster(image, layer_name=\"Image\")\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam = LangSAM()" + ] + }, + { + "cell_type": "markdown", + "id": "77", + "metadata": {}, + "source": [ + "### Specify text prompts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "text_prompt = \"tree\"" + ] + }, + { + "cell_type": "markdown", + "id": "79", + "metadata": {}, + "source": [ + "### Segment the image\n", + "\n", + "Part of the model prediction includes setting appropriate thresholds for object detection and text association with the detected objects. These threshold values range from 0 to 1 and are set while calling the predict method of the LangSAM class.\n", + "\n", + "`box_threshold`: This value is used for object detection in the image. A higher value makes the model more selective, identifying only the most confident object instances, leading to fewer overall detections. A lower value, conversely, makes the model more tolerant, leading to increased detections, including potentially less confident ones.\n", + "\n", + "`text_threshold`: This value is used to associate the detected objects with the provided text prompt. A higher value requires a stronger association between the object and the text prompt, leading to more precise but potentially fewer associations. A lower value allows for looser associations, which could increase the number of associations but also introduce less precise matches.\n", + "\n", + "Remember to test different threshold values on your specific data. The optimal threshold can vary depending on the quality and nature of your images, as well as the specificity of your text prompts. Make sure to choose a balance that suits your requirements, whether that's precision or recall." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.predict(image, text_prompt, box_threshold=0.24, text_threshold=0.24)" + ] + }, + { + "cell_type": "markdown", + "id": "81", + "metadata": {}, + "source": [ + "### Visualize the results\n", + "\n", + "Show the result with bounding boxes on the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.show_anns(\n", + " cmap=\"Greens\",\n", + " box_color=\"red\",\n", + " title=\"Automatic Segmentation of Trees\",\n", + " blend=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "83", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/qRcy16Z.png)" + ] + }, + { + "cell_type": "markdown", + "id": "84", + "metadata": {}, + "source": [ + "Show the result without bounding boxes on the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.show_anns(\n", + " cmap=\"Greens\",\n", + " add_boxes=False,\n", + " alpha=0.5,\n", + " title=\"Automatic Segmentation of Trees\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "86", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/TvqGByH.png)" + ] + }, + { + "cell_type": "markdown", + "id": "87", + "metadata": {}, + "source": [ + "Show the result as a grayscale image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.show_anns(\n", + " cmap=\"Greys_r\",\n", + " add_boxes=False,\n", + " alpha=1,\n", + " title=\"Automatic Segmentation of Trees\",\n", + " blend=False,\n", + " output=\"trees.tif\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "89", + "metadata": {}, + "source": [ + "Convert the result to a vector format." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.raster_to_vector(\"trees.tif\", \"trees.shp\")" + ] + }, + { + "cell_type": "markdown", + "id": "91", + "metadata": {}, + "source": [ + "Show the results on the interactive map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "m.add_raster(\"trees.tif\", layer_name=\"Trees\", palette=\"Greens\", opacity=0.5, nodata=0)\n", + "style = {\n", + " \"color\": \"#3388ff\",\n", + " \"weight\": 2,\n", + " \"fillColor\": \"#7c4185\",\n", + " \"fillOpacity\": 0.5,\n", + "}\n", + "m.add_vector(\"trees.shp\", layer_name=\"Vector\", style=style)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "id": "93", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/WDQgECD.png)" + ] + }, + { + "cell_type": "markdown", + "id": "94", + "metadata": {}, + "source": [ + "### Interactive segmentation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sam.show_map()" + ] + }, + { + "cell_type": "markdown", + "id": "96", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/Zn7Dwty.png)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/workshops/purdue.ipynb b/docs/workshops/purdue.ipynb index 6a5d1fcc..7ab6a21b 100644 --- a/docs/workshops/purdue.ipynb +++ b/docs/workshops/purdue.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "0", "metadata": {}, "source": [ "# Purdue SamGeo Workshop\n", @@ -21,6 +22,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1", "metadata": {}, "outputs": [], "source": [ @@ -29,6 +31,7 @@ }, { "cell_type": "markdown", + "id": "2", "metadata": {}, "source": [ "## Import libraries" @@ -37,6 +40,7 @@ { "cell_type": "code", "execution_count": null, + "id": "3", "metadata": { "tags": [] }, @@ -49,6 +53,7 @@ }, { "cell_type": "markdown", + "id": "4", "metadata": {}, "source": [ "## Download sample data\n", @@ -59,6 +64,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5", "metadata": { "tags": [] }, @@ -71,6 +77,7 @@ }, { "cell_type": "markdown", + "id": "6", "metadata": {}, "source": [ "Pan and zoom the map to select the area of interest. Use the draw tools to draw a polygon or rectangle on the map" @@ -79,6 +86,7 @@ { "cell_type": "code", "execution_count": null, + "id": "7", "metadata": { "tags": [] }, @@ -92,6 +100,7 @@ }, { "cell_type": "markdown", + "id": "8", "metadata": {}, "source": [ "### Download map tiles\n", @@ -102,6 +111,7 @@ { "cell_type": "code", "execution_count": null, + "id": "9", "metadata": { "tags": [] }, @@ -112,6 +122,7 @@ }, { "cell_type": "markdown", + "id": "10", "metadata": {}, "source": [ "Specify the basemap as the source." @@ -120,16 +131,20 @@ { "cell_type": "code", "execution_count": null, + "id": "11", "metadata": { "tags": [] }, "outputs": [], "source": [ - "leafmap.map_tiles_to_geotiff(output=image, bbox=bbox, zoom=18, source=\"Satellite\", overwrite=True)" + "leafmap.map_tiles_to_geotiff(\n", + " output=image, bbox=bbox, zoom=18, source=\"Satellite\", overwrite=True\n", + ")" ] }, { "cell_type": "markdown", + "id": "12", "metadata": {}, "source": [ "You can also use your own image. Uncomment and run the following cell to use your own image." @@ -138,6 +153,7 @@ { "cell_type": "code", "execution_count": null, + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -146,6 +162,7 @@ }, { "cell_type": "markdown", + "id": "14", "metadata": {}, "source": [ "Display the downloaded image on the map." @@ -154,6 +171,7 @@ { "cell_type": "code", "execution_count": null, + "id": "15", "metadata": { "tags": [] }, @@ -166,6 +184,7 @@ }, { "cell_type": "markdown", + "id": "16", "metadata": {}, "source": [ "![](https://i.imgur.com/YHwrpS2.png)" @@ -173,6 +192,7 @@ }, { "cell_type": "markdown", + "id": "17", "metadata": {}, "source": [ "## Automatic mask generation\n", @@ -183,6 +203,7 @@ { "cell_type": "code", "execution_count": null, + "id": "18", "metadata": { "tags": [] }, @@ -196,6 +217,7 @@ }, { "cell_type": "markdown", + "id": "19", "metadata": {}, "source": [ "### Automatic mask generation\n", @@ -206,6 +228,7 @@ { "cell_type": "code", "execution_count": null, + "id": "20", "metadata": { "tags": [] }, @@ -217,6 +240,7 @@ { "cell_type": "code", "execution_count": null, + "id": "21", "metadata": { "tags": [] }, @@ -227,6 +251,7 @@ }, { "cell_type": "markdown", + "id": "22", "metadata": {}, "source": [ "![](https://i.imgur.com/kWqLVuL.png)" @@ -234,6 +259,7 @@ }, { "cell_type": "markdown", + "id": "23", "metadata": {}, "source": [ "Show the object annotations (objects with random color) on the map." @@ -242,6 +268,7 @@ { "cell_type": "code", "execution_count": null, + "id": "24", "metadata": { "tags": [] }, @@ -252,6 +279,7 @@ }, { "cell_type": "markdown", + "id": "25", "metadata": {}, "source": [ "![](https://i.imgur.com/J6Ie0Zj.png)" @@ -259,6 +287,7 @@ }, { "cell_type": "markdown", + "id": "26", "metadata": {}, "source": [ "Compare images with a slider." @@ -267,6 +296,7 @@ { "cell_type": "code", "execution_count": null, + "id": "27", "metadata": { "tags": [] }, @@ -282,6 +312,7 @@ }, { "cell_type": "markdown", + "id": "28", "metadata": {}, "source": [ "![](https://i.imgur.com/cm4QyaR.png)" @@ -289,6 +320,7 @@ }, { "cell_type": "markdown", + "id": "29", "metadata": {}, "source": [ "Add image to the map." @@ -297,6 +329,7 @@ { "cell_type": "code", "execution_count": null, + "id": "30", "metadata": { "tags": [] }, @@ -308,6 +341,7 @@ }, { "cell_type": "markdown", + "id": "31", "metadata": {}, "source": [ "![](https://i.imgur.com/Y6EaGVN.png)" @@ -315,6 +349,7 @@ }, { "cell_type": "markdown", + "id": "32", "metadata": {}, "source": [ "Convert the object annotations to vector format, such as GeoPackage, Shapefile, or GeoJSON." @@ -323,6 +358,7 @@ { "cell_type": "code", "execution_count": null, + "id": "33", "metadata": { "tags": [] }, @@ -334,16 +370,18 @@ { "cell_type": "code", "execution_count": null, + "id": "34", "metadata": { "tags": [] }, "outputs": [], "source": [ - "m.add_vector('masks.shp', layer_name='Masks vector')" + "m.add_vector(\"masks.shp\", layer_name=\"Masks vector\")" ] }, { "cell_type": "markdown", + "id": "35", "metadata": {}, "source": [ "![](https://i.imgur.com/N0xVt9S.png)" @@ -351,6 +389,7 @@ }, { "cell_type": "markdown", + "id": "36", "metadata": {}, "source": [ "### Automatic mask generation options\n", @@ -361,6 +400,7 @@ { "cell_type": "code", "execution_count": null, + "id": "37", "metadata": { "tags": [] }, @@ -379,6 +419,7 @@ { "cell_type": "code", "execution_count": null, + "id": "38", "metadata": { "tags": [] }, @@ -393,6 +434,7 @@ { "cell_type": "code", "execution_count": null, + "id": "39", "metadata": { "tags": [] }, @@ -404,6 +446,7 @@ { "cell_type": "code", "execution_count": null, + "id": "40", "metadata": { "tags": [] }, @@ -414,6 +457,7 @@ }, { "cell_type": "markdown", + "id": "41", "metadata": {}, "source": [ "![](https://i.imgur.com/S2LYen8.png)" @@ -422,6 +466,7 @@ { "cell_type": "code", "execution_count": null, + "id": "42", "metadata": { "tags": [] }, @@ -432,6 +477,7 @@ }, { "cell_type": "markdown", + "id": "43", "metadata": {}, "source": [ "![](https://i.imgur.com/opEKsUu.png)" @@ -439,6 +485,7 @@ }, { "cell_type": "markdown", + "id": "44", "metadata": {}, "source": [ "Compare images with a slider." @@ -447,6 +494,7 @@ { "cell_type": "code", "execution_count": null, + "id": "45", "metadata": { "tags": [] }, @@ -462,6 +510,7 @@ }, { "cell_type": "markdown", + "id": "46", "metadata": {}, "source": [ "## Use points as input prompts\n", @@ -471,6 +520,7 @@ }, { "cell_type": "markdown", + "id": "47", "metadata": {}, "source": [ "Set `automatic=False` to disable the `SamAutomaticMaskGenerator` and enable the `SamPredictor`." @@ -479,6 +529,7 @@ { "cell_type": "code", "execution_count": null, + "id": "48", "metadata": { "tags": [] }, @@ -493,6 +544,7 @@ { "cell_type": "code", "execution_count": null, + "id": "49", "metadata": { "tags": [] }, @@ -507,6 +559,7 @@ }, { "cell_type": "markdown", + "id": "50", "metadata": {}, "source": [ "Specify the image to segment." @@ -515,6 +568,7 @@ { "cell_type": "code", "execution_count": null, + "id": "51", "metadata": { "tags": [] }, @@ -525,6 +579,7 @@ }, { "cell_type": "markdown", + "id": "52", "metadata": {}, "source": [ "### Image segmentation with input points\n", @@ -537,6 +592,7 @@ { "cell_type": "code", "execution_count": null, + "id": "53", "metadata": { "tags": [] }, @@ -550,6 +606,7 @@ }, { "cell_type": "markdown", + "id": "54", "metadata": {}, "source": [ "![](https://i.imgur.com/zUMLUsn.png)" @@ -557,6 +614,7 @@ }, { "cell_type": "markdown", + "id": "55", "metadata": {}, "source": [ "Try multiple points input:" @@ -565,15 +623,18 @@ { "cell_type": "code", "execution_count": null, + "id": "56", "metadata": { "tags": [] }, "outputs": [], "source": [ - "point_coords = [[-86.913162, 40.427157],\n", - " [-86.913425, 40.427157],\n", - " [-86.91343, 40.427721],\n", - " [-86.913012, 40.427741]]\n", + "point_coords = [\n", + " [-86.913162, 40.427157],\n", + " [-86.913425, 40.427157],\n", + " [-86.91343, 40.427721],\n", + " [-86.913012, 40.427741],\n", + "]\n", "sam.predict(point_coords, point_labels=1, point_crs=\"EPSG:4326\", output=\"mask2.tif\")\n", "m.add_raster(\"mask2.tif\", layer_name=\"Mask2\", nodata=0, cmap=\"Greens\", opacity=1)\n", "m" @@ -581,6 +642,7 @@ }, { "cell_type": "markdown", + "id": "57", "metadata": {}, "source": [ "![](https://i.imgur.com/zUMLUsn.png)" @@ -588,6 +650,7 @@ }, { "cell_type": "markdown", + "id": "58", "metadata": {}, "source": [ "### Interactive segmentation\n", @@ -598,6 +661,7 @@ { "cell_type": "code", "execution_count": null, + "id": "59", "metadata": { "tags": [] }, @@ -609,6 +673,7 @@ }, { "cell_type": "markdown", + "id": "60", "metadata": {}, "source": [ "![](https://i.imgur.com/3W7JGqP.png)" @@ -616,6 +681,7 @@ }, { "cell_type": "markdown", + "id": "61", "metadata": {}, "source": [ "## Bounding box input prompts" @@ -623,6 +689,7 @@ }, { "cell_type": "markdown", + "id": "62", "metadata": {}, "source": [ "### Create an interactive map" @@ -631,6 +698,7 @@ { "cell_type": "code", "execution_count": null, + "id": "63", "metadata": { "tags": [] }, @@ -645,6 +713,7 @@ { "cell_type": "code", "execution_count": null, + "id": "64", "metadata": { "tags": [] }, @@ -659,6 +728,7 @@ }, { "cell_type": "markdown", + "id": "65", "metadata": {}, "source": [ "Specify the image to segment. " @@ -667,6 +737,7 @@ { "cell_type": "code", "execution_count": null, + "id": "66", "metadata": { "tags": [] }, @@ -677,6 +748,7 @@ }, { "cell_type": "markdown", + "id": "67", "metadata": {}, "source": [ "### Create bounding boxes\n", @@ -687,6 +759,7 @@ { "cell_type": "code", "execution_count": null, + "id": "68", "metadata": { "tags": [] }, @@ -698,12 +771,13 @@ " boxes = [\n", " [-86.913654, 40.426967, -86.912774, 40.427881],\n", " [-86.914780, 40.426256, -86.913997, 40.426852],\n", - " [-86.913632, 40.426215, -86.912581, 40.426820]\n", + " [-86.913632, 40.426215, -86.912581, 40.426820],\n", " ]" ] }, { "cell_type": "markdown", + "id": "69", "metadata": {}, "source": [ "## Segment the image\n", @@ -714,6 +788,7 @@ { "cell_type": "code", "execution_count": null, + "id": "70", "metadata": { "tags": [] }, @@ -724,6 +799,7 @@ }, { "cell_type": "markdown", + "id": "71", "metadata": {}, "source": [ "## Display the result\n", @@ -734,17 +810,19 @@ { "cell_type": "code", "execution_count": null, + "id": "72", "metadata": { "tags": [] }, "outputs": [], "source": [ - "m.add_raster('mask.tif', cmap='viridis', nodata=0, opacity=0.6, layer_name='Mask')\n", + "m.add_raster(\"mask.tif\", cmap=\"viridis\", nodata=0, opacity=0.6, layer_name=\"Mask\")\n", "m" ] }, { "cell_type": "markdown", + "id": "73", "metadata": {}, "source": [ "![](https://i.imgur.com/9y31xUH.png)" @@ -752,6 +830,7 @@ }, { "cell_type": "markdown", + "id": "74", "metadata": {}, "source": [ "## Text promots\n", @@ -764,6 +843,7 @@ { "cell_type": "code", "execution_count": null, + "id": "75", "metadata": { "tags": [] }, @@ -778,6 +858,7 @@ { "cell_type": "code", "execution_count": null, + "id": "76", "metadata": { "tags": [] }, @@ -788,6 +869,7 @@ }, { "cell_type": "markdown", + "id": "77", "metadata": {}, "source": [ "### Specify text prompts" @@ -796,6 +878,7 @@ { "cell_type": "code", "execution_count": null, + "id": "78", "metadata": { "tags": [] }, @@ -806,6 +889,7 @@ }, { "cell_type": "markdown", + "id": "79", "metadata": {}, "source": [ "### Segment the image\n", @@ -822,6 +906,7 @@ { "cell_type": "code", "execution_count": null, + "id": "80", "metadata": { "tags": [] }, @@ -832,6 +917,7 @@ }, { "cell_type": "markdown", + "id": "81", "metadata": {}, "source": [ "### Visualize the results\n", @@ -842,21 +928,23 @@ { "cell_type": "code", "execution_count": null, + "id": "82", "metadata": { "tags": [] }, "outputs": [], "source": [ "sam.show_anns(\n", - " cmap='Greens',\n", - " box_color='red',\n", - " title='Automatic Segmentation of Trees',\n", + " cmap=\"Greens\",\n", + " box_color=\"red\",\n", + " title=\"Automatic Segmentation of Trees\",\n", " blend=True,\n", ")" ] }, { "cell_type": "markdown", + "id": "83", "metadata": {}, "source": [ "![](https://i.imgur.com/qRcy16Z.png)" @@ -864,6 +952,7 @@ }, { "cell_type": "markdown", + "id": "84", "metadata": {}, "source": [ "Show the result without bounding boxes on the map." @@ -872,21 +961,23 @@ { "cell_type": "code", "execution_count": null, + "id": "85", "metadata": { "tags": [] }, "outputs": [], "source": [ "sam.show_anns(\n", - " cmap='Greens',\n", + " cmap=\"Greens\",\n", " add_boxes=False,\n", " alpha=0.5,\n", - " title='Automatic Segmentation of Trees',\n", + " title=\"Automatic Segmentation of Trees\",\n", ")" ] }, { "cell_type": "markdown", + "id": "86", "metadata": {}, "source": [ "![](https://i.imgur.com/TvqGByH.png)" @@ -894,6 +985,7 @@ }, { "cell_type": "markdown", + "id": "87", "metadata": {}, "source": [ "Show the result as a grayscale image." @@ -902,23 +994,25 @@ { "cell_type": "code", "execution_count": null, + "id": "88", "metadata": { "tags": [] }, "outputs": [], "source": [ "sam.show_anns(\n", - " cmap='Greys_r',\n", + " cmap=\"Greys_r\",\n", " add_boxes=False,\n", " alpha=1,\n", - " title='Automatic Segmentation of Trees',\n", + " title=\"Automatic Segmentation of Trees\",\n", " blend=False,\n", - " output='trees.tif',\n", + " output=\"trees.tif\",\n", ")" ] }, { "cell_type": "markdown", + "id": "89", "metadata": {}, "source": [ "Convert the result to a vector format." @@ -927,6 +1021,7 @@ { "cell_type": "code", "execution_count": null, + "id": "90", "metadata": { "tags": [] }, @@ -937,6 +1032,7 @@ }, { "cell_type": "markdown", + "id": "91", "metadata": {}, "source": [ "Show the results on the interactive map." @@ -945,6 +1041,7 @@ { "cell_type": "code", "execution_count": null, + "id": "92", "metadata": { "tags": [] }, @@ -963,6 +1060,7 @@ }, { "cell_type": "markdown", + "id": "93", "metadata": {}, "source": [ "![](https://i.imgur.com/WDQgECD.png)" @@ -970,6 +1068,7 @@ }, { "cell_type": "markdown", + "id": "94", "metadata": {}, "source": [ "### Interactive segmentation" @@ -978,6 +1077,7 @@ { "cell_type": "code", "execution_count": null, + "id": "95", "metadata": { "tags": [] }, @@ -988,6 +1088,7 @@ }, { "cell_type": "markdown", + "id": "96", "metadata": {}, "source": [ "![](https://i.imgur.com/Zn7Dwty.png)" diff --git a/mkdocs.yml b/mkdocs.yml index 96df95d3..88d18f92 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -63,6 +63,7 @@ nav: - examples/maxar_open_data.ipynb - Workshops: - workshops/purdue.ipynb + - workshops/cn_workshop.ipynb - API Reference: - common module: common.md - samgeo module: samgeo.md diff --git a/paper/paper.bib b/paper/paper.bib index 4f2f8120..249b7cda 100644 --- a/paper/paper.bib +++ b/paper/paper.bib @@ -57,7 +57,7 @@ @ARTICLE{Gillies2013 } @SOFTWARE{Wu2023, - title = {segment-anything-py: An unofficial Python package + title = {segment-anything-py: An unofficial Python package for Meta AI's Segment Anything Model}, author = {Qiusheng Wu}, month = may, @@ -70,7 +70,7 @@ @SOFTWARE{Wu2023 @ARTICLE{Zhang2023, - title={A Comprehensive Survey on Segment Anything Model for Vision and Beyond}, + title={A Comprehensive Survey on Segment Anything Model for Vision and Beyond}, author={Chunhui Zhang and Li Liu and Yawen Cui and Guanjie Huang and Weilin Lin and Yiqian Yang and Yuehong Hu}, year={2023}, eprint={2305.08196}, @@ -82,7 +82,7 @@ @ARTICLE{Zhang2023 @ARTICLE{Ma2023, - title={Segment Anything in Medical Images}, + title={Segment Anything in Medical Images}, author={Jun Ma and Bo Wang}, year={2023}, eprint={2304.12306}, @@ -104,7 +104,7 @@ @SOFTWARE{Hancharenka2023 @ARTICLE{Ji2023, - title={Segment Anything Is Not Always Perfect: An Investigation of SAM on Different Real-world Applications}, + title={Segment Anything Is Not Always Perfect: An Investigation of SAM on Different Real-world Applications}, author={Wei Ji and Jingjing Li and Qi Bi and Wenbo Li and Li Cheng}, year={2023}, eprint={2304.05750}, @@ -116,7 +116,7 @@ @ARTICLE{Ji2023 @ARTICLE{liu2023, - title={Grounding DINO: Marrying DINO with Grounded Pre-Training for Open-Set Object Detection}, + title={Grounding DINO: Marrying DINO with Grounded Pre-Training for Open-Set Object Detection}, author={Shilong Liu and Zhaoyang Zeng and Tianhe Ren and Feng Li and Hao Zhang and Jie Yang and Chunyuan Li and Jianwei Yang and Hang Su and Jun Zhu and Lei Zhang}, year={2023}, eprint={2303.05499}, diff --git a/pyproject-codespell.precommit-toml b/pyproject-codespell.precommit-toml new file mode 100644 index 00000000..8aa8fdfc --- /dev/null +++ b/pyproject-codespell.precommit-toml @@ -0,0 +1,3 @@ +[tool.codespell] +ignore-words-list = "aci,acount,acounts,fallow,ges,hart,hist,nd,ned,ois,wqs,watermask,tre,pres" +skip="*.csv,*.geojson,*.json,*.yml*.js,*.html,*cff,*.pdf" diff --git a/requirements.txt b/requirements.txt index 4a635aa5..d5696474 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,17 @@ -segment-anything-py -segment-anything-hq -opencv-python -pycocotools -matplotlib -huggingface_hub +gdown geopandas +huggingface_hub +leafmap +localtileserver +matplotlib +opencv-python-headless +patool +pycocotools +pyproj rasterio +segment-anything-hq +segment-anything-py +timm tqdm -gdown +xarray xyzservices -pyproj -leafmap -localtileserver -timm \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index e05d4492..5a3ac7ad 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,3 @@ -rio-cogeo groundingdino-py -segment-anything-fast \ No newline at end of file +rio-cogeo +segment-anything-fast diff --git a/requirements_docs.txt b/requirements_docs.txt index 1cca6f0e..b5c8e72b 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,28 +1,28 @@ black black[jupyter] -codespell -deadlink bump2version +codespell coverage +deadlink flake8 ipykernel livereload -nbconvert -nbformat -pip -sphinx -tox -twine -watchdog -wheel mkdocs -mkdocs-git-revision-date-plugin mkdocs-git-revision-date-localized-plugin +mkdocs-git-revision-date-plugin mkdocs-jupyter>=0.24.0 -mkdocs-material>=9.1.3 +mkdocs-material>=9.1.3 mkdocs-pdf-export-plugin mkdocstrings mkdocstrings-crystal mkdocstrings-python-legacy +nbconvert +nbformat +pip pygments -pymdown-extensions \ No newline at end of file +pymdown-extensions +sphinx +tox +twine +watchdog +wheel diff --git a/samgeo/__init__.py b/samgeo/__init__.py index 7dab5929..d7b0f678 100644 --- a/samgeo/__init__.py +++ b/samgeo/__init__.py @@ -2,7 +2,6 @@ __author__ = """Qiusheng Wu""" __email__ = "giswqs@gmail.com" -__version__ = '0.10.3' - +__version__ = "0.10.6" from .samgeo import * diff --git a/samgeo/common.py b/samgeo/common.py index 6c03d334..db617ef7 100644 --- a/samgeo/common.py +++ b/samgeo/common.py @@ -209,19 +209,22 @@ def download_checkpoint(model_type="vit_h", checkpoint_dir=None, hq=False): model_types = { "vit_h": { "name": "sam_hq_vit_h.pth", - "url": "https://drive.google.com/file/d/1qobFYrI4eyIANfBSmYcGuWRaSIXfMOQ8/view?usp=sharing", + "url": [ + "https://github.com/opengeos/datasets/releases/download/models/sam_hq_vit_h.zip", + "https://github.com/opengeos/datasets/releases/download/models/sam_hq_vit_h.z01", + ], }, "vit_l": { "name": "sam_hq_vit_l.pth", - "url": "https://drive.google.com/file/d/1Uk17tDKX1YAKas5knI4y9ZJCo0lRVL0G/view?usp=sharing", + "url": "https://github.com/opengeos/datasets/releases/download/models/sam_hq_vit_l.pth", }, "vit_b": { "name": "sam_hq_vit_b.pth", - "url": "https://drive.google.com/file/d/11yExZLOve38kRZPfRx_MRxfIAKmfMY47/view?usp=sharing", + "url": "https://github.com/opengeos/datasets/releases/download/models/sam_hq_vit_b.pth", }, "vit_tiny": { "name": "sam_hq_vit_tiny.pth", - "url": "https://huggingface.co/lkeab/hq-sam/resolve/main/sam_hq_vit_tiny.pth", + "url": "https://github.com/opengeos/datasets/releases/download/models/sam_hq_vit_tiny.pth", }, } @@ -239,7 +242,10 @@ def download_checkpoint(model_type="vit_h", checkpoint_dir=None, hq=False): if not os.path.exists(checkpoint): print(f"Model checkpoint for {model_type} not found.") url = model_types[model_type]["url"] - download_file(url, checkpoint) + if isinstance(url, str): + download_file(url, checkpoint) + elif isinstance(url, list): + download_files(url, checkpoint_dir, multi_part=True) return checkpoint @@ -780,7 +786,11 @@ def geojson_to_coords( def coords_to_xy( - src_fp: str, coords: list, coord_crs: str = "epsg:4326", **kwargs + src_fp: str, + coords: list, + coord_crs: str = "epsg:4326", + return_out_of_bounds=False, + **kwargs, ) -> list: """Converts a list of coordinates to pixel coordinates, i.e., (col, row) coordinates. @@ -788,11 +798,14 @@ def coords_to_xy( src_fp: The source raster file path. coords: A list of coordinates in the format of [[x1, y1], [x2, y2], ...] coord_crs: The coordinate CRS of the input coordinates. Defaults to "epsg:4326". + return_out_of_bounds: Whether to return out of bounds coordinates. Defaults to False. **kwargs: Additional keyword arguments to pass to rasterio.transform.rowcol. Returns: A list of pixel coordinates in the format of [[x1, y1], [x2, y2], ...] """ + out_of_bounds = [] + if isinstance(coords, np.ndarray): coords = coords.tolist() @@ -805,15 +818,26 @@ def coords_to_xy( rows, cols = rasterio.transform.rowcol(src.transform, xs, ys, **kwargs) result = [[col, row] for col, row in zip(cols, rows)] - result = [ - [x, y] for x, y in result if x >= 0 and y >= 0 and x < width and y < height - ] - if len(result) == 0: + output = [] + + for i, (x, y) in enumerate(result): + if x >= 0 and y >= 0 and x < width and y < height: + output.append([x, y]) + else: + out_of_bounds.append(i) + + # output = [ + # [x, y] for x, y in result if x >= 0 and y >= 0 and x < width and y < height + # ] + if len(output) == 0: print("No valid pixel coordinates found.") - elif len(result) < len(coords): + elif len(output) < len(coords): print("Some coordinates are out of the image boundary.") - return result + if return_out_of_bounds: + return output, out_of_bounds + else: + return output def boxes_to_vector(coords, src_crs, dst_crs="EPSG:4326", output=None, **kwargs): @@ -1098,6 +1122,8 @@ def tiff_to_tiff( func, data_to_rgb=chw_to_hwc, sample_size=(512, 512), + sample_nodata_threshold=1.0, + nodata_value=None, sample_resize=None, bound=128, foreground=True, @@ -1108,6 +1134,9 @@ def tiff_to_tiff( with rasterio.open(src_fp) as src: profile = src.profile + if nodata_value is None: + nodata_values = profile.get("nodata", None) + # Computer blocks rh, rw = profile["height"], profile["width"] sh, sw = sample_size @@ -1130,6 +1159,11 @@ def tiff_to_tiff( for b in tqdm(sample_grid): # Read each tile from the source r = read_block(src, **b) + + if nodata_value is not None: + if (r == nodata_value).mean() >= sample_nodata_threshold: + continue + # Extract the first 3 channels as RGB uint8_rgb_in = data_to_rgb(r) orig_size = uint8_rgb_in.shape[:2] @@ -2969,3 +3003,136 @@ def merge_rasters( dstNodata=output_nodata, options=output_options, ) + + +def extract_archive(archive, outdir=None, **kwargs): + """ + Extracts a multipart archive. + + This function uses the patoolib library to extract a multipart archive. + If the patoolib library is not installed, it attempts to install it. + If the archive does not end with ".zip", it appends ".zip" to the archive name. + If the extraction fails (for example, if the files already exist), it skips the extraction. + + Args: + archive (str): The path to the archive file. + outdir (str): The directory where the archive should be extracted. + **kwargs: Arbitrary keyword arguments for the patoolib.extract_archive function. + + Returns: + None + + Raises: + Exception: An exception is raised if the extraction fails for reasons other than the files already existing. + + Example: + + files = ["sam_hq_vit_tiny.zip", "sam_hq_vit_tiny.z01", "sam_hq_vit_tiny.z02", "sam_hq_vit_tiny.z03"] + base_url = "https://github.com/opengeos/datasets/releases/download/models/" + urls = [base_url + f for f in files] + leafmap.download_files(urls, out_dir="models", multi_part=True) + + """ + try: + import patoolib + except ImportError: + install_package("patool") + import patoolib + + if not archive.endswith(".zip"): + archive = archive + ".zip" + + if outdir is None: + outdir = os.path.dirname(archive) + + try: + patoolib.extract_archive(archive, outdir=outdir, **kwargs) + except Exception as e: + print("The unzipped files might already exist. Skipping extraction.") + return + + +def download_files( + urls, + out_dir=None, + filenames=None, + quiet=False, + proxy=None, + speed=None, + use_cookies=True, + verify=True, + id=None, + fuzzy=False, + resume=False, + unzip=True, + overwrite=False, + subfolder=False, + multi_part=False, +): + """Download files from URLs, including Google Drive shared URL. + + Args: + urls (list): The list of urls to download. Google Drive URL is also supported. + out_dir (str, optional): The output directory. Defaults to None. + filenames (list, optional): Output filename. Default is basename of URL. + quiet (bool, optional): Suppress terminal output. Default is False. + proxy (str, optional): Proxy. Defaults to None. + speed (float, optional): Download byte size per second (e.g., 256KB/s = 256 * 1024). Defaults to None. + use_cookies (bool, optional): Flag to use cookies. Defaults to True. + verify (bool | str, optional): Either a bool, in which case it controls whether the server's TLS certificate is verified, or a string, in which case it must be a path to a CA bundle to use. Default is True.. Defaults to True. + id (str, optional): Google Drive's file ID. Defaults to None. + fuzzy (bool, optional): Fuzzy extraction of Google Drive's file Id. Defaults to False. + resume (bool, optional): Resume the download from existing tmp file if possible. Defaults to False. + unzip (bool, optional): Unzip the file. Defaults to True. + overwrite (bool, optional): Overwrite the file if it already exists. Defaults to False. + subfolder (bool, optional): Create a subfolder with the same name as the file. Defaults to False. + multi_part (bool, optional): If the file is a multi-part file. Defaults to False. + + Examples: + + files = ["sam_hq_vit_tiny.zip", "sam_hq_vit_tiny.z01", "sam_hq_vit_tiny.z02", "sam_hq_vit_tiny.z03"] + base_url = "https://github.com/opengeos/datasets/releases/download/models/" + urls = [base_url + f for f in files] + leafmap.download_files(urls, out_dir="models", multi_part=True) + """ + + if out_dir is None: + out_dir = os.getcwd() + + if filenames is None: + filenames = [None] * len(urls) + + filepaths = [] + for url, output in zip(urls, filenames): + if output is None: + filename = os.path.join(out_dir, os.path.basename(url)) + else: + filename = os.path.join(out_dir, output) + + filepaths.append(filename) + if multi_part: + unzip = False + + download_file( + url, + filename, + quiet, + proxy, + speed, + use_cookies, + verify, + id, + fuzzy, + resume, + unzip, + overwrite, + subfolder, + ) + + if multi_part: + archive = os.path.splitext(filename)[0] + ".zip" + out_dir = os.path.dirname(filename) + extract_archive(archive, out_dir) + + for file in filepaths: + os.remove(file) diff --git a/samgeo/fast_sam.py b/samgeo/fast_sam.py index 86609899..5dfc9b54 100644 --- a/samgeo/fast_sam.py +++ b/samgeo/fast_sam.py @@ -30,8 +30,8 @@ def __init__(self, model="FastSAM-x.pt", **kwargs): ) models = { - "FastSAM-x.pt": "https://drive.google.com/file/d/1m1sjY4ihXBU1fZXdQ-Xdj-mDltW-2Rqv/view?usp=sharing", - "FastSAM-s.pt": "https://drive.google.com/file/d/10XmSj6mmpmRb8NhXbtiuO9cTTBwR_9SV/view?usp=sharing", + "FastSAM-x.pt": "https://github.com/opengeos/datasets/releases/download/models/FastSAM-x.pt", + "FastSAM-s.pt": "https://github.com/opengeos/datasets/releases/download/models/FastSAM-s.pt", } if model not in models: diff --git a/samgeo/hq_sam.py b/samgeo/hq_sam.py index 3a17b438..f5fd9cd4 100644 --- a/samgeo/hq_sam.py +++ b/samgeo/hq_sam.py @@ -7,7 +7,11 @@ import cv2 import torch import numpy as np -from segment_anything_hq import sam_model_registry, SamAutomaticMaskGenerator, SamPredictor +from segment_anything_hq import ( + sam_model_registry, + SamAutomaticMaskGenerator, + SamPredictor, +) from .common import * @@ -573,7 +577,11 @@ def predict( self.boxes = input_boxes - if boxes is None or (len(boxes) == 1) or (len(boxes) == 4 and isinstance(boxes[0], float)): + if ( + boxes is None + or (len(boxes) == 1) + or (len(boxes) == 4 and isinstance(boxes[0], float)) + ): if isinstance(boxes, list) and isinstance(boxes[0], list): boxes = boxes[0] masks, scores, logits = predictor.predict( diff --git a/samgeo/samgeo.py b/samgeo/samgeo.py index fd3a4532..7c7e373e 100644 --- a/samgeo/samgeo.py +++ b/samgeo/samgeo.py @@ -152,6 +152,9 @@ def generate( output=None, foreground=True, batch=False, + batch_sample_size=(512, 512), + batch_nodata_threshold=1.0, + nodata_value=None, erosion_kernel=None, mask_multiplier=255, unique=True, @@ -164,6 +167,12 @@ def generate( output (str, optional): The path to the output image. Defaults to None. foreground (bool, optional): Whether to generate the foreground mask. Defaults to True. batch (bool, optional): Whether to generate masks for a batch of image tiles. Defaults to False. + batch_sample_size (tuple, optional): When batch=True, the size of the sample window when iterating over rasters. + batch_nodata_threshold (float,optional): Batch samples with a fraction of nodata pixels above this threshold will + not be used to generate a mask. The default, 1.0, will skip samples with 100% nodata values. This is useful + when rasters have large areas of nodata values which can be skipped. + nodata_value (int, optional): Nodata value to use in checking batch_nodata_threshold. The default, None, + will use the nodata value in the raster metadata if present. erosion_kernel (tuple, optional): The erosion kernel for filtering object masks and extract borders. Such as (3, 3) or (5, 5). Set to None to disable it. Defaults to None. mask_multiplier (int, optional): The mask multiplier for the output mask, which is usually a binary mask [0, 1]. @@ -190,6 +199,9 @@ def generate( output, self, foreground=foreground, + sample_size=batch_sample_size, + sample_nodata_threshold=batch_nodata_threshold, + nodata_value=nodata_value, erosion_kernel=erosion_kernel, mask_multiplier=mask_multiplier, **kwargs, @@ -503,6 +515,7 @@ def predict( return_results (bool, optional): Whether to return the predicted masks, scores, and logits. Defaults to False. """ + out_of_bounds = [] if isinstance(boxes, str): gdf = gpd.read_file(boxes) @@ -529,7 +542,9 @@ def predict( point_labels = self.point_labels if (point_crs is not None) and (point_coords is not None): - point_coords = coords_to_xy(self.source, point_coords, point_crs) + point_coords, out_of_bounds = coords_to_xy( + self.source, point_coords, point_crs, return_out_of_bounds=True + ) if isinstance(point_coords, list): point_coords = np.array(point_coords) @@ -544,6 +559,13 @@ def predict( if len(point_labels) != len(point_coords): if len(point_labels) == 1: point_labels = point_labels * len(point_coords) + elif len(out_of_bounds) > 0: + print(f"Removing {len(out_of_bounds)} out-of-bound points.") + point_labels_new = [] + for i, p in enumerate(point_labels): + if i not in out_of_bounds: + point_labels_new.append(p) + point_labels = point_labels_new else: raise ValueError( "The length of point_labels must be equal to the length of point_coords." @@ -570,7 +592,11 @@ def predict( self.boxes = input_boxes - if boxes is None or (len(boxes) == 1) or (len(boxes) == 4 and isinstance(boxes[0], float)): + if ( + boxes is None + or (len(boxes) == 1) + or (len(boxes) == 4 and isinstance(boxes[0], float)) + ): if isinstance(boxes, list) and isinstance(boxes[0], list): boxes = boxes[0] masks, scores, logits = predictor.predict( diff --git a/setup.cfg b/setup.cfg index fddcd308..0be203cd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,15 +1,15 @@ [bumpversion] -current_version = 0.10.3 +current_version = 0.10.6 commit = True tag = True [bumpversion:file:setup.py] -search = version='{current_version}' -replace = version='{new_version}' +search = version="{current_version}" +replace = version="{new_version}" [bumpversion:file:samgeo/__init__.py] -search = __version__ = '{current_version}' -replace = __version__ = '{new_version}' +search = __version__ = "{current_version}" +replace = __version__ = "{new_version}" [bdist_wheel] universal = 1 diff --git a/setup.py b/setup.py index 32f3d759..d3c07295 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/opengeos/segment-geospatial", - version='0.10.3', + version="0.10.6", zip_safe=False, ) diff --git a/tests/test_samgeo.py b/tests/test_samgeo.py index 95be51fc..61390e4c 100644 --- a/tests/test_samgeo.py +++ b/tests/test_samgeo.py @@ -61,7 +61,7 @@ def test_predict(self): sam.set_image(self.source) point_coords = [[-122.1419, 37.6383]] sam.predict( - point_coords, point_labels=1, point_crs="EPSG:4326", output='mask1.tif' + point_coords, point_labels=1, point_crs="EPSG:4326", output="mask1.tif" ) self.assertTrue(os.path.exists("mask1.tif")) @@ -71,6 +71,6 @@ def test_predict(self): [-122.1451, 37.6395], ] sam.predict( - point_coords, point_labels=1, point_crs="EPSG:4326", output='mask2.tif' + point_coords, point_labels=1, point_crs="EPSG:4326", output="mask2.tif" ) self.assertTrue(os.path.exists("mask2.tif"))