segment-geospatial 1.0.3__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/workflows/draft-pdf.yml +1 -1
  2. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/workflows/ubuntu.yml +5 -3
  3. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.pre-commit-config.yaml +1 -1
  4. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/PKG-INFO +2 -1
  5. segment_geospatial-1.2.0/docs/examples/sam3_tiled_segmentation.ipynb +274 -0
  6. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/mkdocs.yml +1 -0
  7. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/pyproject.toml +2 -2
  8. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/requirements.txt +1 -0
  9. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/samgeo/__init__.py +1 -1
  10. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/samgeo/common.py +260 -13
  11. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/samgeo/samgeo3.py +492 -9
  12. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/segment_geospatial.egg-info/PKG-INFO +2 -1
  13. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/segment_geospatial.egg-info/SOURCES.txt +1 -0
  14. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/segment_geospatial.egg-info/requires.txt +1 -0
  15. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.editorconfig +0 -0
  16. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/FUNDING.yml +0 -0
  17. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  18. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  19. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  20. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/dependabot.yaml +0 -0
  21. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/workflows/docker-image.yml +0 -0
  22. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/workflows/docker-publish.yml +0 -0
  23. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/workflows/docs-build.yml +0 -0
  24. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/workflows/docs.yml +0 -0
  25. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/workflows/macos.yml +0 -0
  26. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/workflows/pypi.yml +0 -0
  27. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.github/workflows/windows.yml +0 -0
  28. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/.gitignore +0 -0
  29. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/CITATION.cff +0 -0
  30. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/CODE_OF_CONDUCT.md +0 -0
  31. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/Dockerfile +0 -0
  32. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/LICENSE +0 -0
  33. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/MANIFEST.in +0 -0
  34. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/README.md +0 -0
  35. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/CNAME +0 -0
  36. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/assets/README.md +0 -0
  37. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/assets/favicon.png +0 -0
  38. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/assets/logo.png +0 -0
  39. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/assets/logo_rect.png +0 -0
  40. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/caption.md +0 -0
  41. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/changelog.md +0 -0
  42. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/changelog_update.py +0 -0
  43. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/common.md +0 -0
  44. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/contributing.md +0 -0
  45. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/arcgis.ipynb +0 -0
  46. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/automatic_mask_generator.ipynb +0 -0
  47. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/automatic_mask_generator_hq.ipynb +0 -0
  48. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/box_prompts.ipynb +0 -0
  49. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/data/tree_boxes.geojson +0 -0
  50. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/fast_sam.ipynb +0 -0
  51. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/image_captioning.ipynb +0 -0
  52. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/input_prompts.ipynb +0 -0
  53. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/input_prompts_hq.ipynb +0 -0
  54. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/maxar_open_data.ipynb +0 -0
  55. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam2_automatic.ipynb +0 -0
  56. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam2_box_prompts.ipynb +0 -0
  57. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam2_point_prompts.ipynb +0 -0
  58. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam2_predictor.ipynb +0 -0
  59. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam2_text_prompts.ipynb +0 -0
  60. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam2_video.ipynb +0 -0
  61. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam3_automated_segmentation.ipynb +0 -0
  62. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam3_batch_segmentation.ipynb +0 -0
  63. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam3_box_prompts.ipynb +0 -0
  64. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam3_image_segmentation.ipynb +0 -0
  65. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam3_image_segmentation_jpg.ipynb +0 -0
  66. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam3_interactive.ipynb +0 -0
  67. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam3_object_tracking.ipynb +0 -0
  68. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam3_point_prompts.ipynb +0 -0
  69. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam3_point_prompts_batch.ipynb +0 -0
  70. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam3_video_masks.ipynb +0 -0
  71. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam3_video_prompts.ipynb +0 -0
  72. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/sam3_video_segmentation.ipynb +0 -0
  73. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/satellite-predictor.ipynb +0 -0
  74. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/satellite.ipynb +0 -0
  75. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/text_prompts.ipynb +0 -0
  76. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/text_prompts_batch.ipynb +0 -0
  77. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/text_swimming_pools.ipynb +0 -0
  78. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/examples/tree_mapping.ipynb +0 -0
  79. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/faq.md +0 -0
  80. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/fast_sam.md +0 -0
  81. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/hq_sam.md +0 -0
  82. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/index.md +0 -0
  83. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/installation.md +0 -0
  84. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/overrides/main.html +0 -0
  85. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/samgeo.md +0 -0
  86. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/samgeo2.md +0 -0
  87. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/samgeo3.md +0 -0
  88. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/text_sam.md +0 -0
  89. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/usage.md +0 -0
  90. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/workshops/AIforGood_2025.ipynb +0 -0
  91. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/workshops/IPPN_2024.ipynb +0 -0
  92. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/workshops/cn_workshop.ipynb +0 -0
  93. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/workshops/jupytext.toml +0 -0
  94. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/docs/workshops/purdue.ipynb +0 -0
  95. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/paper/10.21105.joss.05663.pdf +0 -0
  96. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/paper/paper.bib +0 -0
  97. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/paper/paper.md +0 -0
  98. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/qgis-samgeo-plugin/LICENSE +0 -0
  99. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/qgis-samgeo-plugin/README.md +0 -0
  100. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/qgis-samgeo-plugin/__init__.py +0 -0
  101. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/qgis-samgeo-plugin/icons/icon.png +0 -0
  102. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/qgis-samgeo-plugin/install_plugin.py +0 -0
  103. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/qgis-samgeo-plugin/install_plugin.sh +0 -0
  104. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/qgis-samgeo-plugin/map_tools.py +0 -0
  105. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/qgis-samgeo-plugin/metadata.txt +0 -0
  106. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/qgis-samgeo-plugin/resources.py +0 -0
  107. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/qgis-samgeo-plugin/samgeo_plugin.py +0 -0
  108. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/qgis-samgeo-plugin/test_plugin.py +0 -0
  109. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/requirements_dev.txt +0 -0
  110. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/requirements_docs.txt +0 -0
  111. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/samgeo/caption.py +0 -0
  112. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/samgeo/fast_sam.py +0 -0
  113. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/samgeo/fer.py +0 -0
  114. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/samgeo/hq_sam.py +0 -0
  115. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/samgeo/samgeo.py +0 -0
  116. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/samgeo/samgeo2.py +0 -0
  117. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/samgeo/text_sam.py +0 -0
  118. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/samgeo/utmconv.py +0 -0
  119. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/segment_geospatial.egg-info/dependency_links.txt +0 -0
  120. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/segment_geospatial.egg-info/top_level.txt +0 -0
  121. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/setup.cfg +0 -0
  122. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/tests/__init__.py +0 -0
  123. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/tests/test_common.py +0 -0
  124. {segment_geospatial-1.0.3 → segment_geospatial-1.2.0}/tests/test_samgeo.py +0 -0
