Spaces:
Sleeping
Sleeping
Download app.py from ajju1020/depthforge: direct link, hf CLI and curl.
- Browser
- Download file 15 kB
-
https://huggingface.co/spaces/ajju1020/depthforge/resolve/main/app.py
- Command line
-
hf download hf://spaces/ajju1020/depthforge/app.py
-
curl -L -o app.py https://huggingface.co/spaces/ajju1020/depthforge/resolve/main/app.py
15 kB
| """DepthForge -- Gradio Space: photo -> CNC relief heightmap (16-bit PNG + STL). | |
| ZeroGPU orchestration (Β§6). ``import spaces`` before torch; a local shim keeps | |
| the app runnable off-Space. Only depth inference runs inside ``@spaces.GPU`` -- | |
| cleanup, detail-injection heightmap, and meshing are CPU work in the plain | |
| callback. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import os | |
| import tempfile | |
| import time | |
| from typing import Optional, Tuple | |
| # --- ZeroGPU: import spaces BEFORE torch; shim so the app runs locally ------- # | |
| try: | |
| import spaces # type: ignore | |
| except ImportError: # local no-op shim | |
| class spaces: # type: ignore | |
| def GPU(*a, **k): | |
| def deco(f): | |
| return f | |
| return deco | |
| import gradio as gr | |
| import numpy as np | |
| from PIL import Image | |
| from core.backends import da3_available, get_backend, safe_infer | |
| from core.cleanup import clean_source | |
| from core.config import ( | |
| BACKEND_DA3_LARGE, | |
| CLEANUP, | |
| MESH, | |
| MODEL_CHOICES, | |
| RELIEF, | |
| ) | |
| from core.heightmap import ReliefParams, build_heightmap | |
| from core.io import write_png16 | |
| from core.mesh import heightmap_to_stl | |
| from core.presets import DEFAULT_PRESET, get_preset, preset_names | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger("depthforge.app") | |
| # Map UI label -> internal backend key. | |
| _MODEL_LABEL_TO_KEY = {label: key for label, key in MODEL_CHOICES} | |
| _MODEL_KEY_TO_LABEL = {key: label for label, key in MODEL_CHOICES} | |
| _MODEL_LABELS = [label for label, _ in MODEL_CHOICES] | |
| _DEFAULT_MODEL_LABEL = _MODEL_LABELS[0] | |
| # --------------------------------------------------------------------------- # | |
| # GPU-decorated inference (ONLY model inference lives here) | |
| # --------------------------------------------------------------------------- # | |
| def _gpu_forward(backend, image: Image.Image) -> np.ndarray: | |
| """GPU-only forward pass (weights already downloaded/constructed on CPU).""" | |
| return backend.infer_loaded(image) | |
| # --------------------------------------------------------------------------- # | |
| # Main callback (CPU orchestration around the single GPU call) | |
| # --------------------------------------------------------------------------- # | |
| def generate( | |
| image: Optional[Image.Image], | |
| model_label: str, | |
| # cleanup | |
| trim_top: float, | |
| trim_bottom: float, | |
| trim_left: float, | |
| trim_right: float, | |
| denoise: bool, | |
| upscale: float, | |
| max_side: float, | |
| # relief | |
| invert: bool, | |
| detail_strength: float, | |
| detail_sigma: float, | |
| fine_strength: float, | |
| contrast_gamma: float, | |
| mask_thresh: float, | |
| bg_flatten: bool, | |
| smooth: float, | |
| fg_expand: bool, | |
| fg_bg_level: float, | |
| fg_local: float, | |
| # mesh | |
| width_mm: float, | |
| relief_mm: float, | |
| base_mm: float, | |
| mesh_step: float, | |
| progress: gr.Progress = gr.Progress(), | |
| ): | |
| """End-to-end: cleanup -> GPU depth -> heightmap -> PNG + STL.""" | |
| if image is None: | |
| raise gr.Error("Please upload a source image first.") | |
| t_all = time.time() | |
| backend_key = _MODEL_LABEL_TO_KEY.get(model_label, BACKEND_DA3_LARGE) | |
| # 1) Cleanup (CPU) | |
| progress(0.05, desc="Cleaning source") | |
| cleaned = clean_source( | |
| image, | |
| trim_top=trim_top, | |
| trim_bottom=trim_bottom, | |
| trim_left=trim_left, | |
| trim_right=trim_right, | |
| denoise=denoise, | |
| upscale=upscale, | |
| max_side=int(max_side), | |
| ) | |
| # 2) Depth inference (GPU only) | |
| progress(0.35, desc="Running depth model") | |
| t_gpu = time.time() | |
| # Weight download/construct happens on CPU inside safe_infer; only the | |
| # forward pass (_gpu_forward) runs under @spaces.GPU. | |
| depth_raw, meta = safe_infer(cleaned, backend_key, gpu_forward=_gpu_forward) | |
| gpu_secs = time.time() - t_gpu | |
| if meta.get("fell_back"): | |
| gr.Warning( | |
| f"{meta['requested']} unavailable β fell back to {meta['name']}." | |
| ) | |
| # 3) Heightmap (CPU, detail injection) | |
| progress(0.65, desc="Building relief heightmap") | |
| params = ReliefParams( | |
| invert=bool(invert), | |
| detail_strength=float(detail_strength), | |
| detail_sigma=float(detail_sigma), | |
| fine_strength=float(fine_strength), | |
| contrast_gamma=float(contrast_gamma), | |
| mask_thresh=float(mask_thresh), | |
| bg_flatten=bool(bg_flatten), | |
| smooth=float(smooth), | |
| fg_expand=bool(fg_expand), | |
| fg_bg_level=float(fg_bg_level), | |
| fg_local=float(fg_local), | |
| ) | |
| height_float, height16 = build_heightmap(depth_raw, cleaned, params) | |
| # 4) Exports (CPU) | |
| progress(0.85, desc="Writing PNG + STL") | |
| tmpdir = tempfile.mkdtemp(prefix="depthforge_") | |
| png_path = os.path.join(tmpdir, "depthforge_heightmap_16bit.png") | |
| write_png16(height16, png_path) | |
| stl_bytes = heightmap_to_stl( | |
| height_float, | |
| width_mm=float(width_mm), | |
| relief_mm=float(relief_mm), | |
| base_mm=float(base_mm), | |
| mesh_step=int(mesh_step), | |
| ) | |
| stl_path = os.path.join(tmpdir, "depthforge_relief.stl") | |
| with open(stl_path, "wb") as fh: | |
| fh.write(stl_bytes) | |
| # Preview image (8-bit view of the 16-bit map, for display only). | |
| preview = (height_float * 255.0).clip(0, 255).astype(np.uint8) | |
| preview_img = Image.fromarray(preview, mode="L") | |
| total_secs = time.time() - t_all | |
| status = ( | |
| f"Backend: {meta['name']} | License: {meta['license']}" | |
| f"{' (fallback)' if meta.get('fell_back') else ''}\n" | |
| f"Depth inference: {gpu_secs:.1f}s | Total: {total_secs:.1f}s\n" | |
| f"Heightmap: {height16.shape[1]}Γ{height16.shape[0]} px, 16-bit | " | |
| f"STL: {len(stl_bytes)/1024:.0f} KB (width {width_mm:.0f} mm, " | |
| f"relief {relief_mm:.0f} mm, base {base_mm:.0f} mm)" | |
| ) | |
| progress(1.0, desc="Done") | |
| gr.Info("Relief generated.") | |
| return preview_img, cleaned, png_path, stl_path, status | |
| # --------------------------------------------------------------------------- # | |
| # Preset wiring | |
| # --------------------------------------------------------------------------- # | |
| def apply_preset(name: str): | |
| """Return relief-slider updates for the selected preset (unset keys stay).""" | |
| p = get_preset(name) | |
| return ( | |
| gr.update(value=p["detail_strength"]) if "detail_strength" in p else gr.update(), | |
| gr.update(value=p["detail_sigma"]) if "detail_sigma" in p else gr.update(), | |
| gr.update(value=p["fine_strength"]) if "fine_strength" in p else gr.update(), | |
| gr.update(value=p["contrast_gamma"]) if "contrast_gamma" in p else gr.update(), | |
| gr.update(value=p["mask_thresh"]) if "mask_thresh" in p else gr.update(), | |
| gr.update(value=p["bg_flatten"]) if "bg_flatten" in p else gr.update(), | |
| gr.update(value=p["smooth"]) if "smooth" in p else gr.update(), | |
| ) | |
| def on_model_change(model_label: str): | |
| """Set the Invert toggle to the selected backend's default.""" | |
| key = _MODEL_LABEL_TO_KEY.get(model_label, BACKEND_DA3_LARGE) | |
| backend = get_backend(key) | |
| return gr.update(value=backend.invert_default) | |
| # --------------------------------------------------------------------------- # | |
| # UI | |
| # --------------------------------------------------------------------------- # | |
| HOW_IT_WORKS = """ | |
| **How it works.** A depth model estimates the 3D *form*; the **Sculpt (depth-led)** | |
| default keeps that smooth form dominant (strong foreground/background separation) | |
| and injects the photo's own fine texture (fabric, hair, folds) only subtly β so | |
| the result reads as a 3D sculpt, not a flat luminance emboss. Switch presets or | |
| raise *Detail strength* for a more engraved look. Exports a 16-bit heightmap and | |
| a watertight STL for CNC carving. | |
| **Models (quality tiers).** Default **DA3-Large 1.1 (Apache-2.0)** = best | |
| commercial-safe quality; **DA3-Base / DA3-Small (Apache)** are faster; **DA3 | |
| Nested-Giant** and **Depth-Anything-V2 Large** are **CC-BY-NC-4.0** β personal / | |
| non-commercial only. **V2-Small (Apache)** is the CPU-friendly guaranteed | |
| fallback. The active backend and its license are shown in Status. | |
| """ | |
| ASPIRE_STEPS = """ | |
| **Vectric Aspire 12.5 workflow** | |
| *Option A β heightmap (16-bit PNG):* | |
| 1. **Model β Import Bitmap / Import Component From File** and select the PNG. | |
| 2. Set the **model Z height** to your intended relief depth (e.g. 8 mm) and the | |
| **base/border** as needed. White = high, black = low. | |
| 3. Position/scale on the job; combine with **Merge High** if layering. | |
| *Option B β 3D model (STL):* | |
| 1. **Model β Import a Component / 3D Model** and select the STL. | |
| 2. Confirm units (mm) and orientation; the STL is a closed solid with a flat base. | |
| 3. Set material Z, then create **3D Roughing** (bull-nose/ballnose) and | |
| **3D Finishing** (ballnose, small stepover) toolpaths. | |
| Cut base-up with a flat bottom; the relief peaks at `base_mm + relief_mm`. | |
| """ | |
| def build_ui() -> gr.Blocks: | |
| with gr.Blocks(title="DepthForge", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown( | |
| "# πͺ΅ DepthForge\n" | |
| "Turn a photo into a CNC-ready relief heightmap β **16-bit PNG + " | |
| "watertight STL** for Vectric Aspire 12.5." | |
| ) | |
| with gr.Accordion("How it works / License", open=False): | |
| gr.Markdown(HOW_IT_WORKS) | |
| with gr.Row(): | |
| # ---------------- Left: input & controls ---------------- # | |
| with gr.Column(scale=1): | |
| with gr.Accordion("Source", open=True): | |
| image_in = gr.Image(label="Photo", type="pil", sources=["upload", "clipboard"]) | |
| model_dd = gr.Dropdown( | |
| _MODEL_LABELS, | |
| value=_DEFAULT_MODEL_LABEL, | |
| label="Depth model", | |
| ) | |
| preset_dd = gr.Dropdown( | |
| preset_names(), | |
| value=DEFAULT_PRESET, | |
| label="Preset", | |
| ) | |
| with gr.Accordion("Cleanup", open=False): | |
| trim_top = gr.Slider(0, 25, value=CLEANUP.trim_top, step=0.5, label="Trim top %") | |
| trim_bottom = gr.Slider(0, 25, value=CLEANUP.trim_bottom, step=0.5, label="Trim bottom %") | |
| trim_left = gr.Slider(0, 25, value=CLEANUP.trim_left, step=0.5, label="Trim left %") | |
| trim_right = gr.Slider(0, 25, value=CLEANUP.trim_right, step=0.5, label="Trim right %") | |
| denoise = gr.Checkbox(value=CLEANUP.denoise, label="Denoise (median 3Γ3)") | |
| upscale = gr.Slider(1.0, 4.0, value=CLEANUP.upscale, step=0.5, label="Upscale Γ") | |
| max_side = gr.Slider(512, 4096, value=CLEANUP.max_side, step=128, label="Max side (px)") | |
| with gr.Accordion("Subject / depth (Sculpt)", open=True): | |
| fg_expand = gr.Checkbox(value=RELIEF.fg_expand, label="Foreground pop (isolate subject + expand its depth)") | |
| fg_local = gr.Slider(0.0, 4.0, value=RELIEF.fg_local, step=0.1, label="Subject relief (local depth contrast)") | |
| fg_bg_level = gr.Slider(0.0, 0.5, value=RELIEF.fg_bg_level, step=0.01, label="Background level (0 = flat)") | |
| with gr.Accordion("Relief detail", open=False): | |
| invert = gr.Checkbox(value=RELIEF.invert, label="Invert (near β high)") | |
| detail_strength = gr.Slider(0.0, 1.5, value=RELIEF.detail_strength, step=0.01, label="Detail strength (coarse)") | |
| detail_sigma = gr.Slider(1.0, 20.0, value=RELIEF.detail_sigma, step=0.5, label="Detail sigma") | |
| fine_strength = gr.Slider(0.0, 1.0, value=RELIEF.fine_strength, step=0.01, label="Fine strength") | |
| contrast_gamma = gr.Slider(0.3, 2.0, value=RELIEF.contrast_gamma, step=0.01, label="Contrast gamma") | |
| mask_thresh = gr.Slider(0.0, 0.5, value=RELIEF.mask_thresh, step=0.01, label="Mask threshold") | |
| bg_flatten = gr.Checkbox(value=RELIEF.bg_flatten, label="Flatten background") | |
| smooth = gr.Slider(0.0, 5.0, value=RELIEF.smooth, step=0.1, label="Smooth") | |
| with gr.Accordion("3D / STL export", open=False): | |
| width_mm = gr.Slider(20, 400, value=MESH.width_mm, step=1, label="Width (mm, longer side)") | |
| relief_mm = gr.Slider(1, 50, value=MESH.relief_mm, step=0.5, label="Relief depth (mm)") | |
| base_mm = gr.Slider(0, 30, value=MESH.base_mm, step=0.5, label="Base thickness (mm)") | |
| mesh_step = gr.Slider(1, 8, value=MESH.mesh_step, step=1, label="Mesh step (1=full, 2=half, 4=quarter)") | |
| generate_btn = gr.Button("Generate relief", variant="primary") | |
| # ---------------- Right: outputs ---------------- # | |
| with gr.Column(scale=1): | |
| height_preview = gr.Image(label="Heightmap preview (grayscale)", type="pil") | |
| cleaned_preview = gr.Image(label="Cleaned source", type="pil", height=180) | |
| png_file = gr.File(label="Download 16-bit PNG") | |
| stl_file = gr.File(label="Download STL") | |
| status = gr.Textbox(label="Status", lines=4, interactive=False) | |
| with gr.Accordion("Aspire 12.5 instructions", open=False): | |
| gr.Markdown(ASPIRE_STEPS) | |
| # Examples row | |
| example_dir = os.path.join(os.path.dirname(__file__), "examples") | |
| example_imgs = [] | |
| if os.path.isdir(example_dir): | |
| for fn in sorted(os.listdir(example_dir)): | |
| if fn.lower().endswith((".png", ".jpg", ".jpeg", ".webp")): | |
| example_imgs.append([os.path.join(example_dir, fn)]) | |
| if example_imgs: | |
| gr.Examples(examples=example_imgs, inputs=[image_in], label="Examples") | |
| # ---- wiring ---- | |
| preset_dd.change( | |
| apply_preset, | |
| inputs=[preset_dd], | |
| outputs=[detail_strength, detail_sigma, fine_strength, contrast_gamma, mask_thresh, bg_flatten, smooth], | |
| ) | |
| model_dd.change(on_model_change, inputs=[model_dd], outputs=[invert]) | |
| generate_btn.click( | |
| generate, | |
| inputs=[ | |
| image_in, model_dd, | |
| trim_top, trim_bottom, trim_left, trim_right, denoise, upscale, max_side, | |
| invert, detail_strength, detail_sigma, fine_strength, contrast_gamma, mask_thresh, bg_flatten, smooth, | |
| fg_expand, fg_bg_level, fg_local, | |
| width_mm, relief_mm, base_mm, mesh_step, | |
| ], | |
| outputs=[height_preview, cleaned_preview, png_file, stl_file, status], | |
| ) | |
| return demo | |
| demo = build_ui() | |
| if __name__ == "__main__": | |
| if not da3_available(): | |
| logger.warning( | |
| "DA3 package not importable in this environment β DA3-Base will " | |
| "fall back to Depth-Anything-V2 Small at inference time." | |
| ) | |
| demo.queue().launch() | |