Skip to content

REST API

segment-geospatial includes a built-in REST API powered by FastAPI that allows you to run image segmentation over HTTP. This is useful for integrating segmentation into web applications, pipelines, and non-Python clients.

Installation

Install the API dependencies with the api extra:

1
pip install "segment-geospatial[api]"

To also install a specific SAM model backend, combine extras:

1
pip install "segment-geospatial[api,samgeo3]"

Starting the Server

Use the samgeo-api command:

1
samgeo-api

Options:

1
2
3
samgeo-api --host 0.0.0.0 --port 8000        # Custom host/port
samgeo-api --preload sam2:sam2-hiera-large     # Preload a model at startup
samgeo-api --reload                            # Auto-reload for development

Alternatively, use uvicorn directly:

1
uvicorn samgeo.api:app --host 0.0.0.0 --port 8000

Once running, interactive API docs (Swagger UI) are available at http://localhost:8000/docs.

Endpoints

Health Check

1
GET /health

Returns the server status and version.

1
curl http://localhost:8000/health
1
{"status": "ok", "version": "1.2.3"}

List Models

1
GET /models

Returns available model versions/IDs and which models are currently loaded in memory.

1
curl http://localhost:8000/models

Clear Models

1
DELETE /models

Clears the model cache and frees GPU memory.

1
curl -X DELETE http://localhost:8000/models

Automatic Segmentation

1
POST /segment/automatic

Runs automatic mask generation on an uploaded image. Supports SAM, SAM2, and SAM3.

Parameters (multipart form):

Parameter Type Default Description
file file required Image file (TIFF, PNG, JPEG)
model_version string sam2 One of sam, sam2, sam3
model_id string auto Model identifier (e.g., sam2-hiera-large)
output_format string geojson One of geojson, geotiff, png, json, detections
foreground bool true Extract foreground objects only
unique bool true Assign unique ID to each object
min_size int 0 Minimum mask size in pixels
max_size int none Maximum mask size in pixels
points_per_side int 32 Points sampled per side (SAM/SAM2)
pred_iou_thresh float 0.8 IoU threshold for filtering
stability_score_thresh float 0.95 Stability score threshold

Example:

1
2
3
4
curl -X POST http://localhost:8000/segment/automatic \
  -F "file=@image.tif" \
  -F "model_version=sam2" \
  -F "output_format=geojson"

Prompt-based Segmentation

1
POST /segment/predict

Runs segmentation with point or bounding box prompts. Supports SAM, SAM2, and SAM3.

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.

Parameters (multipart form):

Parameter Type Default Description
file file required Image file (TIFF, PNG, JPEG)
model_version string sam3 One of sam, sam2, sam3
model_id string auto Model identifier
output_format string geojson One of geojson, geotiff, png, json, detections
point_coords string none JSON array of [[x, y], ...]
point_labels string none JSON array of [1, 0, ...] (1=foreground, 0=background)
boxes string none JSON array of [[xmin, ymin, xmax, ymax], ...]
point_crs string none CRS string (e.g., EPSG:4326) for point/box coordinates
multimask_output bool false Return multiple masks per prompt
min_size int 0 Minimum mask size in pixels
max_size int none Maximum mask size in pixels

Example with point prompts:

1
2
3
4
5
curl -X POST http://localhost:8000/segment/predict \
  -F "file=@image.tif" \
  -F "point_coords=[[100, 200]]" \
  -F "point_labels=[1]" \
  -F "output_format=geojson"

Example with box prompts (finds all similar objects):

1
2
3
4
curl -X POST http://localhost:8000/segment/predict \
  -F "file=@image.tif" \
  -F "boxes=[[10, 20, 300, 400]]" \
  -F "output_format=geojson"

Example with JSON output (pixel-coordinate bounding boxes):

1
2
3
4
curl -X POST http://localhost:8000/segment/predict \
  -F "file=@image.jpg" \
  -F "boxes=[[10, 20, 300, 400]]" \
  -F "output_format=json"