@@ -14,7 +14,7 @@ jobs:
14
14
  # This should be the path to the paper within your repo.
15
15
  paper-path: paper/paper.md
16
16
  - name: Upload
17
- uses: actions/upload-artifact@v5
17
+ uses: actions/upload-artifact@v6
18
18
  with:
19
19
  name: paper
20
20
  # This is the output path where Pandoc will write the compiled
@@ -48,6 +48,8 @@ jobs:
48
48
  pip install --user -r requirements.txt
49
49
  pip install --user -r requirements_dev.txt
50
50
  pip install --user .
51
- - name: PKG-TEST
52
- run: |
53
- python -m unittest discover tests/
51
+ - name: Test import
52
+ run: python -c "import samgeo; print('samgeo import successful')"
53
+ # - name: PKG-TEST
54
+ # run: |
55
+ # python -m unittest discover tests/
@@ -12,7 +12,7 @@ repos:
12
12
  args: ["--maxkb=500"]
13
13
 
14
14
  - repo: https://github.com/astral-sh/ruff-pre-commit
15
- rev: v0.7.4
15
+ rev: v0.14.9
16
16
  hooks:
17
17
  - id: ruff
18
18
  types_or: [pyi, jupyter]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: segment-geospatial
3
- Version: 1.0.3
3
+ Version: 1.2.0
4
4
  Summary: Meta AI' Segment Anything Model (SAM) for Geospatial Data.
5
5
  Author-email: Qiusheng Wu <giswqs@gmail.com>
6
6
  License: MIT license
@@ -29,6 +29,7 @@ Requires-Dist: pyproj
29
29
  Requires-Dist: rasterio
30
30
  Requires-Dist: segment_anything
31
31
  Requires-Dist: shapely
32
+ Requires-Dist: smoothify
32
33
  Requires-Dist: torch
33
34
  Requires-Dist: torchvision
34
35
  Requires-Dist: tqdm
