image-to-stl / README.md
pkoooo
Bump Gradio to 4.44.1 for API schema fix
f4c4ee7

A newer version of the Gradio SDK is available: 6.1.0

Upgrade
metadata
title: Image to STL Converter API
emoji: πŸ‘
colorFrom: indigo
colorTo: yellow
sdk: gradio
sdk_version: 4.44.1
app_file: app.py
pinned: false
license: apache-2.0
short_description: Convert 2D images into 3D STL heightmaps via REST API.

Image to STL Converter API

This Space exposes an API-only backend for converting 2D images into 3D binary STL heightmaps using the depth-anything/Depth-Anything-V2-Small-hf model. It is designed to be called from external applications such as a Next.js frontend.

Under the hood, the pipeline is:

Image β†’ Depth Map β†’ 3D Mesh β†’ STL file

The Gradio interface is primarily used to expose a REST API endpoint. A minimal web UI is still available for quick manual testing.


Model

  • Model: depth-anything/Depth-Anything-V2-Small-hf
  • Task: depth-estimation
  • Framework: transformers, torch

The model is loaded globally when the Space starts, so repeated requests do not reload the model.


API Endpoint

URL

  • Method: POST
  • Path: /api/convert
  • Content-Type: multipart/form-data

On Hugging Face Spaces, the full URL will look like:

curl -X POST https://YOUR-SPACE.hf.space/api/convert \
  -F "[email protected]" \
  -F "height_scale=20" \
  -F "base_thickness=2" \
  -o output.stl

Replace YOUR-SPACE with the actual Space subdomain (for example, lobster2154-image-to-stl).

Request Parameters

All parameters are sent as multipart/form-data fields.

  • image (required)

    • Type: file
    • Description: Input image to be converted to a 3D relief.
  • height_scale (optional)

    • Type: number (float)
    • Default: 20
    • Range: 5 – 50
    • Description: Scales the normalized depth values to control the maximum height of the relief.
  • base_thickness (optional)

    • Type: number (float)
    • Default: 2
    • Range: 1 – 10
    • Description: Thickness of the solid base added under the relief.

Response

  • Status 200 (success)

    • Body: Binary STL file representing a watertight 3D mesh.
    • Headers:
      • Content-Type: application/sla (or model/stl, depending on proxy)
      • Content-Disposition: attachment; filename="output.stl"
  • Status 4xx / 5xx (error)

    • Body: JSON error object produced by Gradio, for example:

      {"error": "Conversion failed: <detailed message>"}
      

Typical error causes include:

  • Missing image file
  • Non-numeric or non-positive height_scale / base_thickness
  • Model initialization issues

Processing Details

  1. Depth estimation

    • The input image is passed into the depth-estimation pipeline.
    • The pipeline returns predicted_depth, which is converted to a NumPy array.
  2. Normalization

    • Depth values are normalized to [0, 1] using:

      normalized = (depth - depth.min()) / (depth.max() - depth.min())

  3. Downsampling

    • For performance, the depth map is downsampled to a maximum of 150 Γ— 150 pixels using LANCZOS interpolation if the original is larger.
  4. Mesh generation

    • A top surface is created from the heightmap.
    • A flat bottom surface is added (z = 0).
    • Four side walls are generated (left, right, front, back) to produce a watertight mesh.
  5. STL export

    • The mesh is converted to a numpy-stl mesh and saved as a binary STL file, which is then streamed back to the client as a file download.

Processing is designed to complete within ~30 seconds for typical image sizes thanks to the downsampling step.


Example Usage (cURL)

curl -X POST https://YOUR-SPACE.hf.space/api/convert \
  -F "[email protected]" \
  -F "height_scale=20" \
  -F "base_thickness=2" \
  -o output.stl
  • [email protected]: The image to convert.
  • height_scale: Optional; controls relief height.
  • base_thickness: Optional; controls the base thickness.
  • output.stl: The STL file to be written locally.

Next.js Integration Example

Below is an example of calling this Space from a Next.js app using fetch.

Client-side call (e.g. inside a React component)

// Example client-side call from a React component
async function uploadImageAndDownloadStl(imageFile: File) {
  const formData = new FormData();
  formData.append("image", imageFile);
  formData.append("height_scale", "20");
  formData.append("base_thickness", "2");

  const response = await fetch("https://YOUR-SPACE.hf.space/api/convert", {
    method: "POST",
    body: formData,
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Conversion failed: ${errorText}`);
  }

  const stlBlob = await response.blob();
  const url = URL.createObjectURL(stlBlob);

  // Trigger download in the browser
  const a = document.createElement("a");
  a.href = url;
  a.download = "output.stl";
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}

Example Next.js API Route (App Router)

// app/api/convert-image/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const formData = await req.formData();
  const image = formData.get("image") as File | null;

  if (!image) {
    return NextResponse.json({ error: "Image file is required" }, { status: 400 });
  }

  const relayFormData = new FormData();
  relayFormData.append("image", image);
  relayFormData.append("height_scale", (formData.get("height_scale") ?? "20") as string);
  relayFormData.append(
    "base_thickness",
    (formData.get("base_thickness") ?? "2") as string,
  );

  const hfResponse = await fetch("https://YOUR-SPACE.hf.space/api/convert", {
    method: "POST",
    body: relayFormData,
  });

  if (!hfResponse.ok) {
    const errorText = await hfResponse.text();
    return NextResponse.json(
      { error: "Conversion failed", details: errorText },
      { status: hfResponse.status },
    );
  }

  const stlArrayBuffer = await hfResponse.arrayBuffer();
  return new NextResponse(stlArrayBuffer, {
    status: 200,
    headers: {
      "Content-Type": "application/sla",
      "Content-Disposition": "attachment; filename=output.stl",
    },
  });
}

In your frontend, you can then call /api/convert-image from React components without exposing the Hugging Face Space URL directly.


Error Handling

The backend performs several validations and provides descriptive error messages:

  • Missing image β†’ 400-like error with message "No image was provided. Please upload an image file."
  • Invalid parameters β†’ error if height_scale or base_thickness are non-numeric or non-positive.
  • Model not loaded β†’ error if the depth model failed to initialize.
  • Internal errors β†’ any unexpected exception is logged and returned as a generic "Conversion failed: ..." message.

These errors are surfaced via Gradio's API as JSON responses with non-200 HTTP status codes, making them easy to handle from your Next.js frontend.