1
2
3
4
5
6
7
8
9
{
  "image_width": 2647,
  "image_height": 1464,
  "num_detections": 12,
  "detections": [
    {"id": 1, "value": 1, "bbox": [50, 80, 200, 250], "width": 150, "height": 170},
    {"id": 2, "value": 2, "bbox": [310, 45, 480, 210], "width": 170, "height": 165}
  ]
}

Example with detections output (geographic-coordinate bounding boxes):

1
2
3
4
curl -X POST http://localhost:8000/segment/predict \
  -F "file=@image.tif" \
  -F "boxes=[[10, 20, 300, 400]]" \
  -F "output_format=detections"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "type": "FeatureCollection",
  "crs": "EPSG:3857",
  "num_detections": 12,
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Polygon",
        "coordinates": [[[-13609328.39, 4561446.23], [-13609284.55, 4561446.23], [-13609284.55, 4561389.77], [-13609328.39, 4561389.77], [-13609328.39, 4561446.23]]]
      },
      "properties": {"id": 1, "value": 1, "bbox_pixel": [50.0, 80.0, 200.0, 250.0]}
    }
  ]
}

Text-prompt Segmentation

1
POST /segment/text

Runs text-prompt segmentation using SAM3.

Parameters (multipart form):

Parameter Type Default Description
file file required Image file (TIFF, PNG, JPEG)
prompt string required Text description (e.g., building, tree)
model_id string auto SAM3 model identifier
backend string meta One of meta, transformers
output_format string geojson One of geojson, geotiff, png, json, detections
confidence_threshold float 0.5 Detection confidence threshold
min_size int 0 Minimum mask size in pixels
max_size int none Maximum mask size in pixels

Example (GeoJSON mask polygons):

1
2
3
4
curl -X POST http://localhost:8000/segment/text \
  -F "file=@image.tif" \
  -F "prompt=building" \
  -F "output_format=geojson"

Example with JSON output (pixel-coordinate bounding boxes):

1
2
3
4
curl -X POST http://localhost:8000/segment/text \
  -F "file=@image.jpg" \
  -F "prompt=building" \
  -F "output_format=json"
1
2
3
4
5
6
7
8
9
{
  "image_width": 2647,
  "image_height": 1464,
  "num_detections": 46,
  "detections": [
    {"id": 1, "bbox": [2506, 134, 2653, 324], "width": 147, "height": 190, "score": 0.887},
    {"id": 2, "bbox": [1200, 450, 1380, 620], "width": 180, "height": 170, "score": 0.862}
  ]
}

Example with detections output (geographic-coordinate bounding boxes):

1
2
3
4
curl -X POST http://localhost:8000/segment/text \
  -F "file=@image.tif" \
  -F "prompt=building" \
  -F "output_format=detections"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "type": "FeatureCollection",
  "crs": "EPSG:3857",
  "num_detections": 46,
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Polygon",
        "coordinates": [[[-13609328.39, 4561446.23], [-13609284.55, 4561446.23], [-13609284.55, 4561389.77], [-13609328.39, 4561389.77], [-13609328.39, 4561446.23]]]
      },
      "properties": {"id": 1, "score": 0.887, "bbox_pixel": [2506.47, 134.43, 2653.27, 323.52]}
    }
  ]
}

Caching

The API automatically caches models and image encodings for better performance:

  • Model cache: Models are loaded once and reused across requests. Use DELETE /models to free GPU memory.
  • Image cache: When the same image is sent multiple times (e.g., with different prompts), the expensive image encoding step is skipped. This makes subsequent requests significantly faster.

Example timing with a 13 MB GeoTIFF:

Request Description Time
1st Model load + image encoding ~7s
2nd Same image, different prompt ~0.4s
3rd Same image, another prompt ~0.2s

Python Client Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import requests

url = "http://localhost:8000/segment/text"

# Get GeoJSON mask polygons
with open("image.tif", "rb") as f:
    response = requests.post(
        url,
        files={"file": ("image.tif", f, "image/tiff")},
        data={"prompt": "building", "output_format": "geojson"},
    )

