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.
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/PKG-INFO +1 -1
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/api.md +124 -9
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/pyproject.toml +2 -2
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/__init__.py +1 -1
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/api.py +315 -24
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/samgeo3.py +81 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/segment_geospatial.egg-info/PKG-INFO +1 -1
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.editorconfig +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/FUNDING.yml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/dependabot.yaml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/docker-image.yml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/docker-publish.yml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/docs-build.yml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/docs.yml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/draft-pdf.yml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/macos.yml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/pypi.yml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/ubuntu.yml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.github/workflows/windows.yml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.gitignore +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/.pre-commit-config.yaml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/CITATION.cff +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/CODE_OF_CONDUCT.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/Dockerfile +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/LICENSE +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/MANIFEST.in +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/README.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/CNAME +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/assets/README.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/assets/favicon.png +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/assets/logo.png +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/assets/logo_rect.png +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/caption.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/changelog.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/changelog_update.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/common.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/contributing.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/detectree2.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/arcgis.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/automatic_mask_generator.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/automatic_mask_generator_hq.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/box_prompts.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/data/tree_boxes.geojson +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/detectree2.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/fast_sam.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/image_captioning.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/input_prompts.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/input_prompts_hq.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/maxar_open_data.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam2_automatic.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam2_box_prompts.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam2_point_prompts.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam2_predictor.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam2_text_prompts.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam2_video.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_automated_segmentation.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_batch_segmentation.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_box_prompts.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_image_segmentation.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_image_segmentation_jpg.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_interactive.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_object_tracking.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_point_prompts.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_point_prompts_batch.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_tiled_segmentation.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_video_masks.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_video_prompts.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/sam3_video_segmentation.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/satellite-predictor.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/satellite.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/text_prompts.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/text_prompts_batch.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/text_swimming_pools.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/examples/tree_mapping.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/faq.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/fast_sam.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/hq_sam.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/index.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/installation.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/overrides/main.html +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/samgeo.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/samgeo2.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/samgeo3.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/text_sam.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/usage.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/workshops/AIforGood_2025.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/workshops/IPPN_2024.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/workshops/cn_workshop.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/workshops/jupytext.toml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/docs/workshops/purdue.ipynb +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/mkdocs.yml +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/paper/10.21105.joss.05663.pdf +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/paper/paper.bib +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/paper/paper.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/LICENSE +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/README.md +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/__init__.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/icons/icon.png +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/install_plugin.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/install_plugin.sh +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/map_tools.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/metadata.txt +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/resources.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/samgeo_plugin.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/qgis-samgeo-plugin/test_plugin.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/requirements.txt +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/requirements_dev.txt +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/requirements_docs.txt +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/caption.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/common.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/detectree2.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/fast_sam.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/fer.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/hq_sam.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/samgeo.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/samgeo2.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/text_sam.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/samgeo/utmconv.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/segment_geospatial.egg-info/SOURCES.txt +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/segment_geospatial.egg-info/dependency_links.txt +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/segment_geospatial.egg-info/entry_points.txt +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/segment_geospatial.egg-info/requires.txt +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/segment_geospatial.egg-info/top_level.txt +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/setup.cfg +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/tests/__init__.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/tests/test_api.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/tests/test_common.py +0 -0
- {segment_geospatial-1.3.0 → segment_geospatial-1.3.2}/tests/test_samgeo.py +0 -0
|
@@ -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
|
|
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 | `
|
|
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=
|
|
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.
|
|
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.
|
|
120
|
+
current_version = "1.3.2"
|
|
121
121
|
commit = true
|
|
122
122
|
tag = true
|
|
123
123
|
|
|
@@ -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
|
|
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("
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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.
|
|
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)
|