@@ -0,0 +1,274 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Tiled Segmentation for Large GeoTIFF Images with SAM 3\n",
8
+ "\n",
9
+ "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/segment-geospatial/blob/main/docs/examples/sam3_tiled_segmentation.ipynb)\n",
10
+ "\n",
11
+ "This notebook demonstrates how to use the sliding window (tiled) approach to segment large GeoTIFF images that would otherwise exceed GPU memory limits.\n",
12
+ "\n",
13
+ "## Overview\n",
14
+ "\n",
15
+ "When working with large satellite or aerial imagery, loading the entire image into GPU memory for SAM 3 inference is often not feasible. The `generate_masks_tiled()` method solves this by:\n",
16
+ "\n",
17
+ "1. **Dividing** the large image into smaller, overlapping tiles\n",
18
+ "2. **Processing** each tile independently with SAM 3\n",
19
+ "3. **Merging** the results back into a seamless output mask\n",
20
+ "4. **Preserving** georeferencing information from the original GeoTIFF\n",
21
+ "\n",
22
+ "## Key Parameters\n",
23
+ "\n",
24
+ "- **tile_size**: Size of each processing tile (default: 1024 pixels)\n",
25
+ "- **overlap**: Overlap between adjacent tiles (default: 128 pixels) - helps prevent edge artifacts\n",
26
+ "- **prompt**: Text description of objects to segment\n",
27
+ "- **min_size/max_size**: Filter objects by pixel area"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "markdown",
32
+ "metadata": {},
33
+ "source": [
34
+ "## Installation\n",
35
+ "\n",
36
+ "Uncomment and run the following cell to install the required packages:"
37
+ ]
38
+ },
39
+ {
40
+ "cell_type": "code",
41
+ "execution_count": null,
42
+ "metadata": {},
43
+ "outputs": [],
44
+ "source": [
45
+ "# %pip install \"segment-geospatial[samgeo3]\""
46
+ ]
47
+ },
48
+ {
49
+ "cell_type": "markdown",
50
+ "metadata": {},
51
+ "source": [
52
+ "## Import Libraries"
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "code",
57
+ "execution_count": null,
58
+ "metadata": {},
59
+ "outputs": [],
60
+ "source": [
61
+ "import os\n",
62
+ "import leafmap\n",
63
+ "from samgeo import SamGeo3, common"
64
+ ]
65
+ },
66
+ {
67
+ "cell_type": "markdown",
68
+ "metadata": {},
69
+ "source": [
70
+ "## Download Sample Data\n",
71
+ "\n",
72
+ "We'll use a sample satellite image for this demonstration. You can replace this with your own large GeoTIFF."
73
+ ]
74
+ },
75
+ {
76
+ "cell_type": "code",
77
+ "execution_count": null,
78
+ "metadata": {},
79
+ "outputs": [],
80
+ "source": [
81
+ "# Download a sample satellite image\n",
82
+ "url = \"https://huggingface.co/datasets/giswqs/geospatial/resolve/main/naip/naip_water_train.tif\"\n",
83
+ "image_path = \"naip_water_train.tif\"\n",
84
+ "\n",
85
+ "if not os.path.exists(image_path):\n",
86
+ " common.download_file(url, image_path)"
87
+ ]
88
+ },
89
+ {
90
+ "cell_type": "markdown",
91
+ "metadata": {},
92
+ "source": [
93
+ "## Check Raster Info\n",
94
+ "\n",
95
+ "Let's check the dimensions of our image to understand why tiling might be necessary."
96
+ ]
97
+ },
98
+ {
99
+ "cell_type": "code",
100
+ "execution_count": null,
101
+ "metadata": {},
102
+ "outputs": [],
103
+ "source": [
104
+ "common.print_raster_info(image_path)"
105
+ ]
106
+ },
107
+ {
108
+ "cell_type": "markdown",
109
+ "metadata": {},
110
+ "source": [
111
+ "## Initialize SamGeo3"
112
+ ]
113
+ },
114
+ {
115
+ "cell_type": "code",
116
+ "execution_count": null,
117
+ "metadata": {},
118
+ "outputs": [],
119
+ "source": [
120
+ "sam = SamGeo3(backend=\"meta\")"
121
+ ]
122
+ },
123
+ {
124
+ "cell_type": "markdown",
125
+ "metadata": {},
126
+ "source": [
127
+ "## Run Tiled Segmentation\n",
128
+ "\n",
129
+ "Now we'll use the `generate_masks_tiled()` method to process the image. This method:\n",
130
+ "\n",
131
+ "1. Reads the image tile by tile\n",
132
+ "2. Processes each tile with SAM3\n",
133
+ "3. Merges overlapping regions intelligently\n",
134
+ "4. Saves the result as a georeferenced GeoTIFF"
135
+ ]
136
+ },
137
+ {
138
+ "cell_type": "code",
139
+ "execution_count": null,
140
+ "metadata": {},
141
+ "outputs": [],
142
+ "source": [
143
+ "# Output path for the mask\n",
144
+ "output_path = \"segmentation_mask.tif\"\n",
145
+ "\n",
146
+ "# Run tiled segmentation\n",
147
+ "sam.generate_masks_tiled(\n",
148
+ " source=image_path,\n",
149
+ " prompt=\"water\", # Change prompt based on what you want to segment\n",
150
+ " output=output_path,\n",
151
+ " tile_size=1024, # Size of each tile (adjust based on GPU memory)\n",
152
+ " overlap=128, # Overlap between tiles\n",
153
+ " min_size=100, # Minimum object size in pixels\n",
154
+ " unique=False, # Create binary mask (0 or 1)\n",
155
+ " dtype=\"int32\", # Data type for output\n",
156
+ " verbose=True, # Show progress\n",
157
+ ")"
158
+ ]
159
+ },
160
+ {
161
+ "cell_type": "markdown",
162
+ "metadata": {},
163
+ "source": [
164
+ "## Visualize Results"
165
+ ]
166
+ },
167
+ {
168
+ "cell_type": "code",
169
+ "execution_count": null,
170
+ "metadata": {},
171
+ "outputs": [],
172
+ "source": [
173
+ "m = leafmap.Map()\n",
174
+ "m.add_raster(image_path, layer_name=\"Original Image\")\n",
175
+ "m.add_raster(\n",
176
+ " output_path, nodata=0, opacity=0.8, cmap=\"Blues\", layer_name=\"Segmentation Mask\"\n",
177
+ ")\n",
178
+ "m"
179
+ ]
180
+ },
181
+ {
182
+ "cell_type": "markdown",
183
+ "metadata": {},
184
+ "source": [
185
+ "## Convert Mask to Vector\n",
186
+ "\n",
187
+ "You can convert the raster mask to vector format (GeoPackage, Shapefile, etc.) for further analysis in GIS software."
188
+ ]
189
+ },
190
+ {
191
+ "cell_type": "code",
192
+ "execution_count": null,
193
+ "metadata": {},
194
+ "outputs": [],
195
+ "source": [
196
+ "# Convert mask to vector\n",
197
+ "vector_path = \"segmentation_mask.gpkg\"\n",
198
+ "common.raster_to_vector(output_path, vector_path)"
199
+ ]
200
+ },
201
+ {
202
+ "cell_type": "markdown",
203
+ "metadata": {},
204
+ "source": [
205
+ "## Smooth Vector"
206
+ ]
207
+ },
208
+ {
209
+ "cell_type": "code",
210
+ "execution_count": null,
211
+ "metadata": {},
212
+ "outputs": [],
213
+ "source": [
214
+ "smooth_vector_path = \"segmentation_mask_smooth.gpkg\"\n",
215
+ "gdf = common.smooth_vector(vector_path, smooth_vector_path)"
216
+ ]
217
+ },
218
+ {
219
+ "cell_type": "code",
220
+ "execution_count": null,
221
+ "metadata": {},
222
+ "outputs": [],
223
+ "source": [
224
+ "m.add_gdf(gdf, layer_name=\"Smoothed Vector\", info_mode=None)\n",
225
+ "m"
226
+ ]
227
+ },
228
+ {
229
+ "cell_type": "code",
230
+ "execution_count": null,
231
+ "metadata": {},
232
+ "outputs": [],
233
+ "source": []
234
+ },
235
+ {
236
+ "cell_type": "markdown",
237
+ "metadata": {},
238
+ "source": [
239
+ "## Tips for Processing Large Images\n",
240
+ "\n",
241
+ "1. **Tile Size**: Larger tiles capture more context but require more GPU memory. Start with 512 or 1024 and increase if you have sufficient GPU memory.\n",
242
+ "\n",
243
+ "2. **Overlap**: Higher overlap (e.g., 128-256) helps prevent artifacts at tile boundaries but increases processing time. Lower overlap (e.g., 64) is faster but may have more edge effects.\n",
244
+ "\n",
245
+ "3. **Memory Management**: The method automatically clears GPU memory after each tile. If you still encounter memory issues, try reducing the tile_size.\n",
246
+ "\n",
247
+ "4. **Data Type**: Use `int32` for images with many objects, `int16` for up to 65535 objects, or `int8` for up to 255 objects.\n",
248
+ "\n",
249
+ "5. **Filtering**: Use `min_size` and `max_size` to filter out noise (small objects) or irrelevant large regions."
250
+ ]
251
+ }
252
+ ],
253
+ "metadata": {
254
+ "kernelspec": {
255
+ "display_name": "geo",
256
+ "language": "python",
257
+ "name": "python3"
258
+ },
259
+ "language_info": {
260
+ "codemirror_mode": {
261
+ "name": "ipython",
262
+ "version": 3
263
+ },
264
+ "file_extension": ".py",
265
+ "mimetype": "text/x-python",
266
+ "name": "python",
267
+ "nbconvert_exporter": "python",
268
+ "pygments_lexer": "ipython3",
269
+ "version": "3.12.2"
270
+ }
271
+ },
272
+ "nbformat": 4,
273
+ "nbformat_minor": 4
274
+ }
@@ -81,6 +81,7 @@ nav:
81
81
  - examples/sam3_point_prompts.ipynb