geojson = response.json()
print(f"Found {len(geojson['features'])} features")
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Get bounding boxes in pixel coordinates (suitable for non-georeferenced images)
with open("image.jpg", "rb") as f:
    response = requests.post(
        url,
        files={"file": ("image.jpg", f, "image/jpeg")},
        data={"prompt": "car", "output_format": "json"},
    )

result = response.json()
for det in result["detections"]:
    print(f"Object {det['id']}: bbox={det['bbox']}, score={det['score']:.3f}")

API Reference

REST API for segment-geospatial.

Provides FastAPI endpoints for image segmentation using SAM, SAM2, and SAM3 models. Install with: pip install segment-geospatial[api]

Usage

samgeo-api # Start on default port 8000 samgeo-api --port 9000 # Custom port samgeo-api --preload sam2:sam2-hiera-large # Preload a model uvicorn samgeo.api:app # Direct uvicorn usage

clear_models()

Clear the model cache and free GPU memory.

Source code in samgeo/api.py
530
531
532
533
534
535
536
537
538
539
540
541
542
@app.delete("/models")
def clear_models():
    """Clear the model cache and free GPU memory."""
    _model_cache.clear()
    _image_hash_cache.clear()
    try:
        import torch

        if torch.cuda.is_available():
            torch.cuda.empty_cache()
    except ImportError:
        pass
    return {"status": "cleared"}

get_model(model_version, model_id=None, **kwargs)

Get or create a cached model instance.

Parameters:

Name Type Description Default
model_version str

One of "sam", "sam2", "sam3".

required
model_id Optional[str]

Specific model identifier. Uses default if None.

None
**kwargs

Additional keyword arguments for model initialization.

{}

Returns:

Name Type Description
tuple

(model_instance, threading.Lock)

Raises:

Type Description
HTTPException

If model_version or model_id is invalid, or dependencies are missing.

Source code in samgeo/api.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def get_model(model_version: str, model_id: Optional[str] = None, **kwargs):
    """Get or create a cached model instance.

    Args:
        model_version: One of "sam", "sam2", "sam3".
        model_id: Specific model identifier. Uses default if None.
        **kwargs: Additional keyword arguments for model initialization.

    Returns:
        tuple: (model_instance, threading.Lock)

    Raises:
        HTTPException: If model_version or model_id is invalid, or
            dependencies are missing.
    """
    if model_version not in _DEFAULT_MODEL_IDS:
        raise HTTPException(
            status_code=400,
            detail=(
                f"Invalid model_version '{model_version}'. "
                f"Must be one of: {list(_DEFAULT_MODEL_IDS.keys())}"
            ),
        )

    if not model_id:
        model_id = _DEFAULT_MODEL_IDS[model_version]

    valid_ids = _AVAILABLE_MODELS[model_version]
    if model_id not in valid_ids:
        raise HTTPException(
            status_code=400,
            detail=(
                f"Invalid model_id '{model_id}' for {model_version}. "
                f"Must be one of: {valid_ids}"
            ),
        )

    key = (model_version, model_id)
    with _model_cache_lock:
        if key in _model_cache:
            logger.info("Model cache hit for %s", key)
            return _model_cache[key]

        logger.info("Loading model %s", key)
        extra = _EXTRAS_MAP.get(model_version, model_version)
        try:
            if model_version == "sam":
                from samgeo.samgeo import SamGeo

                model = SamGeo(model_type=model_id, **kwargs)
            elif model_version == "sam2":
                from samgeo.samgeo2 import SamGeo2

                model = SamGeo2(model_id=model_id, **kwargs)
            elif model_version == "sam3":
                from samgeo.samgeo3 import SamGeo3

                kwargs.setdefault("enable_inst_interactivity", True)
                model = SamGeo3(**kwargs)
        except ImportError as e:
            raise HTTPException(
                status_code=503,
                detail=(
                    f"Dependencies for {model_version} are not installed. "
                    f"Install with: pip install segment-geospatial[{extra}]. "
                    f"Error: {e}"
                ),
            )
        _model_cache[key] = (model, threading.Lock())
        return _model_cache[key]

health()

Health check endpoint.

