segment-geospatial 1.3.0__tar.gz → 1.3.2__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 (131) hide show
  1. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/PKG-INFO +1 -1
  2. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/api.md +124 -9
  3. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/pyproject.toml +2 -2
  4. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/__init__.py +1 -1
  5. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/api.py +315 -24
  6. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/samgeo3.py +81 -0
  7. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/segment_geospatial.egg-info/PKG-INFO +1 -1
  8. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.editorconfig +0 -0
  9. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/FUNDING.yml +0 -0
  10. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  11. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  12. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/dependabot.yaml +0 -0
  14. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/docker-image.yml +0 -0
  15. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/docker-publish.yml +0 -0
  16. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/docs-build.yml +0 -0
  17. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/docs.yml +0 -0
  18. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/draft-pdf.yml +0 -0
  19. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/macos.yml +0 -0
  20. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/pypi.yml +0 -0
  21. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/ubuntu.yml +0 -0
  22. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/windows.yml +0 -0
  23. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.gitignore +0 -0
  24. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.pre-commit-config.yaml +0 -0
  25. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/CITATION.cff +0 -0
  26. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/CODE_OF_CONDUCT.md +0 -0
  27. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/Dockerfile +0 -0
  28. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/LICENSE +0 -0
  29. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/MANIFEST.in +0 -0
  30. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/README.md +0 -0
  31. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/CNAME +0 -0
  32. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/assets/README.md +0 -0
  33. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/assets/favicon.png +0 -0
  34. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/assets/logo.png +0 -0
  35. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/assets/logo_rect.png +0 -0
  36. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/caption.md +0 -0
  37. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/changelog.md +0 -0
  38. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/changelog_update.py +0 -0
  39. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/common.md +0 -0
  40. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/contributing.md +0 -0
  41. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/detectree2.md +0 -0
  42. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/arcgis.ipynb +0 -0
  43. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/automatic_mask_generator.ipynb +0 -0
  44. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/automatic_mask_generator_hq.ipynb +0 -0
  45. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/box_prompts.ipynb +0 -0
  46. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/data/tree_boxes.geojson +0 -0
  47. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/detectree2.ipynb +0 -0
  48. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/fast_sam.ipynb +0 -0
  49. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/image_captioning.ipynb +0 -0
  50. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/input_prompts.ipynb +0 -0
  51. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/input_prompts_hq.ipynb +0 -0
  52. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/maxar_open_data.ipynb +0 -0
  53. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam2_automatic.ipynb +0 -0
  54. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam2_box_prompts.ipynb +0 -0
  55. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam2_point_prompts.ipynb +0 -0
  56. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam2_predictor.ipynb +0 -0
  57. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam2_text_prompts.ipynb +0 -0
  58. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam2_video.ipynb +0 -0
  59. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_automated_segmentation.ipynb +0 -0
  60. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_batch_segmentation.ipynb +0 -0
  61. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_box_prompts.ipynb +0 -0
  62. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_image_segmentation.ipynb +0 -0
  63. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_image_segmentation_jpg.ipynb +0 -0
  64. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_interactive.ipynb +0 -0
  65. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_object_tracking.ipynb +0 -0
  66. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_point_prompts.ipynb +0 -0
  67. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_point_prompts_batch.ipynb +0 -0
  68. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_tiled_segmentation.ipynb +0 -0
  69. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_video_masks.ipynb +0 -0
  70. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_video_prompts.ipynb +0 -0
  71. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_video_segmentation.ipynb +0 -0
  72. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/satellite-predictor.ipynb +0 -0
  73. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/satellite.ipynb +0 -0
  74. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/text_prompts.ipynb +0 -0
  75. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/text_prompts_batch.ipynb +0 -0
  76. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/text_swimming_pools.ipynb +0 -0
  77. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/tree_mapping.ipynb +0 -0
  78. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/faq.md +0 -0
  79. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/fast_sam.md +0 -0
  80. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/hq_sam.md +0 -0
  81. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/index.md +0 -0
  82. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/installation.md +0 -0
  83. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/overrides/main.html +0 -0
  84. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/samgeo.md +0 -0
  85. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/samgeo2.md +0 -0
  86. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/samgeo3.md +0 -0
  87. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/text_sam.md +0 -0
  88. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/usage.md +0 -0
  89. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/workshops/AIforGood_2025.ipynb +0 -0
  90. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/workshops/IPPN_2024.ipynb +0 -0
  91. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/workshops/cn_workshop.ipynb +0 -0
  92. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/workshops/jupytext.toml +0 -0
  93. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/workshops/purdue.ipynb +0 -0
  94. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/mkdocs.yml +0 -0
  95. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/paper/10.21105.joss.05663.pdf +0 -0
  96. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/paper/paper.bib +0 -0
  97. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/paper/paper.md +0 -0
  98. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/LICENSE +0 -0
  99. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/README.md +0 -0
  100. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/__init__.py +0 -0
  101. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/icons/icon.png +0 -0
  102. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/install_plugin.py +0 -0
  103. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/install_plugin.sh +0 -0
  104. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/map_tools.py +0 -0
  105. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/metadata.txt +0 -0
  106. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/resources.py +0 -0
  107. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/samgeo_plugin.py +0 -0
  108. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/test_plugin.py +0 -0
  109. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/requirements.txt +0 -0
  110. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/requirements_dev.txt +0 -0
  111. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/requirements_docs.txt +0 -0
  112. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/caption.py +0 -0
  113. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/common.py +0 -0
  114. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/detectree2.py +0 -0
  115. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/fast_sam.py +0 -0
  116. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/fer.py +0 -0
  117. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/hq_sam.py +0 -0
  118. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/samgeo.py +0 -0
  119. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/samgeo2.py +0 -0
  120. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/text_sam.py +0 -0
  121. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/utmconv.py +0 -0
  122. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/segment_geospatial.egg-info/SOURCES.txt +0 -0
  123. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/segment_geospatial.egg-info/dependency_links.txt +0 -0
  124. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/segment_geospatial.egg-info/entry_points.txt +0 -0
  125. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/segment_geospatial.egg-info/requires.txt +0 -0
  126. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/segment_geospatial.egg-info/top_level.txt +0 -0
  127. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/setup.cfg +0 -0
  128. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/tests/__init__.py +0 -0
  129. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/tests/test_api.py +0 -0
  130. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/tests/test_common.py +0 -0
  131. {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/tests/test_samgeo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: segment-geospatial
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: Meta AI's Segment Anything Model (SAM) for Geospatial Data.
5
5
  Author-email: Qiusheng Wu <giswqs@gmail.com>
6
6
  License: MIT license
@@ -97,7 +97,7 @@ Runs automatic mask generation on an uploaded image. Supports SAM, SAM2, and SAM
97
97
  | `file` | file | required | Image file (TIFF, PNG, JPEG) |
98
98
  | `model_version` | string | `sam2` | One of `sam`, `sam2`, `sam3` |
99
99
  | `model_id` | string | auto | Model identifier (e.g., `sam2-hiera-large`) |
100
- | `output_format` | string | `geojson` | One of `geojson`, `geotiff`, `png` |
100
+ | `output_format` | string | `geojson` | One of `geojson`, `geotiff`, `png`, `json`, `detections` |
101
101
  | `foreground` | bool | `true` | Extract foreground objects only |
102
102
  | `unique` | bool | `true` | Assign unique ID to each object |
103
103
  | `min_size` | int | `0` | Minimum mask size in pixels |
@@ -121,21 +121,25 @@ curl -X POST http://localhost:8000/segment/automatic \
121
121
  POST /segment/predict
122
122
  ```
123
123
 
124
- Runs segmentation with point or bounding box prompts. Supports SAM and SAM2.
124
+ Runs segmentation with point or bounding box prompts. Supports SAM, SAM2, and SAM3.
125
+
126
+ For SAM3 with bounding box prompts, the model finds **all similar objects** in the image (not just the object inside the box). Point prompts with SAM3 segment the specific object at the point location.
125
127
 
126
128
  **Parameters (multipart form):**
127
129
 
128
130
  | Parameter | Type | Default | Description |
129
131
  |-----------|------|---------|-------------|
130
132
  | `file` | file | required | Image file (TIFF, PNG, JPEG) |
131
- | `model_version` | string | `sam2` | One of `sam`, `sam2` |
133
+ | `model_version` | string | `sam3` | One of `sam`, `sam2`, `sam3` |
132
134
  | `model_id` | string | auto | Model identifier |
133
- | `output_format` | string | `geojson` | One of `geojson`, `geotiff`, `png` |
135
+ | `output_format` | string | `geojson` | One of `geojson`, `geotiff`, `png`, `json`, `detections` |
134
136
  | `point_coords` | string | none | JSON array of `[[x, y], ...]` |
135
137
  | `point_labels` | string | none | JSON array of `[1, 0, ...]` (1=foreground, 0=background) |
136
138
  | `boxes` | string | none | JSON array of `[[xmin, ymin, xmax, ymax], ...]` |
137
- | `point_crs` | string | none | CRS string (e.g., `EPSG:4326`) |
139
+ | `point_crs` | string | none | CRS string (e.g., `EPSG:4326`) for point/box coordinates |
138
140
  | `multimask_output` | bool | `false` | Return multiple masks per prompt |
141
+ | `min_size` | int | `0` | Minimum mask size in pixels |
142
+ | `max_size` | int | none | Maximum mask size in pixels |
139
143
 
140
144
  **Example with point prompts:**
141
145
 
@@ -147,13 +151,61 @@ curl -X POST http://localhost:8000/segment/predict \
147
151
  -F "output_format=geojson"
148
152
  ```
149
153
 
150
- **Example with box prompts:**
154
+ **Example with box prompts (finds all similar objects):**
151
155
 
152
156
  ```bash
153
157
  curl -X POST http://localhost:8000/segment/predict \
154
158
  -F "file=@image.tif" \
155
159
  -F "boxes=[[10, 20, 300, 400]]" \
156
- -F "output_format=geotiff"
160
+ -F "output_format=geojson"
161
+ ```
162
+
163
+ **Example with JSON output (pixel-coordinate bounding boxes):**
164
+
165
+ ```bash
166
+ curl -X POST http://localhost:8000/segment/predict \
167
+ -F "file=@image.jpg" \
168
+ -F "boxes=[[10, 20, 300, 400]]" \
169
+ -F "output_format=json"
170
+ ```
171
+
172
+ ```json
173
+ {
174
+ "image_width": 2647,
175
+ "image_height": 1464,
176
+ "num_detections": 12,
177
+ "detections": [
178
+ {"id": 1, "value": 1, "bbox": [50, 80, 200, 250], "width": 150, "height": 170},
179
+ {"id": 2, "value": 2, "bbox": [310, 45, 480, 210], "width": 170, "height": 165}
180
+ ]
181
+ }
182
+ ```
183
+
184
+ **Example with detections output (geographic-coordinate bounding boxes):**
185
+
186
+ ```bash
187
+ curl -X POST http://localhost:8000/segment/predict \
188
+ -F "file=@image.tif" \
189
+ -F "boxes=[[10, 20, 300, 400]]" \
190
+ -F "output_format=detections"
191
+ ```
192
+
193
+ ```json
194
+ {
195
+ "type": "FeatureCollection",
196
+ "crs": "EPSG:3857",
197
+ "num_detections": 12,
198
+ "features": [
199
+ {
200
+ "type": "Feature",
201
+ "geometry": {
202
+ "type": "Polygon",
203
+ "coordinates": [[[-13609328.39, 4561446.23], [-13609284.55, 4561446.23], [-13609284.55, 4561389.77], [-13609328.39, 4561389.77], [-13609328.39, 4561446.23]]]
204
+ },
205
+ "properties": {"id": 1, "value": 1, "bbox_pixel": [50.0, 80.0, 200.0, 250.0]}
206
+ }
207
+ ]
208
+ }
157
209
  ```
158
210
 
159
211
  ### Text-prompt Segmentation
@@ -172,12 +224,12 @@ Runs text-prompt segmentation using SAM3.
172
224
  | `prompt` | string | required | Text description (e.g., `building`, `tree`) |
173
225
  | `model_id` | string | auto | SAM3 model identifier |
174
226
  | `backend` | string | `meta` | One of `meta`, `transformers` |
175
- | `output_format` | string | `geojson` | One of `geojson`, `geotiff`, `png` |
227
+ | `output_format` | string | `geojson` | One of `geojson`, `geotiff`, `png`, `json`, `detections` |
176
228
  | `confidence_threshold` | float | `0.5` | Detection confidence threshold |
177
229
  | `min_size` | int | `0` | Minimum mask size in pixels |
178
230
  | `max_size` | int | none | Maximum mask size in pixels |
179
231
 
180
- **Example:**
232
+ **Example (GeoJSON mask polygons):**
181
233
 
182
234
  ```bash
183
235
  curl -X POST http://localhost:8000/segment/text \
@@ -186,6 +238,54 @@ curl -X POST http://localhost:8000/segment/text \
186
238
  -F "output_format=geojson"
187
239
  ```
188
240
 
241
+ **Example with JSON output (pixel-coordinate bounding boxes):**
242
+
243
+ ```bash
244
+ curl -X POST http://localhost:8000/segment/text \
245
+ -F "file=@image.jpg" \
246
+ -F "prompt=building" \
247
+ -F "output_format=json"
248
+ ```
249
+
250
+ ```json
251
+ {
252
+ "image_width": 2647,
253
+ "image_height": 1464,
254
+ "num_detections": 46,
255
+ "detections": [
256
+ {"id": 1, "bbox": [2506, 134, 2653, 324], "width": 147, "height": 190, "score": 0.887},
257
+ {"id": 2, "bbox": [1200, 450, 1380, 620], "width": 180, "height": 170, "score": 0.862}
258
+ ]
259
+ }
260
+ ```
261
+
262
+ **Example with detections output (geographic-coordinate bounding boxes):**
263
+
264
+ ```bash
265
+ curl -X POST http://localhost:8000/segment/text \
266
+ -F "file=@image.tif" \
267
+ -F "prompt=building" \
268
+ -F "output_format=detections"
269
+ ```
270
+
271
+ ```json
272
+ {
273
+ "type": "FeatureCollection",
274
+ "crs": "EPSG:3857",
275
+ "num_detections": 46,
276
+ "features": [
277
+ {
278
+ "type": "Feature",
279
+ "geometry": {
280
+ "type": "Polygon",
281
+ "coordinates": [[[-13609328.39, 4561446.23], [-13609284.55, 4561446.23], [-13609284.55, 4561389.77], [-13609328.39, 4561389.77], [-13609328.39, 4561446.23]]]
282
+ },
283
+ "properties": {"id": 1, "score": 0.887, "bbox_pixel": [2506.47, 134.43, 2653.27, 323.52]}
284
+ }
285
+ ]
286
+ }
287
+ ```
288
+
189
289
  ## Caching
190
290
 
191
291
  The API automatically caches models and image encodings for better performance:
@@ -208,6 +308,7 @@ import requests
208
308
 
209
309
  url = "http://localhost:8000/segment/text"
210
310
 
311
+ # Get GeoJSON mask polygons
211
312
  with open("image.tif", "rb") as f:
212
313
  response = requests.post(
213
314
  url,
@@ -219,6 +320,20 @@ geojson = response.json()
219
320
  print(f"Found {len(geojson['features'])} features")
220
321
  ```
221
322
 
323
+ ```python
324
+ # Get bounding boxes in pixel coordinates (suitable for non-georeferenced images)
325
+ with open("image.jpg", "rb") as f:
326
+ response = requests.post(
327
+ url,
328
+ files={"file": ("image.jpg", f, "image/jpeg")},
329
+ data={"prompt": "car", "output_format": "json"},
330
+ )
331
+
332
+ result = response.json()
333
+ for det in result["detections"]:
334
+ print(f"Object {det['id']}: bbox={det['bbox']}, score={det['score']:.3f}")
335
+ ```
336
+
222
337
  ## API Reference
223
338
 
224
339
  ::: samgeo.api
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "segment-geospatial"
7
- version = "1.3.0"
7
+ version = "1.3.2"
8
8
  dynamic = [
9
9
  "dependencies",
10
10
  ]
@@ -117,7 +117,7 @@ dependencies = {file = ["requirements.txt"]}
117
117
  universal = true
118
118
 
119
119
  [tool.bumpversion]
120
- current_version = "1.3.0"
120
+ current_version = "1.3.2"
121
121
  commit = true
122
122
  tag = true
123
123
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  __author__ = """Qiusheng Wu"""
4
4
  __email__ = "giswqs@gmail.com"
5
- __version__ = "1.3.0"
5
+ __version__ = "1.3.2"
6
6
 
7
7
 
8
8
  from .samgeo import * # noqa: F403
@@ -31,13 +31,14 @@ except ImportError:
31
31
 
32
32
  import numpy as np
33
33
  from fastapi import FastAPI, File, Form, HTTPException, UploadFile
34
+ from fastapi.middleware.cors import CORSMiddleware
34
35
  from fastapi.responses import FileResponse, JSONResponse
35
36
 
36
37
  from samgeo import __version__
37
38
 
38
39
  logger = logging.getLogger("uvicorn.error")
39
40
 
40
- _VALID_OUTPUT_FORMATS = {"geojson", "geotiff", "png"}
41
+ _VALID_OUTPUT_FORMATS = {"geojson", "geotiff", "png", "detections", "json"}
41
42
 
42
43
 
43
44
  def _normalize_max_size(max_size: Optional[int]) -> Optional[int]:
@@ -60,6 +61,14 @@ app = FastAPI(
60
61
  version=__version__,
61
62
  )
62
63
 
64
+ app.add_middleware(
65
+ CORSMiddleware,
66
+ allow_origins=["*"],
67
+ allow_credentials=True,
68
+ allow_methods=["*"],
69
+ allow_headers=["*"],
70
+ )
71
+
63
72
  # Model cache: (model_version, model_id) -> (model_instance, lock)
64
73
  _model_cache: dict = {}
65
74
  _model_cache_lock = threading.Lock()
@@ -118,7 +127,7 @@ def get_model(model_version: str, model_id: Optional[str] = None, **kwargs):
118
127
  ),
119
128
  )
120
129
 
121
- if model_id is None:
130
+ if not model_id:
122
131
  model_id = _DEFAULT_MODEL_IDS[model_version]
123
132
 
124
133
  valid_ids = _AVAILABLE_MODELS[model_version]
@@ -151,6 +160,7 @@ def get_model(model_version: str, model_id: Optional[str] = None, **kwargs):
151
160
  elif model_version == "sam3":
152
161
  from samgeo.samgeo3 import SamGeo3
153
162
 
163
+ kwargs.setdefault("enable_inst_interactivity", True)
154
164
  model = SamGeo3(**kwargs)
155
165
  except ImportError as e:
156
166
  raise HTTPException(
@@ -283,6 +293,214 @@ def _format_response(raster_path: str, output_format: str, tmpdir: str):
283
293
  png_path, media_type="image/png", filename="mask.png"
284
294
  )
285
295
 
296
+ elif output_format in ("json", "detections"):
297
+ data = _extract_bboxes_from_raster(raster_path, output_format)
298
+ _cleanup_tmpdir(tmpdir)
299
+ return JSONResponse(content=data)
300
+
301
+
302
+ def _extract_bboxes_from_raster(raster_path: str, output_format: str) -> dict:
303
+ """Extract bounding boxes from a raster mask file.
304
+
305
+ Each unique non-zero value in the raster is treated as a separate object.
306
+ Bounding boxes are computed from the pixel regions of each object.
307
+
308
+ Args:
309
+ raster_path: Path to the raster mask file.
310
+ output_format: Either "json" for pixel-coordinate bboxes or
311
+ "detections" for a GeoJSON FeatureCollection with geographic
312
+ coordinates.
313
+
314
+ Returns:
315
+ A dict with bounding box information in the requested format.
316
+ """
317
+ import rasterio
318
+
319
+ with rasterio.open(raster_path) as src:
320
+ mask = src.read(1)
321
+ transform = src.transform
322
+ crs = src.crs.to_string() if src.crs else None
323
+ has_georef = crs is not None
324
+
325
+ unique_vals = np.unique(mask)
326
+ unique_vals = unique_vals[unique_vals != 0]
327
+
328
+ if output_format == "json":
329
+ detections = []
330
+ for i, val in enumerate(unique_vals):
331
+ rows, cols = np.where(mask == val)
332
+ x1, y1, x2, y2 = int(cols.min()), int(rows.min()), int(cols.max()), int(rows.max())
333
+ detections.append({
334
+ "id": i + 1,
335
+ "value": int(val),
336
+ "bbox": [x1, y1, x2, y2],
337
+ "width": x2 - x1,
338
+ "height": y2 - y1,
339
+ })
340
+ return {
341
+ "image_width": int(mask.shape[1]),
342
+ "image_height": int(mask.shape[0]),
343
+ "num_detections": len(detections),
344
+ "detections": detections,
345
+ }
346
+
347
+ else: # detections (GeoJSON)
348
+ features = []
349
+ for i, val in enumerate(unique_vals):
350
+ rows, cols = np.where(mask == val)
351
+ x1, y1, x2, y2 = float(cols.min()), float(rows.min()), float(cols.max()), float(rows.max())
352
+
353
+ if has_georef:
354
+ geo_x1, geo_y1 = transform * (x1, y1)
355
+ geo_x2, geo_y2 = transform * (x2, y2)
356
+ else:
357
+ geo_x1, geo_y1 = x1, y1
358
+ geo_x2, geo_y2 = x2, y2
359
+
360
+ coords = [
361
+ [geo_x1, geo_y1],
362
+ [geo_x2, geo_y1],
363
+ [geo_x2, geo_y2],
364
+ [geo_x1, geo_y2],
365
+ [geo_x1, geo_y1],
366
+ ]
367
+ features.append({
368
+ "type": "Feature",
369
+ "geometry": {"type": "Polygon", "coordinates": [coords]},
370
+ "properties": {
371
+ "id": i + 1,
372
+ "value": int(val),
373
+ "bbox_pixel": [x1, y1, x2, y2],
374
+ },
375
+ })
376
+ result = {
377
+ "type": "FeatureCollection",
378
+ "features": features,
379
+ "num_detections": len(features),
380
+ }
381
+ if crs:
382
+ result["crs"] = crs
383
+ return result
384
+
385
+
386
+ def _build_detections_json(model) -> dict:
387
+ """Build a plain JSON response with bounding boxes and scores in pixel coordinates.
388
+
389
+ Suitable for non-georeferenced images where geographic coordinates are
390
+ not available.
391
+
392
+ Args:
393
+ model: The SAM model instance with boxes, scores, and image
394
+ dimension attributes.
395
+
396
+ Returns:
397
+ A dict with image dimensions and a list of detections, each containing
398
+ id, bbox (pixel coords), and score.
399
+ """
400
+ boxes = model.boxes if model.boxes is not None else []
401
+ scores = model.scores if model.scores is not None else []
402
+
403
+ detections = []
404
+ for i, box in enumerate(boxes):
405
+ box_np = np.asarray(box).flatten()
406
+ x1, y1, x2, y2 = int(round(box_np[0])), int(round(box_np[1])), int(round(box_np[2])), int(round(box_np[3]))
407
+ det = {
408
+ "id": i + 1,
409
+ "bbox": [x1, y1, x2, y2],
410
+ "width": x2 - x1,
411
+ "height": y2 - y1,
412
+ }
413
+ if i < len(scores):
414
+ score_val = scores[i]
415
+ det["score"] = float(score_val) if not isinstance(score_val, float) else score_val
416
+ detections.append(det)
417
+
418
+ return {
419
+ "image_width": model.image_width,
420
+ "image_height": model.image_height,
421
+ "num_detections": len(detections),
422
+ "detections": detections,
423
+ }
424
+
425
+
426
+ def _build_detections_geojson(model, source_path: str) -> dict:
427
+ """Build a GeoJSON FeatureCollection from detected bounding boxes and scores.
428
+
429
+ Converts pixel-coordinate bounding boxes to geographic coordinates when
430
+ the source image has georeferencing information. Falls back to pixel
431
+ coordinates otherwise.
432
+
433
+ Args:
434
+ model: The SAM model instance with boxes and scores attributes.
435
+ source_path: Path to the source image file.
436
+
437
+ Returns:
438
+ A GeoJSON FeatureCollection dict with one polygon feature per detection.
439
+ """
440
+ import rasterio
441
+
442
+ boxes = model.boxes if model.boxes is not None else []
443
+ scores = model.scores if model.scores is not None else []
444
+
445
+ has_georef = False
446
+ transform = None
447
+ crs = None
448
+ try:
449
+ if source_path and source_path.lower().endswith((".tif", ".tiff")):
450
+ with rasterio.open(source_path) as src:
451
+ if src.crs is not None:
452
+ transform = src.transform
453
+ crs = src.crs.to_string()
454
+ has_georef = True
455
+ except Exception:
456
+ pass
457
+
458
+ features = []
459
+ for i, box in enumerate(boxes):
460
+ box_np = np.asarray(box).flatten()
461
+ x1, y1, x2, y2 = float(box_np[0]), float(box_np[1]), float(box_np[2]), float(box_np[3])
462
+
463
+ if has_georef:
464
+ # Convert pixel coords to geographic coords
465
+ geo_x1, geo_y1 = transform * (x1, y1)
466
+ geo_x2, geo_y2 = transform * (x2, y2)
467
+ else:
468
+ geo_x1, geo_y1 = x1, y1
469
+ geo_x2, geo_y2 = x2, y2
470
+
471
+ # Build bbox polygon (clockwise)
472
+ coords = [
473
+ [geo_x1, geo_y1],
474
+ [geo_x2, geo_y1],
475
+ [geo_x2, geo_y2],
476
+ [geo_x1, geo_y2],
477
+ [geo_x1, geo_y1],
478
+ ]
479
+
480
+ props = {"id": i + 1}
481
+ if i < len(scores):
482
+ score_val = scores[i]
483
+ props["score"] = float(score_val) if not isinstance(score_val, float) else score_val
484
+ props["bbox_pixel"] = [x1, y1, x2, y2]
485
+
486
+ features.append(
487
+ {
488
+ "type": "Feature",
489
+ "geometry": {"type": "Polygon", "coordinates": [coords]},
490
+ "properties": props,
491
+ }
492
+ )
493
+
494
+ result = {
495
+ "type": "FeatureCollection",
496
+ "features": features,
497
+ "num_detections": len(features),
498
+ }
499
+ if crs:
500
+ result["crs"] = crs
501
+
502
+ return result
503
+
286
504
 
287
505
  def _cleanup_tmpdir(tmpdir: str) -> None:
288
506
  """Remove a temporary directory, ignoring errors.
@@ -416,7 +634,7 @@ async def segment_automatic(
416
634
  @app.post("/segment/predict")
417
635
  async def segment_predict(
418
636
  file: UploadFile = File(...),
419
- model_version: str = Form("sam2"),
637
+ model_version: str = Form("sam3"),
420
638
  model_id: Optional[str] = Form(None),
421
639
  output_format: str = Form("geojson"),
422
640
  point_coords: Optional[str] = Form(None),
@@ -429,16 +647,20 @@ async def segment_predict(
429
647
  ):
430
648
  """Run prompt-based segmentation with points or bounding boxes.
431
649
 
650
+ For SAM3 with bounding box prompts, the model finds all similar objects
651
+ in the image (not just the object inside the box). Point prompts with
652
+ SAM3 segment the specific object at the point location.
653
+
432
654
  Args:
433
655
  file: Image file (TIFF, PNG, JPEG).
434
- model_version: One of "sam", "sam2".
656
+ model_version: One of "sam", "sam2", "sam3".
435
657
  model_id: Specific model identifier.
436
- output_format: One of "geojson", "geotiff", "png".
658
+ output_format: One of "geojson", "geotiff", "png", "json", "detections".
437
659
  point_coords: JSON string of [[x, y], ...] coordinate pairs.
438
660
  point_labels: JSON string of [1, 0, ...] labels (1=foreground,
439
661
  0=background).
440
662
  boxes: JSON string of [[xmin, ymin, xmax, ymax], ...] bounding boxes.
441
- point_crs: CRS string (e.g., "EPSG:4326") for point coordinates.
663
+ point_crs: CRS string (e.g., "EPSG:4326") for point/box coordinates.
442
664
  multimask_output: Whether to return multiple masks per prompt.
443
665
  min_size: Minimum mask size in pixels.
444
666
  max_size: Maximum mask size in pixels.
@@ -448,11 +670,15 @@ async def segment_predict(
448
670
  """
449
671
  _validate_output_format(output_format)
450
672
 
451
- if model_version == "sam3":
452
- raise HTTPException(
453
- status_code=400,
454
- detail="Use /segment/text for SAM3 text-based segmentation.",
455
- )
673
+ # Swagger UI sends empty strings for unfilled optional fields
674
+ if not point_coords:
675
+ point_coords = None
676
+ if not point_labels:
677
+ point_labels = None
678
+ if not boxes:
679
+ boxes = None
680
+ if not point_crs:
681
+ point_crs = None
456
682
 
457
683
  if point_coords is None and boxes is None:
458
684
  raise HTTPException(
@@ -479,18 +705,61 @@ async def segment_predict(
479
705
  parsed_boxes = np.array(json.loads(boxes))
480
706
 
481
707
  t_start = time.time()
482
- model, lock = get_model(model_version, model_id, automatic=False)
483
- model_key = (model_version, model_id or _DEFAULT_MODEL_IDS[model_version])
484
- with lock:
485
- _set_image_cached(model, model_key, input_path, image_hash)
486
- model.predict(
487
- point_coords=parsed_coords,
488
- point_labels=parsed_labels,
489
- boxes=parsed_boxes,
490
- point_crs=point_crs,
491
- multimask_output=multimask_output,
492
- output=output_path,
708
+
709
+ if model_version == "sam3":
710
+ model, lock = get_model(model_version, model_id)
711
+ model_key = (
712
+ model_version,
713
+ model_id or _DEFAULT_MODEL_IDS[model_version],
714
+ )
715
+ with lock:
716
+ _set_image_cached(model, model_key, input_path, image_hash)
717
+ if parsed_boxes is not None:
718
+ # Use generate_masks_by_boxes to find all similar objects
719
+ box_list = parsed_boxes.tolist()
720
+ if parsed_boxes.ndim == 1:
721
+ box_list = [box_list]
722
+ model.generate_masks_by_boxes(
723
+ boxes=box_list,
724
+ box_crs=point_crs,
725
+ min_size=min_size,
726
+ max_size=max_size,
727
+ )
728
+ else:
729
+ # Use predict_inst for point-only prompts
730
+ model.predict_inst(
731
+ point_coords=parsed_coords,
732
+ point_labels=parsed_labels,
733
+ multimask_output=multimask_output,
734
+ point_crs=point_crs,
735
+ )
736
+ if model.masks is None or len(model.masks) == 0:
737
+ _cleanup_tmpdir(tmpdir)
738
+ raise HTTPException(
739
+ status_code=404,
740
+ detail="No objects found for the given prompts.",
741
+ )
742
+ model.save_masks(
743
+ output=output_path,
744
+ min_size=min_size,
745
+ max_size=max_size,
746
+ )
747
+ else:
748
+ model, lock = get_model(model_version, model_id, automatic=False)
749
+ model_key = (
750
+ model_version,
751
+ model_id or _DEFAULT_MODEL_IDS[model_version],
493
752
  )
753
+ with lock:
754
+ _set_image_cached(model, model_key, input_path, image_hash)
755
+ model.predict(
756
+ point_coords=parsed_coords,
757
+ point_labels=parsed_labels,
758
+ boxes=parsed_boxes,
759
+ point_crs=point_crs,
760
+ multimask_output=multimask_output,
761
+ output=output_path,
762
+ )
494
763
 
495
764
  t_inference = time.time() - t_start
496
765
  logger.info(
@@ -530,7 +799,11 @@ async def segment_text(
530
799
  prompt: Text description of objects to segment (e.g., "building").
531
800
  model_id: SAM3 model identifier.
532
801
  backend: SAM3 backend, one of "meta" or "transformers".
533
- output_format: One of "geojson", "geotiff", "png".
802
+ output_format: One of "geojson", "geotiff", "png", "detections", "json".
803
+ Use "detections" to get a GeoJSON FeatureCollection of bounding
804
+ box polygons in geographic coordinates with confidence scores.
805
+ Use "json" for a plain JSON array of bounding boxes in pixel
806
+ coordinates, suitable for non-georeferenced images.
534
807
  confidence_threshold: Confidence threshold for detections.
535
808
  min_size: Minimum mask size in pixels.
536
809
  max_size: Maximum mask size in pixels.
@@ -560,7 +833,22 @@ async def segment_text(
560
833
  min_size=min_size,
561
834
  max_size=max_size,
562
835
  )
563
- model.save_masks(output=output_path)
836
+ if model.masks is None or len(model.masks) == 0:
837
+ _cleanup_tmpdir(tmpdir)
838
+ raise HTTPException(
839
+ status_code=404,
840
+ detail=(
841
+ "No objects found for the given prompt. "
842
+ "Please try a different prompt or adjust parameters."
843
+ ),
844
+ )
845
+ if output_format in ("detections", "json"):
846
+ if output_format == "detections":
847
+ det_result = _build_detections_geojson(model, input_path)
848
+ else:
849
+ det_result = _build_detections_json(model)
850
+ else:
851
+ model.save_masks(output=output_path)
564
852
 
565
853
  t_inference = time.time() - t_start
566
854
  logger.info(
@@ -568,6 +856,9 @@ async def segment_text(
568
856
  t_inference,
569
857
  prompt,
570
858
  )
859
+ if output_format in ("detections", "json"):
860
+ _cleanup_tmpdir(tmpdir)
861
+ return JSONResponse(content=det_result)
571
862
  return _format_response(output_path, output_format, tmpdir)
572
863
  except HTTPException:
573
864
  _cleanup_tmpdir(tmpdir)