Spaces:
Running
Running
Commit ·
4776a40
1
Parent(s): bedcfb7
Simplify STL viewer layout
Browse files
README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# STL to TIFF Gradio App
|
| 2 |
|
| 3 |
-
This project provides a Gradio app that takes an uploaded STL file, slices it along the Z axis, saves each slice as a TIFF image, and lets you browse the stack inside the UI.
|
| 4 |
|
| 5 |
## Run
|
| 6 |
|
|
@@ -14,6 +14,7 @@ Then open the local Gradio URL in your browser, upload an STL file, and generate
|
|
| 14 |
## What the app does
|
| 15 |
|
| 16 |
- Uploads a single `.stl` file
|
|
|
|
| 17 |
- Lets you choose layer height and XY pixel size
|
| 18 |
- Produces one `.tif` image per slice
|
| 19 |
- Lets you step through the slice stack in the browser
|
|
|
|
| 1 |
# STL to TIFF Gradio App
|
| 2 |
|
| 3 |
+
This project provides a Gradio app that takes an uploaded STL file, shows an interactive 3D viewer, slices it along the Z axis, saves each slice as a TIFF image, and lets you browse the stack inside the UI.
|
| 4 |
|
| 5 |
## Run
|
| 6 |
|
|
|
|
| 14 |
## What the app does
|
| 15 |
|
| 16 |
- Uploads a single `.stl` file
|
| 17 |
+
- Shows an interactive 3D viewer for rotating the model
|
| 18 |
- Lets you choose layer height and XY pixel size
|
| 19 |
- Produces one `.tif` image per slice
|
| 20 |
- Lets you step through the slice stack in the browser
|
app.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
| 6 |
import gradio as gr
|
| 7 |
from PIL import Image
|
| 8 |
|
| 9 |
-
from stl_slicer import SliceStack, slice_stl_to_tiffs
|
| 10 |
|
| 11 |
|
| 12 |
ViewerState = dict[str, Any]
|
|
@@ -21,6 +21,10 @@ def _empty_state() -> ViewerState:
|
|
| 21 |
return {"tiff_paths": [], "z_values": []}
|
| 22 |
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
def _stack_to_state(stack: SliceStack) -> ViewerState:
|
| 25 |
return {
|
| 26 |
"tiff_paths": [str(path) for path in stack.tiff_paths],
|
|
@@ -43,6 +47,30 @@ def _format_summary(stack: SliceStack, source_name: str) -> str:
|
|
| 43 |
)
|
| 44 |
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
def _slice_label(state: ViewerState, index: int) -> str:
|
| 47 |
path = Path(state["tiff_paths"][index]).name
|
| 48 |
z_value = state["z_values"][index]
|
|
@@ -64,6 +92,36 @@ def _render_selected_slice(state: ViewerState, index: int) -> tuple[str, Image.I
|
|
| 64 |
)
|
| 65 |
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
def generate_stack(
|
| 68 |
stl_file: str | None,
|
| 69 |
layer_height: float,
|
|
@@ -123,8 +181,8 @@ def build_demo() -> gr.Blocks:
|
|
| 123 |
gr.Markdown(
|
| 124 |
"""
|
| 125 |
# STL to TIFF Slicer
|
| 126 |
-
Upload an STL
|
| 127 |
-
|
| 128 |
"""
|
| 129 |
)
|
| 130 |
|
|
@@ -155,12 +213,19 @@ def build_demo() -> gr.Blocks:
|
|
| 155 |
|
| 156 |
with gr.Column(scale=2):
|
| 157 |
summary = gr.Markdown("Upload an STL file to begin.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
slice_label = gr.Markdown("No slice stack loaded yet.")
|
| 159 |
slice_preview = gr.Image(
|
| 160 |
label="Slice Preview",
|
| 161 |
type="pil",
|
| 162 |
image_mode="L",
|
| 163 |
-
height=
|
| 164 |
)
|
| 165 |
with gr.Row():
|
| 166 |
prev_button = gr.Button("Previous Slice")
|
|
@@ -174,6 +239,22 @@ def build_demo() -> gr.Blocks:
|
|
| 174 |
interactive=False,
|
| 175 |
)
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
generate_button.click(
|
| 178 |
fn=generate_stack,
|
| 179 |
inputs=[stl_file, layer_height, pixel_size],
|
|
|
|
| 6 |
import gradio as gr
|
| 7 |
from PIL import Image
|
| 8 |
|
| 9 |
+
from stl_slicer import SliceStack, load_mesh, slice_stl_to_tiffs
|
| 10 |
|
| 11 |
|
| 12 |
ViewerState = dict[str, Any]
|
|
|
|
| 21 |
return {"tiff_paths": [], "z_values": []}
|
| 22 |
|
| 23 |
|
| 24 |
+
def _reset_slider() -> dict[str, Any]:
|
| 25 |
+
return gr.update(minimum=0, maximum=0, value=0, step=1, interactive=False)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
def _stack_to_state(stack: SliceStack) -> ViewerState:
|
| 29 |
return {
|
| 30 |
"tiff_paths": [str(path) for path in stack.tiff_paths],
|
|
|
|
| 47 |
)
|
| 48 |
|
| 49 |
|
| 50 |
+
def _format_model_status(source_name: str) -> str:
|
| 51 |
+
return "\n".join(
|
| 52 |
+
[
|
| 53 |
+
"### Model Loaded",
|
| 54 |
+
f"- Source: `{source_name}`",
|
| 55 |
+
"- Rotate the model in the 3D viewer, then choose slice settings and generate the TIFF stack.",
|
| 56 |
+
]
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _format_model_details(source_name: str, mesh) -> str:
|
| 61 |
+
extents = mesh.extents
|
| 62 |
+
return "\n".join(
|
| 63 |
+
[
|
| 64 |
+
"### Model Details",
|
| 65 |
+
f"- Source: `{source_name}`",
|
| 66 |
+
f"- Extents: `{extents[0]:.3f} x {extents[1]:.3f} x {extents[2]:.3f}`",
|
| 67 |
+
f"- Faces: `{len(mesh.faces)}`",
|
| 68 |
+
f"- Vertices: `{len(mesh.vertices)}`",
|
| 69 |
+
f"- Watertight: `{'yes' if mesh.is_watertight else 'no'}`",
|
| 70 |
+
]
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
def _slice_label(state: ViewerState, index: int) -> str:
|
| 75 |
path = Path(state["tiff_paths"][index]).name
|
| 76 |
z_value = state["z_values"][index]
|
|
|
|
| 92 |
)
|
| 93 |
|
| 94 |
|
| 95 |
+
def load_model_assets(stl_file: str | None):
|
| 96 |
+
if not stl_file:
|
| 97 |
+
return (
|
| 98 |
+
"Upload an STL file to begin.",
|
| 99 |
+
_empty_state(),
|
| 100 |
+
_reset_slider(),
|
| 101 |
+
"No slice stack loaded yet.",
|
| 102 |
+
None,
|
| 103 |
+
None,
|
| 104 |
+
None,
|
| 105 |
+
None,
|
| 106 |
+
"No model loaded yet.",
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
mesh = load_mesh(stl_file)
|
| 110 |
+
source_name = Path(stl_file).name
|
| 111 |
+
|
| 112 |
+
return (
|
| 113 |
+
_format_model_status(source_name),
|
| 114 |
+
_empty_state(),
|
| 115 |
+
_reset_slider(),
|
| 116 |
+
"No slice stack loaded yet.",
|
| 117 |
+
None,
|
| 118 |
+
None,
|
| 119 |
+
None,
|
| 120 |
+
stl_file,
|
| 121 |
+
_format_model_details(source_name, mesh),
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
def generate_stack(
|
| 126 |
stl_file: str | None,
|
| 127 |
layer_height: float,
|
|
|
|
| 181 |
gr.Markdown(
|
| 182 |
"""
|
| 183 |
# STL to TIFF Slicer
|
| 184 |
+
Upload an STL to inspect it in a rotatable 3D viewer.
|
| 185 |
+
Then choose a layer height and XY pixel size, generate the TIFF stack, and browse the slices below.
|
| 186 |
"""
|
| 187 |
)
|
| 188 |
|
|
|
|
| 213 |
|
| 214 |
with gr.Column(scale=2):
|
| 215 |
summary = gr.Markdown("Upload an STL file to begin.")
|
| 216 |
+
model_details = gr.Markdown("No model loaded yet.")
|
| 217 |
+
model_viewer = gr.Model3D(
|
| 218 |
+
label="Interactive 3D Viewer",
|
| 219 |
+
display_mode="solid",
|
| 220 |
+
clear_color=(0.94, 0.95, 0.97, 1.0),
|
| 221 |
+
height=360,
|
| 222 |
+
)
|
| 223 |
slice_label = gr.Markdown("No slice stack loaded yet.")
|
| 224 |
slice_preview = gr.Image(
|
| 225 |
label="Slice Preview",
|
| 226 |
type="pil",
|
| 227 |
image_mode="L",
|
| 228 |
+
height=420,
|
| 229 |
)
|
| 230 |
with gr.Row():
|
| 231 |
prev_button = gr.Button("Previous Slice")
|
|
|
|
| 239 |
interactive=False,
|
| 240 |
)
|
| 241 |
|
| 242 |
+
stl_file.change(
|
| 243 |
+
fn=load_model_assets,
|
| 244 |
+
inputs=stl_file,
|
| 245 |
+
outputs=[
|
| 246 |
+
summary,
|
| 247 |
+
state,
|
| 248 |
+
slice_slider,
|
| 249 |
+
slice_label,
|
| 250 |
+
slice_preview,
|
| 251 |
+
download_zip,
|
| 252 |
+
current_tiff,
|
| 253 |
+
model_viewer,
|
| 254 |
+
model_details,
|
| 255 |
+
],
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
generate_button.click(
|
| 259 |
fn=generate_stack,
|
| 260 |
inputs=[stl_file, layer_height, pixel_size],
|