Source code in samgeo/api.py
517
518
519
520
@app.get("/health")
def health():
    """Health check endpoint."""
    return {"status": "ok", "version": __version__}

list_models()

List available and currently loaded models.

Source code in samgeo/api.py
523
524
525
526
527
@app.get("/models")
def list_models():
    """List available and currently loaded models."""
    loaded = [list(key) for key in _model_cache]
    return {"models": _AVAILABLE_MODELS, "loaded": loaded}

main()

Entry point for the samgeo-api console script.

Source code in samgeo/api.py
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
def main():
    """Entry point for the samgeo-api console script."""
    parser = argparse.ArgumentParser(
        description="Run the samgeo REST API server."
    )
    parser.add_argument(
        "--host", default="0.0.0.0", help="Host to bind to (default: 0.0.0.0)"
    )
    parser.add_argument(
        "--port", type=int, default=8000, help="Port to listen on (default: 8000)"
    )
    parser.add_argument(
        "--reload", action="store_true", help="Enable auto-reload for development"
    )
    parser.add_argument(
        "--preload",
        type=str,
        default=None,
        help="Preload a model at startup, e.g. 'sam2:sam2-hiera-large'",
    )
    args = parser.parse_args()

    if args.preload:
        if ":" not in args.preload:
            parser.error(
                "Invalid --preload format. "
                "Expected 'model_version:model_id', e.g. 'sam2:sam2-hiera-large'"
            )
        version, mid = args.preload.split(":", 1)
        get_model(version, mid)

    uvicorn.run("samgeo.api:app", host=args.host, port=args.port, reload=args.reload)

segment_automatic(file=File(...), model_version=Form('sam2'), model_id=Form(None), output_format=Form('geojson'), foreground=Form(True), unique=Form(True), min_size=Form(0), max_size=Form(None), points_per_side=Form(32), pred_iou_thresh=Form(0.8), stability_score_thresh=Form(0.95)) async

Run automatic mask generation on an uploaded image.

Parameters:

Name Type Description Default
file UploadFile

Image file (TIFF, PNG, JPEG).

File(...)
model_version str

One of "sam", "sam2", "sam3".

Form('sam2')
model_id Optional[str]

Specific model identifier.

Form(None)
output_format str

One of "geojson", "geotiff", "png".

Form('geojson')
foreground bool

Whether to extract foreground objects only.

Form(True)
unique bool

Whether to assign unique IDs to each object.

Form(True)
min_size int

Minimum mask size in pixels.

Form(0)
max_size Optional[int]

Maximum mask size in pixels.

Form(None)
points_per_side int

Number of points sampled per side (SAM/SAM2).

Form(32)
pred_iou_thresh float

IoU threshold for filtering masks.

Form(0.8)
stability_score_thresh float

Stability score threshold for filtering.

Form(0.95)

Returns:

Type Description

Segmentation result in the requested format.