82
82
  - examples/sam3_point_prompts_batch.ipynb
83
83
  - examples/sam3_box_prompts.ipynb
84
+ - examples/sam3_tiled_segmentation.ipynb
84
85
  - Workshops:
85
86
  - workshops/purdue.ipynb
86
87
  - workshops/cn_workshop.ipynb
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "segment-geospatial"
7
- version = "1.0.3"
7
+ version = "1.2.0"
8
8
  dynamic = [
9
9
  "dependencies",
10
10
  ]
@@ -111,7 +111,7 @@ dependencies = {file = ["requirements.txt"]}
111
111
  universal = true
112
112
 
113
113
  [tool.bumpversion]
114
- current_version = "1.0.3"
114
+ current_version = "1.2.0"
115
115
  commit = true
116
116
  tag = true
117
117
 
@@ -10,6 +10,7 @@ pyproj
10
10
  rasterio
11
11
  segment_anything
12
12
  shapely
13
+ smoothify
13
14
  torch
14
15
  torchvision
15
16
  tqdm
@@ -2,7 +2,7 @@
2
2
 
3
3
  __author__ = """Qiusheng Wu"""
4
4
  __email__ = "giswqs@gmail.com"
5
- __version__ = "1.0.3"
5
+ __version__ = "1.2.0"
6
6
 
