HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //opt/depthanything/depth_server.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DepthAnything V2 – einfacher HTTP-Dienst (FastAPI) für Depthmap-Inferenz.

- Endpunkt: POST /depth
  Request (JSON):
    {
      "image_url": "https://…/bild.jpg",   # optional
      "image_base64": "<BASE64>",          # optional, wird priorisiert
      "encoder": "vitb" | "vits" | "vitl", # optional, default: "vitb"
      "grayscale": true | false,           # optional, default: true
      "input_size": 518                    # optionale Kantenlänge (Skalierung vor Inferenz)
    }
  Response:
    - PNG Binary (image/png) bei Erfolg
    - JSON {"error": "..."} bei Fehler

Voraussetzungen:
- Repo: https://github.com/DepthAnything/Depth-Anything-V2
- Checkpoints liegen in DEPTH_REPO_DIR/checkpoints/
- venv aktiv, uvicorn installiert

Start (Entwicklung):
  uvicorn depth_server:app --host 0.0.0.0 --port 5005

Systemd siehe bereitgestellte Unit.
"""

import io
import os
import sys
import base64
import json
import cv2
import numpy as np
import torch
import requests
from typing import Optional
from PIL import Image
from fastapi import FastAPI, Response
from pydantic import BaseModel

# Pfad zu Depth-Anything-V2 einbinden
DEPTH_REPO_DIR = "/opt/depthanything/Depth-Anything-V2"
sys.path.append(DEPTH_REPO_DIR)

from depth_anything_v2.dpt import DepthAnythingV2  # noqa: E402

app = FastAPI()

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"

MODEL_CFG = {
    "vits": {"encoder": "vits", "features": 64,  "out_channels": [48, 96, 192, 384]},
    "vitb": {"encoder": "vitb", "features": 128, "out_channels": [96, 192, 384, 768]},
    "vitl": {"encoder": "vitl", "features": 256, "out_channels": [256, 512, 1024, 1024]},
}

# Passe ggf. die Checkpoint-Pfade an deine Downloads an
CHECKPOINTS = {
    "vits": os.path.join(DEPTH_REPO_DIR, "checkpoints", "depth_anything_v2_vits.pth"),
    "vitb": os.path.join(DEPTH_REPO_DIR, "checkpoints", "depth_anything_v2_vitb.pth"),
    "vitl": os.path.join(DEPTH_REPO_DIR, "checkpoints", "depth_anything_v2_vitl.pth"),
}

MODELS = {}  # Cache geladener Modelle


def get_model(encoder: str) -> DepthAnythingV2:
    enc = encoder if encoder in MODEL_CFG else "vitb"
    if enc not in MODELS:
        m = DepthAnythingV2(**MODEL_CFG[enc])
        m.load_state_dict(torch.load(CHECKPOINTS[enc], map_location="cpu"))
        MODELS[enc] = m.to(DEVICE).eval()
    return MODELS[enc]


class DepthReq(BaseModel):
    image_url: Optional[str] = None
    image_base64: Optional[str] = None  # VR kann Base64 mitsenden
    encoder: str = "vitb"
    grayscale: bool = True
    input_size: int = 518               # Kantenlänge zum Runterskalieren vor Inferenz


@app.get("/health")
def health():
    return {"status": "ok", "device": DEVICE}


@app.post("/depth")
def depth(req: DepthReq):
    # 1) Bild laden: priorisiere Base64, sonst URL
    try:
        if req.image_base64:
            image_bytes = base64.b64decode(req.image_base64)
            image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
        elif req.image_url:
            r = requests.get(req.image_url, timeout=25)
            r.raise_for_status()
            image = Image.open(io.BytesIO(r.content)).convert("RGB")
        else:
            return Response(
                status_code=400,
                content=json.dumps({"error": "no image provided (image_url or image_base64)"}).encode(),
                media_type="application/json",
            )
    except Exception as e:
        return Response(
            status_code=400,
            content=json.dumps({"error": f"image load failed: {str(e)}"}).encode(),
            media_type="application/json",
        )

    # PIL RGB -> OpenCV BGR
    arr = np.array(image)[:, :, ::-1]  # HxWx3

    # 2) Vor-Inferenz skalieren, um VRAM zu schonen (Seitenverhältnis erhalten)
    try:
        h0, w0 = arr.shape[:2]
        s = req.input_size / max(h0, w0)
        if s < 1.0:
            new_w = max(1, int(w0 * s))
            new_h = max(1, int(h0 * s))
            arr_resized = cv2.resize(arr, (new_w, new_h), interpolation=cv2.INTER_AREA)
        else:
            arr_resized = arr
    except Exception as e:
        return Response(
            status_code=500,
            content=json.dumps({"error": f"resize failed: {str(e)}"}).encode(),
            media_type="application/json",
        )

    # 3) Inferenz
    try:
        model = get_model(req.encoder)
        with torch.no_grad():
            depth_small = model.infer_image(arr_resized)  # HxW float32 numpy
    except torch.cuda.OutOfMemoryError as e:
        return Response(
            status_code=500,
            content=json.dumps({"error": f"cuda oom: {str(e)}"}).encode(),
            media_type="application/json",
        )
    except Exception as e:
        return Response(
            status_code=500,
            content=json.dumps({"error": f"inference failed: {str(e)}"}).encode(),
            media_type="application/json",
        )

    # 4) Wieder auf Originalgröße hochskalieren (optional, bessere Passung)
    try:
        if depth_small.shape[0] != h0 or depth_small.shape[1] != w0:
            depth = cv2.resize(depth_small, (w0, h0), interpolation=cv2.INTER_CUBIC)
        else:
            depth = depth_small
    except Exception as e:
        return Response(
            status_code=500,
            content=json.dumps({"error": f"upsample failed: {str(e)}"}).encode(),
            media_type="application/json",
        )

    # 5) Normalisieren auf [0,255] und ggf. einfärben
    try:
        d = depth.astype(np.float32)
        d -= d.min()
        if d.max() > 0:
            d /= d.max()
        d = (d * 255.0).clip(0, 255).astype(np.uint8)
        if not req.grayscale:
            d = cv2.applyColorMap(d, cv2.COLORMAP_TURBO)
    except Exception as e:
        return Response(
            status_code=500,
            content=json.dumps({"error": f"normalize failed: {str(e)}"}).encode(),
            media_type="application/json",
        )

    # 6) PNG encodieren und binär zurückgeben
    ok, png = cv2.imencode(".png", d)
    if not ok:
        return Response(
            status_code=500,
            content=json.dumps({"error": "png encode failed"}).encode(),
            media_type="application/json",
        )

    return Response(content=png.tobytes(), media_type="image/png")