Source code in samgeo/api.py
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
@app.post("/segment/automatic")
async def segment_automatic(
    file: UploadFile = File(...),
    model_version: str = Form("sam2"),
    model_id: Optional[str] = Form(None),
    output_format: str = Form("geojson"),
    foreground: bool = Form(True),
    unique: bool = Form(True),
    min_size: int = Form(0),
    max_size: Optional[int] = Form(None),
    points_per_side: int = Form(32),
    pred_iou_thresh: float = Form(0.8),
    stability_score_thresh: float = Form(0.95),
):
    """Run automatic mask generation on an uploaded image.

    Args:
        file: Image file (TIFF, PNG, JPEG).
        model_version: One of "sam", "sam2", "sam3".
        model_id: Specific model identifier.
        output_format: One of "geojson", "geotiff", "png".
        foreground: Whether to extract foreground objects only.
        unique: Whether to assign unique IDs to each object.
        min_size: Minimum mask size in pixels.
        max_size: Maximum mask size in pixels.
        points_per_side: Number of points sampled per side (SAM/SAM2).
        pred_iou_thresh: IoU threshold for filtering masks.
        stability_score_thresh: Stability score threshold for filtering.

    Returns:
        Segmentation result in the requested format.
    """
    _validate_output_format(output_format)
    max_size = _normalize_max_size(max_size)
    tmpdir = tempfile.mkdtemp()
    try:
        input_path, image_hash = await _save_upload(file, tmpdir)
        output_path = os.path.join(tmpdir, "mask.tif")

        t_start = time.time()
        if model_version == "sam3":
            model, lock = get_model(model_version, model_id)
            model_key = (model_version, model_id or _DEFAULT_MODEL_IDS[model_version])
            with lock:
                _set_image_cached(model, model_key, input_path, image_hash)
                model.generate_masks(
                    prompt="everything",
                    min_size=min_size,
                    max_size=max_size,
                )
                model.save_masks(output=output_path, unique=unique)
        else:
            sam_kwargs = {
                "points_per_side": points_per_side,
                "pred_iou_thresh": pred_iou_thresh,
                "stability_score_thresh": stability_score_thresh,
            }
            if model_version == "sam":
                model, lock = get_model(
                    model_version, model_id, sam_kwargs=sam_kwargs
                )
            else:
                model, lock = get_model(model_version, model_id, **sam_kwargs)

            with lock:
                model.generate(
                    source=input_path,
                    output=output_path,
                    foreground=foreground,
                    unique=unique,
                    min_size=min_size,
                    max_size=max_size,
                )

        t_inference = time.time() - t_start
        logger.info(
            "Automatic segmentation completed in %.2fs (model: %s)",
            t_inference,
            model_version,
        )
        return _format_response(output_path, output_format, tmpdir)
    except HTTPException:
        _cleanup_tmpdir(tmpdir)
        raise
    except Exception as e:
        _cleanup_tmpdir(tmpdir)
        raise HTTPException(status_code=500, detail=str(e))

segment_predict(file=File(...), model_version=Form('sam3'), model_id=Form(None), output_format=Form('geojson'), point_coords=Form(None), point_labels=Form(None), boxes=Form(None), point_crs=Form(None), multimask_output=Form(False), min_size=Form(0), max_size=Form(None)) async

Run prompt-based segmentation with points or bounding boxes.

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.

Parameters:

Name Type Description Default
file UploadFile

Image file (TIFF, PNG, JPEG).

File(...)
model_version str

One of "sam", "sam2", "sam3".

Form('sam3')
model_id Optional[str]

Specific model identifier.

Form(None)
output_format str

One of "geojson", "geotiff", "png", "json", "detections".

Form('geojson')
point_coords Optional[str]

JSON string of [[x, y], ...] coordinate pairs.

Form(None)
point_labels Optional[str]

JSON string of [1, 0, ...] labels (1=foreground, 0=background).

Form(None)
boxes Optional[str]

JSON string of [[xmin, ymin, xmax, ymax], ...] bounding boxes.

Form(None)
point_crs Optional[str]

CRS string (e.g., "EPSG:4326") for point/box coordinates.

Form(None)
multimask_output bool

Whether to return multiple masks per prompt.

Form(False)
min_size int

Minimum mask size in pixels.

Form(0)
max_size Optional[int]

Maximum mask size in pixels.

Form(None)

Returns:

Type Description

Segmentation result in the requested format.