7
7
 
8
8
  from .samgeo import * # noqa: F403
@@ -4,7 +4,7 @@ The source code is adapted from https://github.com/aliaksandr960/segment-anythin
4
4
 
5
5
  import os
6
6
  import tempfile
7
- from typing import Any, List, Optional, Tuple, Union
7
+ from typing import Any, Dict, List, Optional, Tuple, Union
8
8
 
9
9
  import cv2
10
10
  import geopandas as gpd
@@ -3737,27 +3737,50 @@ def make_temp_dir(**kwargs) -> str:
3737
3737
  return temp_dir
3738
3738
 
3739
3739
 
3740
- def geotiff_to_jpg(geotiff_path: str, output_path: str) -> None:
3740
+ def geotiff_to_jpg(
3741
+ geotiff_path: str,
3742
+ output_path: str,
3743
+ bands: Optional[List[int]] = None,
3744
+ ) -> None:
3741
3745
  """Convert a GeoTIFF file to a JPG file.
3742
3746
 
3743
3747
  Args:
3744
3748
  geotiff_path (str): The path to the input GeoTIFF file.
3745
3749
  output_path (str): The path to the output JPG file.
3750
+ bands (List[int], optional): List of band indices (1-based) to use for RGB.
3751
+ For example, [4, 3, 2] for NIR-R-G false color composite.
3752
+ If None, uses the first 3 bands. Defaults to None.
3746
3753
  """
3747
3754
 
3748
3755
  from PIL import Image
3749
3756
 
3750
3757
  # Open the GeoTIFF file
3751
3758
  with rasterio.open(geotiff_path) as src:
3752
- # Read the first band (for grayscale) or all bands
3753
- array = src.read()
3754
-
3755
- # If the array has more than 3 bands, reduce it to the first 3 (RGB)
3756
- if array.shape[0] >= 3:
3757
- array = array[:3, :, :] # Select the first 3 bands (R, G, B)
3758
- elif array.shape[0] == 1:
3759
- # For single-band images, repeat the band to create a grayscale RGB
3760
- array = np.repeat(array, 3, axis=0)
3759
+ if bands is not None:
3760
+ # Validate band indices (1-based)
3761
+ if len(bands) != 3:
3762
+ raise ValueError("bands must contain exactly 3 band indices for RGB.")
3763
+ for band in bands:
3764
+ if band < 1 or band > src.count:
3765
+ raise ValueError(
3766
+ f"Band index {band} is out of range. "
3767
+ f"Image has {src.count} bands (1-indexed)."
3768
+ )
3769
+ # Read specified bands (rasterio uses 1-based indexing)
3770
+ array = np.stack([src.read(b) for b in bands], axis=0)
3771
+ else:
3772
+ # Read all bands
3773
+ array = src.read()
3774
+
3775
+ # If the array has more than 3 bands, reduce it to the first 3 (RGB)
3776
+ if array.shape[0] >= 3:
3777
+ array = array[:3, :, :] # Select the first 3 bands (R, G, B)
3778
+ elif array.shape[0] == 1:
3779
+ # For single-band images, repeat the band to create a grayscale RGB
3780
+ array = np.repeat(array, 3, axis=0)
3781
+ elif array.shape[0] == 2:
3782
+ # For two-band images, repeat the first band to create a 3-band image
3783
+ array = np.concatenate([array, array[0:1, :, :]], axis=0)
3761
3784
 
3762
3785
  # Transpose the array from (bands, height, width) to (height, width, bands)
3763
3786
  array = np.transpose(array, (1, 2, 0))
@@ -3774,12 +3797,19 @@ def geotiff_to_jpg(geotiff_path: str, output_path: str) -> None:
3774
3797
  image.save(output_path)
3775
3798
 
3776
3799
 