Source code in samgeo/api.py
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
@app.post("/segment/predict")
async def segment_predict(
    file: UploadFile = File(...),
    model_version: str = Form("sam3"),
    model_id: Optional[str] = Form(None),
    output_format: str = Form("geojson"),
    point_coords: Optional[str] = Form(None),
    point_labels: Optional[str] = Form(None),
    boxes: Optional[str] = Form(None),
    point_crs: Optional[str] = Form(None),
    multimask_output: bool = Form(False),
    min_size: int = Form(0),
    max_size: Optional[int] = Form(None),
):
    """Run prompt-based segmentation with points or bounding boxes.

    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.

    Args:
        file: Image file (TIFF, PNG, JPEG).
        model_version: One of "sam", "sam2", "sam3".
        model_id: Specific model identifier.
        output_format: One of "geojson", "geotiff", "png", "json", "detections".
        point_coords: JSON string of [[x, y], ...] coordinate pairs.
        point_labels: JSON string of [1, 0, ...] labels (1=foreground,
            0=background).
        boxes: JSON string of [[xmin, ymin, xmax, ymax], ...] bounding boxes.
        point_crs: CRS string (e.g., "EPSG:4326") for point/box coordinates.
        multimask_output: Whether to return multiple masks per prompt.
        min_size: Minimum mask size in pixels.
        max_size: Maximum mask size in pixels.

    Returns:
        Segmentation result in the requested format.
    """
    _validate_output_format(output_format)

    # Swagger UI sends empty strings for unfilled optional fields
    if not point_coords:
        point_coords = None
    if not point_labels:
        point_labels = None
    if not boxes:
        boxes = None
    if not point_crs:
        point_crs = None

    if point_coords is None and boxes is None:
        raise HTTPException(
            status_code=400,
            detail="At least one of point_coords or boxes must be provided.",
        )

    max_size = _normalize_max_size(max_size)
    tmpdir = tempfile.mkdtemp()
    try:
        input_path, image_hash = await _save_upload(file, tmpdir)
        output_path = os.path.join(tmpdir, "mask.tif")

        # Parse JSON prompt fields
        parsed_coords = None
        parsed_labels = None
        parsed_boxes = None

        if point_coords is not None:
            parsed_coords = np.array(json.loads(point_coords))
        if point_labels is not None:
            parsed_labels = np.array(json.loads(point_labels))
        if boxes is not None:
            parsed_boxes = np.array(json.loads(boxes))

        t_start = time.time()

        if model_version == "sam3":
            model, lock = get_model(model_version, model_id)
            model_key = (
                model_version,
                model_id or _DEFAULT_MODEL_IDS[model_version],
            )
            with lock:
                _set_image_cached(model, model_key, input_path, image_hash)
                if parsed_boxes is not None:
                    # Use generate_masks_by_boxes to find all similar objects
                    box_list = parsed_boxes.tolist()
                    if parsed_boxes.ndim == 1:
                        box_list = [box_list]
                    model.generate_masks_by_boxes(
                        boxes=box_list,
                        box_crs=point_crs,
                        min_size=min_size,
                        max_size=max_size,
                    )
                else:
                    # Use predict_inst for point-only prompts
                    model.predict_inst(
                        point_coords=parsed_coords,
                        point_labels=parsed_labels,
                        multimask_output=multimask_output,
                        point_crs=point_crs,
                    )
                if model.masks is None or len(model.masks) == 0:
                    _cleanup_tmpdir(tmpdir)
                    raise HTTPException(
                        status_code=404,
                        detail="No objects found for the given prompts.",
                    )
                model.save_masks(
                    output=output_path,
                    min_size=min_size,
                    max_size=max_size,
                )
        else:
            model, lock = get_model(model_version, model_id, automatic=False)
            model_key = (
                model_version,
                model_id or _DEFAULT_MODEL_IDS[model_version],
            )
            with lock:
                _set_image_cached(model, model_key, input_path, image_hash)
                model.predict(
                    point_coords=parsed_coords,
                    point_labels=parsed_labels,
                    boxes=parsed_boxes,
                    point_crs=point_crs,
                    multimask_output=multimask_output,
                    output=output_path,
                )

        t_inference = time.time() - t_start
        logger.info(
            "Prompt segmentation completed in %.2fs (model: %s)",
            t_inference,
            model_version,
        )
        return _format_response(output_path, output_format, tmpdir)
    except HTTPException:
        _cleanup_tmpdir(tmpdir)
        raise
    except json.JSONDecodeError as e:
        _cleanup_tmpdir(tmpdir)
        raise HTTPException(
            status_code=400, detail=f"Invalid JSON in prompt fields: {e}"
        )
    except Exception as e:
        _cleanup_tmpdir(tmpdir)
        raise HTTPException(status_code=500, detail=str(e))

segment_text(file=File(...), prompt=Form(...), model_id=Form(None), backend=Form('meta'), output_format=Form('geojson'), confidence_threshold=Form(0.5), min_size=Form(0), max_size=Form(None)) async

Run text-prompt segmentation using SAM3.

Parameters:

Name Type Description Default
file UploadFile

Image file (TIFF, PNG, JPEG).

File(...)
prompt str

Text description of objects to segment (e.g., "building").

Form(...)
model_id Optional[str]

SAM3 model identifier.

Form(None)
backend str

SAM3 backend, one of "meta" or "transformers".

Form('meta')
output_format str

One of "geojson", "geotiff", "png", "detections", "json". Use "detections" to get a GeoJSON FeatureCollection of bounding box polygons in geographic coordinates with confidence scores. Use "json" for a plain JSON array of bounding boxes in pixel coordinates, suitable for non-georeferenced images.

Form('geojson')
confidence_threshold float

Confidence threshold for detections.

Form(0.5)
min_size int

Minimum mask size in pixels.

Form(0)
max_size Optional[int]

Maximum mask size in pixels.

Form(None)

Returns:

Type Description

Segmentation result in the requested format.

Source code in samgeo/api.py
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
@app.post("/segment/text")
async def segment_text(
    file: UploadFile = File(...),
    prompt: str = Form(...),
    model_id: Optional[str] = Form(None),
    backend: str = Form("meta"),
    output_format: str = Form("geojson"),
    confidence_threshold: float = Form(0.5),
    min_size: int = Form(0),
    max_size: Optional[int] = Form(None),
):
    """Run text-prompt segmentation using SAM3.

    Args:
        file: Image file (TIFF, PNG, JPEG).
        prompt: Text description of objects to segment (e.g., "building").
        model_id: SAM3 model identifier.
        backend: SAM3 backend, one of "meta" or "transformers".
        output_format: One of "geojson", "geotiff", "png", "detections", "json".
            Use "detections" to get a GeoJSON FeatureCollection of bounding
            box polygons in geographic coordinates with confidence scores.
            Use "json" for a plain JSON array of bounding boxes in pixel
            coordinates, suitable for non-georeferenced images.
        confidence_threshold: Confidence threshold for detections.
        min_size: Minimum mask size in pixels.
        max_size: Maximum mask size in pixels.

    Returns:
        Segmentation result in the requested format.
    """
    _validate_output_format(output_format)
    max_size = _normalize_max_size(max_size)
    tmpdir = tempfile.mkdtemp()
    try:
        input_path, image_hash = await _save_upload(file, tmpdir)
        output_path = os.path.join(tmpdir, "mask.tif")

        model, lock = get_model(
            "sam3",
            model_id,
            backend=backend,
            confidence_threshold=confidence_threshold,
        )
        t_start = time.time()
        model_key = ("sam3", model_id or _DEFAULT_MODEL_IDS["sam3"])
        with lock:
            _set_image_cached(model, model_key, input_path, image_hash)
            model.generate_masks(
                prompt=prompt,
                min_size=min_size,
                max_size=max_size,
            )
            if model.masks is None or len(model.masks) == 0:
                _cleanup_tmpdir(tmpdir)
                raise HTTPException(
                    status_code=404,
                    detail=(
                        "No objects found for the given prompt. "
                        "Please try a different prompt or adjust parameters."
                    ),
                )
            if output_format in ("detections", "json"):
                if output_format == "detections":
                    det_result = _build_detections_geojson(model, input_path)
                else:
                    det_result = _build_detections_json(model)
            else:
                model.save_masks(output=output_path)

        t_inference = time.time() - t_start
        logger.info(
            "Text segmentation completed in %.2fs (prompt: '%s')",
            t_inference,
            prompt,
        )
        if output_format in ("detections", "json"):
            _cleanup_tmpdir(tmpdir)
            return JSONResponse(content=det_result)
        return _format_response(output_path, output_format, tmpdir)
    except HTTPException:
        _cleanup_tmpdir(tmpdir)
        raise
    except Exception as e:
        _cleanup_tmpdir(tmpdir)
        raise HTTPException(status_code=500, detail=str(e))