3777
- def geotiff_to_jpg_batch(input_folder: str, output_folder: str = None) -> str:
3800
+ def geotiff_to_jpg_batch(
3801
+ input_folder: str,
3802
+ output_folder: str = None,
3803
+ bands: Optional[List[int]] = None,
3804
+ ) -> str:
3778
3805
  """Convert all GeoTIFF files in a folder to JPG files.
3779
3806
 
3780
3807
  Args:
3781
3808
  input_folder (str): The path to the folder containing GeoTIFF files.
3782
3809
  output_folder (str): The path to the folder to save the output JPG files.
3810
+ bands (List[int], optional): List of band indices (1-based) to use for RGB.
3811
+ For example, [4, 3, 2] for NIR-R-G false color composite.
3812
+ If None, uses the first 3 bands. Defaults to None.
3783
3813
 
3784
3814
  Returns:
3785
3815
  str: The path to the output folder containing the JPG files.
@@ -3800,7 +3830,7 @@ def geotiff_to_jpg_batch(input_folder: str, output_folder: str = None) -> str:
3800
3830
  geotiff_path = os.path.join(input_folder, filename)
3801
3831
  jpg_filename = os.path.splitext(filename)[0] + ".jpg"
3802
3832
  output_path = os.path.join(output_folder, jpg_filename)
3803
- geotiff_to_jpg(geotiff_path, output_path)
3833
+ geotiff_to_jpg(geotiff_path, output_path, bands=bands)
3804
3834
 
3805
3835
  return output_folder
3806
3836
 
@@ -4025,3 +4055,220 @@ def get_device() -> torch.device:
4025
4055
  return torch.device("mps")
4026
4056
  else:
4027
4057
  return torch.device("cpu")
4058
+
4059
+
4060
+ def get_raster_info(raster_path: str) -> Dict[str, Any]:
4061
+ """Display basic information about a raster dataset.
4062
+
4063
+ Args:
4064
+ raster_path (str): Path to the raster file
4065
+
4066
+ Returns:
4067
+ dict: Dictionary containing the basic information about the raster
4068
+ """
4069
+ # Open the raster dataset
4070
+ with rasterio.open(raster_path) as src:
4071
+ # Get basic metadata
4072
+ info = {
4073
+ "driver": src.driver,
4074
+ "width": src.width,
4075
+ "height": src.height,
4076
+ "count": src.count,
4077
+ "dtype": src.dtypes[0],
4078
+ "crs": src.crs.to_string() if src.crs else "No CRS defined",
4079
+ "transform": src.transform,
4080
+ "bounds": src.bounds,
4081
+ "resolution": (src.transform[0], -src.transform[4]),
4082
+ "nodata": src.nodata,
4083
+ }
4084
+
4085
+ # Calculate statistics for each band
4086
+ stats = []
4087
+ for i in range(1, src.count + 1):
4088
+ band = src.read(i, masked=True)
4089
+ band_stats = {
4090
+ "band": i,
4091
+ "min": float(band.min()),
4092
+ "max": float(band.max()),
4093
+ "mean": float(band.mean()),
4094
+ "std": float(band.std()),
4095
+ }
4096
+ stats.append(band_stats)
4097
+
4098
+ info["band_stats"] = stats
4099
+
4100
+ return info
4101
+
4102
+
4103
+ def get_raster_stats(raster_path: str, divide_by: float = 1.0) -> Dict[str, Any]:
4104
+ """Calculate statistics for each band in a raster dataset.
4105
+
4106
+ This function computes min, max, mean, and standard deviation values
4107
+ for each band in the provided raster, returning results in a dictionary
4108
+ with lists for each statistic type.
4109
+
4110
+ Args:
4111
+ raster_path (str): Path to the raster file
4112
+ divide_by (float, optional): Value to divide pixel values by.
4113
+ Defaults to 1.0, which keeps the original pixel values unchanged.
4114
+
4115
+ Returns:
4116
+ dict: Dictionary containing lists of statistics with keys:
4117
+ - 'min': List of minimum values for each band
4118
+ - 'max': List of maximum values for each band
4119
+ - 'mean': List of mean values for each band
4120
+ - 'std': List of standard deviation values for each band
4121
+ """
4122
+ # Initialize the results dictionary with empty lists
4123
+ stats = {"min": [], "max": [], "mean": [], "std": []}
4124
+
4125
+ # Open the raster dataset
4126
+ with rasterio.open(raster_path) as src:
4127
+ # Calculate statistics for each band
4128
+ for i in range(1, src.count + 1):
4129
+ band = src.read(i, masked=True)
4130
+
4131
+ # Append statistics for this band to each list
4132
+ stats["min"].append(float(band.min()) / divide_by)
4133
+ stats["max"].append(float(band.max()) / divide_by)
4134
+ stats["mean"].append(float(band.mean()) / divide_by)
4135
+ stats["std"].append(float(band.std()) / divide_by)
4136
+
4137
+ return stats
4138
+
4139
+
4140
+ def print_raster_info(
4141
+ raster_path: str, show_preview: bool = True, figsize: Tuple[int, int] = (10, 8)
4142
+ ) -> Optional[Dict[str, Any]]:
4143
+ """Print formatted information about a raster dataset and optionally show a preview.
4144
+
4145
+ Args:
4146
+ raster_path (str): Path to the raster file
4147
+ show_preview (bool, optional): Whether to display a visual preview of the raster.
4148
+ Defaults to True.
4149
+ figsize (tuple, optional): Figure size as (width, height). Defaults to (10, 8).
4150
+
4151
+ Returns:
4152
+ dict: Dictionary containing raster information if successful, None otherwise
4153
+ """
4154
+ import matplotlib.pyplot as plt
4155
+ from rasterio.plot import show
4156
+
4157
+ try:
4158
+ info = get_raster_info(raster_path)
4159
+
4160
+ # Print basic information
4161
+ print(f"===== RASTER INFORMATION: {raster_path} =====")
4162
+ print(f"Driver: {info['driver']}")
4163
+ print(f"Dimensions: {info['width']} x {info['height']} pixels")
4164
+ print(f"Number of bands: {info['count']}")
4165
+ print(f"Data type: {info['dtype']}")
4166
+ print(f"Coordinate Reference System: {info['crs']}")
4167
+ print(f"Georeferenced Bounds: {info['bounds']}")
4168
+ print(f"Pixel Resolution: {info['resolution'][0]}, {info['resolution'][1]}")
4169
+ print(f"NoData Value: {info['nodata']}")
4170
+
4171
+ # Print band statistics
4172
+ print("\n----- Band Statistics -----")
4173
+ for band_stat in info["band_stats"]:
4174
+ print(f"Band {band_stat['band']}:")
4175
+ print(f" Min: {band_stat['min']:.2f}")
4176
+ print(f" Max: {band_stat['max']:.2f}")
4177
+ print(f" Mean: {band_stat['mean']:.2f}")
4178
+ print(f" Std Dev: {band_stat['std']:.2f}")
4179
+
4180
+ # Show a preview if requested
4181
+ if show_preview:
4182
+ with rasterio.open(raster_path) as src:
4183
+ # For multi-band images, show RGB composite or first band
4184
+ if src.count >= 3:
4185
+ # Try to show RGB composite
4186
+ rgb = np.dstack([src.read(i) for i in range(1, 4)])
4187
+ plt.figure(figsize=figsize)
4188
+ plt.imshow(rgb)
4189
+ plt.title(f"RGB Preview: {raster_path}")
4190
+ else:
4191
+ # Show first band for single-band images
4192
+ plt.figure(figsize=figsize)
4193
+ show(
4194
+ src.read(1),
4195
+ cmap="viridis",
4196
+ title=f"Band 1 Preview: {raster_path}",
4197
+ )
4198
+ plt.colorbar(label="Pixel Value")
4199
+ plt.show()
4200
+
4201
+ return info
4202
+
4203
+ except Exception as e:
4204
+ print(f"Error reading raster: {str(e)}")
4205
+ return None
4206
+
4207
+
4208
+ def smooth_vector(
4209
+ vector_data: Union[str, gpd.GeoDataFrame],
4210
+ output_path: str = None,
4211
+ segment_length: float = None,
4212
+ smooth_iterations: int = 3,
4213
+ num_cores: int = 0,
4214
+ merge_collection: bool = True,
4215
+ merge_field: str = None,
4216
+ merge_multipolygons: bool = True,
4217
+ preserve_area: bool = True,
4218
+ area_tolerance: float = 0.01,
4219
+ **kwargs: Any,
4220
+ ) -> gpd.GeoDataFrame:
4221
+ """Smooth a vector data using the smoothify library.
4222
+ See https://github.com/DPIRD-DMA/Smoothify for more details.
4223
+
4224
+ Args:
4225
+ vector_data: The vector data to smooth.
4226
+ output_path: The path to save the smoothed vector data. If None, returns the smoothed vector data.
4227
+ segment_length: Resolution of the original raster data in map units. If None (default), automatically
4228
+ detects by finding the minimum segment length (from a data sample). Recommended to specify explicitly when known.
4229
+ smooth_iterations: The number of iterations to smooth the vector data.
4230
+ num_cores: Number of cores to use for parallel processing. If 0 (default), uses all available cores.
4231
+ merge_collection: Whether to merge/dissolve adjacent geometries in collections before smoothing.
4232
+ merge_field: Column name to use for dissolving geometries. Only valid when merge_collection=True.
4233
+ If None, dissolves all geometries together. If specified, dissolves geometries grouped by the column values.
4234
+ merge_multipolygons: Whether to merge adjacent polygons within MultiPolygons before smoothing
4235
+ preserve_area: Whether to restore original area after smoothing via buffering (applies to Polygons only)
4236
+ area_tolerance: Percentage of original area allowed as error (e.g., 0.01 = 0.01% error = 99.99% preservation).
4237
+ Only affects Polygons when preserve_area=True
4238
+
4239
+ Returns:
4240
+ gpd.GeoDataFrame: The smoothed vector data.
4241
+
4242
+ Examples:
4243
+ >>> from samgeo import common
4244
+ >>> gdf = common.read_vector("path/to/vector.geojson")
4245
+ >>> smoothed_gdf = common.smooth_vector(gdf, smooth_iterations=3, output_path="path/to/smoothed_vector.geojson")
4246
+ >>> smoothed_gdf.head()
4247
+ >>> smoothed_gdf.explore()
4248
+ """
4249
+ import leafmap
4250
+
4251
+ try:
4252
+ from smoothify import smoothify
4253
+ except ImportError:
4254
+ install_package("smoothify")
4255
+ from smoothify import smoothify
4256
+
4257
+ if isinstance(vector_data, str):
4258
+ vector_data = leafmap.read_vector(vector_data)
4259
+
4260
+ smoothed_vector_data = smoothify(
4261
+ geom=vector_data,
4262
+ segment_length=segment_length,
4263
+ smooth_iterations=smooth_iterations,
4264
+ num_cores=num_cores,
4265
+ merge_collection=merge_collection,
4266
+ merge_field=merge_field,
4267
+ merge_multipolygons=merge_multipolygons,
4268
+ preserve_area=preserve_area,
4269
+ area_tolerance=area_tolerance,
4270
+ **kwargs,
4271
+ )
4272
+ if output_path is not None:
4273
+ smoothed_vector_data.to_file(output_path)
4274
+ return smoothed_vector_data