Das Projekt kombiniert führende Open-Source-Frameworks zu einer leistungsstarken Pipeline:
- XAMPP: Bereitstellung der Weboberfläche
- Python: Die zentrale Steuerung und Logik der Anwendung.
- Stable Diffusion: Erstellung präziser 2D-Referenzbilder und Texturen aus Prompts.
- TripoSR: Blitzschnelle Rekonstruktion von 3D-Strukturen aus den generierten Bildern.
Important: This documentation is currently a work in progress and may be incomplete.
Suggestions for improvement are welcome and can be submitted via the contact form.
Deutsch:
1. Grundvoraussetzungen installieren
Installiere das ALLES zuerst:
- Python (3.10.x empfohlen)
- ⚠️ Wichtig: NICHT 3.11 oder 3.12
- Beim Installieren: Add to PATH
- Git
- Wird für Repos gebraucht
- CMake
- Wichtig für Open3D / native Builds
- Microsoft Visual C++ Build Tools
- Workload: Desktop development with C++
2. XAMPP (Frontend Hosting)
- Installiere XAMPP
- Starte:
- Apache
Dein Webprojekt liegt dann z. B. hier:
C:\xampp\htdocs\openmeshy
3. Stable Diffusion (AUTOMATIC1111)
Installation
git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.gitcd stable-diffusion-webui
Start-Konfiguration
Bearbeite webui-user.bat:
set COMMANDLINE_ARGS=--api --cors-allow-origins "*" --allow-code
Stable Diffusion muss noch konfiguriert werden, dazu müssen Modelle platziert werden.
Lade ein .safetensors– oder .ckpt-Modell herunter.
und kopiere es in den Ordner stable-diffusion-webui\models\Stable-diffusion\
Neben den Hauptmodellen (Checkpoints) gibt es auch LoRAs
Diese legst du in den entsprechenden Unterordnern im models-Verzeichnis ab
für genauere Anleitung schaue im Internet oder frage eine AI.
Stable Diffusion starten
webui-user.bat
API läuft auf:
http://127.0.0.1:7860
4. TripoSR (Image → 3D Mesh)
Installation
git clone https://github.com/VAST-AI-Research/TripoSR.gitcd TripoSRVirtuelle Umgebung
python -m venv venvvenv\Scripts\activateDependencies
pip install -r requirements.txtpip install rembgpip install torch torchvision torchaudiopip install open3dpip install git+https://github.com/tatsy/torchmcubes.gitFalls GPU: passende CUDA-Version installieren
5. OpenMeshy Backend (FastAPI)
Das ist dein eigener Server.
Installation
pip install fastapi uvicornpip install pillow numpy opencv-pythonpip install matplotlibInstalliere zusätzlich:
pip install rembgpip install open3dpip install torchpip install requests6. Projektstruktur
So könnte dein Setup ungefähr aussehen:
C:\OpenMeshy\│├── backend\│ └── main.py (FastAPI)│├── \Users\DeinBenutzername\TripoSR\│ ├── run.py│ └── venv\│├── stable-diffusion-webui\│└── xampp\htdocs\openmeshy\└── index.html7. Open Meshy vorbereiten
index.html und info.html in den htdocs/openmehsy Ordner kopieren
die Datei main.py in den Ordner C:\OpenMeshy kopieren
in der Datei main.py muss der Pfad zu deinem TripoSR Ordner geändert werden wenn er woanders als hier "C:\Users\DeinBenutzername\TripoSR" liegt
TRIPOSR_DIR = os.path.join(os.path.expanduser("~"),"TripoSR")
dieser Teil muss angepasst werden, je nachdem wo dein Ordner liegt
8. Backend starten
uvicorn main:app --reloadläuft dann auf:
http://127.0.0.1:80009. Aufrufen der Webseite im Browser
gebe folgendes in deinen Browser ein nachdem Xampp, main.py und Stable Diffusion gestartet wurde
http://127.0.0.1/openmeshy/
Important: This documentation is currently a work in progress and may be incomplete.
Suggestions for improvement are welcome and can be submitted via the contact form.
English:
1. Install Basic Requirements
Install ALL of this first:
-
Python (3.10.x recommended)
-
⚠️ Important: NOT 3.11 or 3.12
-
During installation: Check Add to PATH
-
-
Git – needed for repositories
-
CMake – important for Open3D / native builds
-
Microsoft Visual C++ Build Tools
-
Workload: Desktop development with C++
-
2. XAMPP (Frontend Hosting)
-
Install XAMPP
-
Start: Apache
Your web project will then be located here, for example:
C:\xampp\htdocs\openmeshy
3. Stable Diffusion (AUTOMATIC1111)
Installation
git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.gitcd stable-diffusion-webui
Startup Configuration
Edit webui-user.bat:
set COMMANDLINE_ARGS=--api --cors-allow-origins "*" --allow-code
Stable Diffusion still needs to be configured – you must place models.
-
Download a
.safetensorsor.ckptmodel -
Copy it into the folder:
stable-diffusion-webui\models\Stable-diffusion\
In addition to the main models (checkpoints), there are also LoRAs.
Place them in the corresponding subfolders inside the models directory.
For more detailed instructions, search online or ask an AI.
Start Stable Diffusion
webui-user.bat
The API runs on:
http://127.0.0.1:7860
4. TripoSR (Image → 3D Mesh)
Installation
git clone https://github.com/VAST-AI-Research/TripoSR.gitcd TripoSR
Virtual Environment
python -m venv venvvenv\Scripts\activate
Dependencies
pip install -r requirements.txtpip install rembgpip install torch torchvision torchaudiopip install open3dpip install git+https://github.com/tatsy/torchmcubes.git
If you have a GPU: install the appropriate CUDA version.
5. OpenMeshy Backend (FastAPI)
This is your own server.
Installation
pip install fastapi uvicornpip install pillow numpy opencv-pythonpip install matplotlib
pip install rembgpip install open3dpip install torchpip install requests
6. Project Structure
Your setup could look something like this:
C:\OpenMeshy\│├── backend\│ └── main.py (FastAPI)│├── Users\YourUsername\TripoSR\│ ├── run.py│ └── venv\│├── stable-diffusion-webui\│└── xampp\htdocs\openmeshy\ └── index.html
7. Prepare Open Meshy
-
Copy
index.htmlandinfo.htmlinto thehtdocs\openmeshyfolder -
Copy the
main.pyfile into theC:\OpenMeshyfolder
In the main.py file, the path to your TripoSR folder must be changed if it is located somewhere other than:
C:\Users\YourUsername\TripoSR
TRIPOSR_DIR = os.path.join( os.path.expanduser("~"), "TripoSR")
This part must be adjusted depending on where your folder is located.
8. Start the Backend
uvicorn main:app --reload
It then runs on:
http://127.0.0.1:8000
9. Open the Website in Your Browser
After XAMPP, main.py, and Stable Diffusion have been started, enter the following in your browser:
http://127.0.0.1/openmeshy/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Open Meshy</title>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.module.js"
}
}
</script>
<style>
body {
font-family: Arial, sans-serif;
background: #0f0f0f;
color: #ffffff;
display: flex;
justify-content: center;
margin: 0;
padding-top: 80px;
}
.container {
width: 420px;
padding: 25px;
background: #1a1a1a;
border-radius: 12px;
box-shadow: 0 0 25px rgba(0,0,0,0.6);
}
.texture-btn {
background: #2196F3 !important; /* Blue for texture-related actions */
margin-top: 10px !important;
}
.texture-btn:hover {
background: #1976D2 !important;
}
.texture-preview {
width: 100%;
max-height: 250px;
object-fit: cover;
border-radius: 8px;
margin-top: 5px;
border: 1px dashed #444;
display: none; /* Initially hidden until processing is complete */
}
h1 {
text-align: center;
margin-bottom: 10px;
}
p {
text-align: center;
font-size: 14px;
opacity: 0.7;
}
label {
font-size: 14px;
}
textarea {
width: 100%;
height: 80px;
margin-bottom: 10px;
padding: 10px;
border-radius: 6px;
border: none;
resize: none;
background: #2a2a2a;
color: white;
box-sizing: border-box; /* Ensures padding doesn't affect width */
}
button {
width: 100%;
padding: 12px;
background: #00c853;
border: none;
border-radius: 6px;
color: white;
font-size: 16px;
cursor: pointer;
margin-top: 10px;
transition: 0.2s;
}
button:hover {
background: #00e676;
}
button:disabled {
background: #555;
cursor: not-allowed;
}
.status {
margin-top: 10px;
text-align: center;
font-size: 14px;
opacity: 0.8;
}
#resultImage {
margin-top: 15px;
width: 100%;
max-height: 300px;
object-fit: contain;
border-radius: 8px;
display: none;
}
/* Loading Overlay Styling */
#overlay {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.75);
color: white;
justify-content: center;
align-items: center;
font-size: 20px;
z-index: 999;
}
.navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 60px;
background: #141414;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 25px;
box-shadow: 0 2px 10px rgba(0,0,0,0.6);
z-index: 1000;
}
.logo {
font-weight: bold;
font-size: 18px;
}
.nav-right {
position: absolute;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px;
}
.nav-right a {
color: #ffffff;
text-decoration: none;
margin-left: 20px;
font-size: 14px;
opacity: 0.8;
transition: 0.2s;
}
.nav-right a:hover {
opacity: 1;
color: #00e676;
}
.download-btn {
background: #2a2a2a !important;
margin-top: 5px !important;
font-size: 14px !important;
padding: 8px !important;
}
.download-btn:hover {
background: #3a3a3a !important;
}
</style>
</head>
<body>
<div class="navbar">
<div class="nav-left">
<span class="logo">🧠 Open Meshy</span>
</div>
<div class="nav-right">
<a href="info.html">About</a>
<a href="https://computerkids.berlin/kontakt/">Contact</a>
</div>
</div>
<div id="overlay">
Generating... please wait ⏳
</div>
<div class="container">
<h1>🧠 Open Meshy</h1>
<p>Step 1: Generate Image → Step 2: Create 3D Mesh</p>
<label>Prompt</label>
<textarea id="prompt" placeholder="e.g., low poly tank, green, stylized"></textarea>
<label>Negative Prompt</label>
<textarea id="negative" placeholder="e.g., blurry, realistic, high detail"></textarea>
<button onclick="generate()">Generate Image</button>
<div class="status" id="status">System Ready</div>
<input type="file" id="fileInput" accept="image/*" style="display: none;" />
<button onclick="uploadImage()">Upload Existing Image</button>
<img id="resultImage" />
<img id="depthImage" style="display:none; width:100%; margin-top:10px; border-radius:8px;" />
<div id="viewer" style="width:100%; height:300px; margin-top:15px; display:none;"></div>
<button id="downloadBtn" style="display:none;">Download Mesh</button>
<button id="continueBtn" disabled onclick="continueProcess()">
Continue / Create 3D Mesh
</button>
<div id="resultsArea" style="display:none; margin-top: 20px;">
<h3 style="text-align:center; color:#00c853;">Generation Results</h3>
<div style="display: flex; gap: 10px; margin-bottom: 20px;">
<div style="flex: 1; text-align: center;">
<label style="font-size:12px; opacity:0.7;">Original View</label>
<img id="resultImage" style="width:100%; max-height:200px; object-fit:contain; border-radius:8px;" />
</div>
<div style="flex: 1; text-align: center;">
<label style="font-size:12px; opacity:0.7;">Depth Map</label>
<img id="depthImage" style="width:100%; max-height:200px; object-fit:contain; border-radius:8px;" />
</div>
</div>
<div style="margin-bottom: 20px;">
<label>3D Mesh (from Original Image)</label>
<div id="viewerOriginal" style="width:100%; height:250px; background:#141414; border-radius:8px; margin-top:5px;"></div>
<button id="downloadOriginalBtn" class="download-btn">⬇️ Download Original Mesh</button>
</div>
<div style="margin-bottom: 20px;">
<label>3D Mesh (from Depth Map)</label>
<div id="viewerDepth" style="width:100%; height:250px; background:#141414; border-radius:8px; margin-top:5px;"></div>
<button id="downloadDepthBtn" class="download-btn">⬇️ Download Depth Mesh</button>
</div>
<div style="margin-bottom: 20px; border-top: 1px solid #333; padding-top: 20px;">
<label>🎨 Texture Generation (img2img)</label>
<p style="text-align:left; font-size:12px; margin-bottom:10px;">
Creates a matching, flat texture based on the original image using AI.
</p>
<img id="texturePreview" class="texture-preview" />
<button id="genTextureBtn" class="texture-btn" onclick="generateTexture()">
✨ Generate Textures
</button>
<button id="downloadTextureBtn" class="download-btn" style="display:none;">
⬇️ Download Texture (.png)
</button>
</div>
</div>
</div>
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.module.js";
import { OBJLoader } from "https://cdn.jsdelivr.net/npm/three@0.158.0/examples/jsm/loaders/OBJLoader.js";
import { OrbitControls } from "https://cdn.jsdelivr.net/npm/three@0.158.0/examples/jsm/controls/OrbitControls.js";
/**
* Stage 1: Generates an AI image based on the text prompt.
* Communicates with the local server to trigger the diffusion model.
*/
async function generate() {
const prompt = document.getElementById("prompt").value;
const negative = document.getElementById("negative").value;
const status = document.getElementById("status");
const overlay = document.getElementById("overlay");
if (!prompt) {
status.innerText = "Please enter a prompt!";
alert("Prompt is required to generate an image.");
return;
}
status.innerText = "Generating image...";
overlay.style.display = "flex";
try {
const response = await fetch("http://localhost:8000/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: prompt,
negative_prompt: negative
})
});
if (!response.ok) throw new Error("Server error occurred during generation.");
const data = await response.json();
if (data.images && data.images.length > 0) {
const base64 = data.images[0];
const imageUrl = "data:image/png;base64," + base64;
const img = document.getElementById("resultImage");
img.src = imageUrl;
img.style.display = "block";
status.innerText = "Image generated successfully!";
document.getElementById("continueBtn").disabled = false;
} else {
status.innerText = "No image data received from server.";
}
} catch (error) {
console.error(error);
status.innerText = "Error: Is your local backend running?";
}
overlay.style.display = "none";
}
/**
* Stage 2: Takes the generated image and requests a 3D Mesh conversion.
* Sends the Base64 image back to the server for OBJ creation.
*/
function continueProcess() {
const img = document.getElementById("resultImage");
const src = img.src;
const status = document.getElementById("status");
const overlay = document.getElementById("overlay");
if (!src || src.startsWith('data:image/png;base64,,')) {
alert("No valid image found! Please generate an image first.");
return;
}
status.innerText = "Creating 3D Meshes (this may take a moment)...";
overlay.style.display = "flex";
// Clear previous viewer instances
document.getElementById("viewerOriginal").innerHTML = "";
document.getElementById("viewerDepth").innerHTML = "";
fetch("http://localhost:8000/mesh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ image: src })
})
.then(res => res.json())
.then(data => {
console.log("Mesh Response:", data);
if (data.status === "ok" || data.run_id) {
pollStatus(data.run_id); // Start checking the processing status
} else {
overlay.style.display = "none";
status.innerText = "Mesh generation failed.";
alert("Backend Error:\n" + JSON.stringify(data, null, 2));
}
})
.catch(err => {
console.error("NETWORK OR JS ERROR:", err);
overlay.style.display = "none";
status.innerText = "Frontend error (check console).";
});
}
/**
* Initializes a Three.js scene to preview the generated OBJ models.
* @param {string} objPath - The URL to the .obj file.
* @param {string} containerId - The ID of the HTML element to render into.
*/
function show3DModel(objPath, containerId) {
const status = document.getElementById("status");
const viewer = document.getElementById(containerId);
if (!viewer) return;
viewer.innerHTML = "";
const scene = new THREE.Scene();
const aspect = viewer.clientWidth / viewer.clientHeight;
const camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(viewer.clientWidth, viewer.clientHeight);
renderer.setClearColor(0x000000, 0);
viewer.appendChild(renderer.domElement);
// Basic Lighting setup
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(1, 1, 1);
scene.add(light);
const loader = new OBJLoader();
status.innerText = `Loading 3D model into ${containerId}...`;
loader.load(objPath, function (object) {
// Apply a neutral standard material to the mesh
object.traverse(function (child) {
if (child.isMesh) {
child.material = new THREE.MeshStandardMaterial({
color: 0xffffff,
metalness: 0.1,
roughness: 0.5
});
}
});
// Center the model in the scene
const box = new THREE.Box3().setFromObject(object);
const center = box.getCenter(new THREE.Vector3());
object.position.sub(center);
// Normalize scale for the preview window
const size = box.getSize(new THREE.Vector3()).length();
const scale = 2.5 / size;
object.scale.setScalar(scale);
scene.add(object);
status.innerText = "Model loaded successfully.";
},
// Progress logging
function (xhr) {
console.log(containerId + ': ' + (xhr.loaded / xhr.total * 100) + '% loaded');
},
// Error handling for loader
function (error) {
console.error('Error loading model:', error);
viewer.innerHTML = "<p style='color:red; text-align:center; padding-top:100px;'>Failed to load 3D model.</p>";
});
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(0, 1, 4);
controls.update();
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// Handle window resizing to keep the aspect ratio correct
window.addEventListener('resize', () => {
camera.aspect = viewer.clientWidth / viewer.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(viewer.clientWidth, viewer.clientHeight);
}, false);
}
/**
* Stage 3: Generates a texture map from the original image using img2img.
*/
async function generateTexture() {
const status = document.getElementById("status");
const overlay = document.getElementById("overlay");
const resultImg = document.getElementById("resultImage");
const texturePreview = document.getElementById("texturePreview");
const downloadBtn = document.getElementById("downloadTextureBtn");
if (!resultImg.src || resultImg.src.includes('placeholder')) {
alert("No image found to create a texture from!");
return;
}
status.innerText = "Extracting texture via img2img...";
overlay.style.display = "flex";
try {
// Convert the current image source to a blob to avoid cross-origin or data-url issues
const responseBlob = await fetch(resultImg.src);
const blob = await responseBlob.blob();
const base64data = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
status.innerText = "AI is generating the texture...";
const response = await fetch("http://localhost:8000/generate-texture", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
image: base64data,
denoising: 0.5
})
});
const data = await response.json();
if (data.status === "ok") {
const imageUrl = "http://localhost:8000" + data.texture_url;
texturePreview.src = imageUrl
texturePreview.style.display = "block";
downloadBtn.style.display = "block";
downloadBtn.onclick = async () => {
try {
// Download mechanism using a virtual link and Blob URL
const response = await fetch(texturePreview.src);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = "open_meshy_texture.png";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (err) {
console.error("Download failed:", err);
window.open(texturePreview.src, "_blank");
}
};
status.innerText = "Texture ready!";
} else {
throw new Error(data.message || "Backend error during texture generation.");
}
} catch (error) {
console.error(error);
alert("Texture Error: " + error.message);
status.innerText = "Failed to generate texture.";
} finally {
overlay.style.display = "none";
}
}
/**
* Utility: Triggers the hidden file input to allow users to upload their own images.
*/
function uploadImage() {
const input = document.getElementById("fileInput");
input.click();
input.onchange = async () => {
const file = input.files[0];
if (!file) return;
const formData = new FormData();
formData.append("image", file);
try {
const response = await fetch("http://127.0.0.1:8000/upload", {
method: "POST",
body: formData
});
const result = await response.json();
if (result.run_id) {
document.getElementById("overlay").style.display = "flex";
document.getElementById("status").innerText = "Upload successful, polling status...";
pollStatus(result.run_id);
}
} catch (error) {
console.error("Upload failed:", error);
alert("Error uploading the image.");
}
};
}
/**
* Polling Mechanism: Periodically checks the server for the completion status of a background task.
* @param {string} runId - The ID of the current background process.
*/
async function pollStatus(runId) {
const statusText = document.getElementById("status");
while (true) {
try {
const response = await fetch(`http://localhost:8000/status/${runId}`);
const data = await response.json();
if (data.status === "completed") {
statusText.innerText = "Generation complete!";
displayResults(data);
document.getElementById("overlay").style.display = "none";
break;
} else if (data.status === "error") {
statusText.innerText = "Backend error: " + data.error;
document.getElementById("overlay").style.display = "none";
break;
} else {
statusText.innerText = "AI is working... (Creating 3D Models)";
}
} catch (err) {
console.error("Polling error:", err);
}
// Wait 2 seconds before the next poll attempt
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
/**
* Updates the UI with final images and initializes the 3D viewers.
* @param {object} data - The completion data containing URLs for images and meshes.
*/
function displayResults(data) {
const resultsArea = document.getElementById("resultsArea");
resultsArea.style.display = "block";
const baseUrl = "http://localhost:8000";
// Update result images
document.getElementById("resultImage").src = baseUrl + data.original_image_url;
document.getElementById("depthImage").src = baseUrl + data.depth_image_url;
// Load 3D models into their respective viewers
show3DModel(baseUrl + data.original_mesh_url, "viewerOriginal");
show3DModel(baseUrl + data.depth_mesh_url, "viewerDepth");
// Assign download links to buttons
document.getElementById("downloadOriginalBtn").onclick = () => window.open(baseUrl + data.original_mesh_url, "_blank");
document.getElementById("downloadDepthBtn").onclick = () => window.open(baseUrl + data.depth_mesh_url, "_blank");
}
// Global scope registration for HTML onclick attributes
window.uploadImage = uploadImage;
window.generate = generate;
window.continueProcess = continueProcess;
window.generateTexture = generateTexture;
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OpenMeshy – Info</title>
<style>
body {
font-family: Arial, sans-serif;
background: #0f0f0f;
color: #ffffff;
margin: 0;
padding-top: 80px;
display: flex;
justify-content: center;
}
.navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 60px;
background: #141414;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 25px;
box-shadow: 0 2px 10px rgba(0,0,0,0.6);
z-index: 1000;
}
.nav-links {
position: absolute;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px;
}
.nav-links a {
color: white;
text-decoration: none;
font-size: 14px;
opacity: 0.8;
}
.nav-links a:hover {
opacity: 1;
}
.container {
width: 700px;
background: #1a1a1a;
padding: 30px;
border-radius: 12px;
box-shadow: 0 0 25px rgba(0,0,0,0.6);
}
h1 {
text-align: center;
margin-bottom: 20px;
}
h2 {
color: #00e676;
margin-top: 25px;
}
p {
line-height: 1.6;
opacity: 0.9;
}
ul {
line-height: 1.6;
}
</style>
</head>
<body>
<div class="navbar">
<div class="logo">🧠 OpenMeshy</div>
<div class="nav-links">
<a href="index.html">Home</a>
<a href="https://computerkids.berlin/kontakt/">Contact</a>
</div>
</div>
<div class="container">
<h1>About OpenMeshy</h1>
<h2>What is this?</h2>
<p>
OpenMeshy is an experimental open-source project for local 3D model generation.
Inspired by commercial solutions like Meshy.ai, the focus here is on complete independence
and cost-free usage by leveraging modern AI models on your own hardware.
</p>
<h2>Project Goals</h2>
<ul>
<li>Testing the limits of open-source technologies in 3D creation.</li>
<li>Generating high-quality 3D assets without subscription models or cloud costs.</li>
<li>Automating the pipeline from text input to the finished mesh.</li>
<li>Creating a transparent alternative to proprietary AI platforms.</li>
</ul>
<h2>Technology & Requirements</h2>
<p>
The project combines leading open-source frameworks into a powerful pipeline:
</p>
<ul>
<li><strong>XAMPP:</strong> Provides the web interface</li>
<li><strong>Python:</strong> Central control and application logic.</li>
<li><strong>Stable Diffusion:</strong> Creation of precise 2D reference images and textures from prompts.</li>
<li><strong>TripoSR:</strong> Lightning-fast reconstruction of 3D structures from the generated images.</li>
</ul>
<h2>Why this project?</h2>
<p>
While commercial tools are often expensive and upload data to the cloud, OpenMeshy aims to show
that professional 3D generation is possible locally and for free. It is a proof-of-concept
for developers and creators who want to keep full control over their workflow.
</p>
</div>
</body>
</html>
from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import requests
import base64
import uuid
from fastapi import Request
from fastapi.responses import JSONResponse
import torch
import cv2
import open3d as o3d
import numpy as np
import matplotlib.pyplot as plt
import threading
from fastapi.staticfiles import StaticFiles
import os
import subprocess
import sys
import shutil
import time
from PIL import Image
import io
# Define valid image formats for upload
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg"}
# Dictionary to store the status and results of asynchronous mesh generation tasks
mesh_tasks = {}
# Ensure required directories exist
os.makedirs("outputs", exist_ok=True)
if not os.path.exists("uploads"):
os.makedirs("uploads")
# Path to the TripoSR local installation Directory
# IMPORTANT: Replace this with the path to your own TripoSR directory
TRIPOSR_DIR = os.path.join(
os.path.expanduser("~"),
"TripoSR"
)
# --- AI Model Initialization ---
# Load MiDaS model for depth estimation only once during startup to save memory/VRAM
print("Loading MiDaS depth estimation model...")
model = torch.hub.load("intel-isl/MiDaS", "MiDaS_small")
model.eval()
# Load specific preprocessing transforms for the MiDaS "small" variant
transforms = torch.hub.load("intel-isl/MiDaS", "transforms")
transform = transforms.small_transform
app = FastAPI()
# Enable CORS for frontend communication (crucial for local development)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Serve the 'outputs' folder as a static directory so the frontend can access images and meshes via URL
app.mount("/outputs", StaticFiles(directory="outputs"), name="outputs")
class GenerateRequest(BaseModel):
prompt: str
negative_prompt: str = ""
def get_depth(image_path):
"""
Predicts a depth map from a given image using the MiDaS model.
Includes preprocessing, inference, and post-filtering.
"""
img = cv2.imread(image_path)
if img is None:
raise Exception("Failed to load image for depth estimation")
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Preprocess the image according to model requirements
input_batch = transform(img)
# Add batch dimension if necessary (NCHW format)
if len(input_batch.shape) == 3:
input_batch = input_batch.unsqueeze(0)
with torch.no_grad():
prediction = model(input_batch)
depth = prediction.squeeze().cpu().numpy()
# Normalize depth for visualization and filtering
depth_vis = cv2.normalize(depth, None, 0, 255, cv2.NORM_MINMAX).astype("uint8")
# Apply bilateral filter to smooth out noise while preserving edges
depth = cv2.bilateralFilter(depth, 9, 75, 75)
return depth
def depth_to_mesh(depth):
"""
Converts a 2D depth map into a 3D point cloud and subsequently into a mesh using Poisson reconstruction.
"""
h, w = depth.shape
points = []
for y in range(h):
for x in range(w):
z = float(depth[y][x])
points.append([x, y, z])
points = np.array(points)
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points)
# Estimate normals for the point cloud, which is required for Poisson reconstruction
pcd.estimate_normals(
search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=5, max_nn=30)
)
# Generate the 3D triangle mesh from the points
mesh, _ = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(pcd, depth=8)
return mesh
def show_mesh(path):
""" Utility function to open an Open3D visualization window for local debugging. """
mesh = o3d.io.read_triangle_mesh(path)
o3d.visualization.draw_geometries([mesh])
def save_depth_image(depth, path):
""" Normalizes the float depth map to 8-bit and saves it as a PNG. """
depth_vis = cv2.normalize(depth, None, 0, 255, cv2.NORM_MINMAX)
depth_vis = depth_vis.astype("uint8")
cv2.imwrite(path, depth_vis)
def run_triposr(image_path, run_id, suffix=""):
"""
External call to the TripoSR local installation via subprocess.
This generates a 3D OBJ mesh from a single 2D image.
"""
local_output_dir = os.path.join("outputs", run_id)
os.makedirs(local_output_dir, exist_ok=True)
filename = os.path.basename(image_path)
# Copy source image to TripoSR directory so run.py can find it locally
triposr_input_path = os.path.join(TRIPOSR_DIR, filename)
shutil.copy(image_path, triposr_input_path)
# Define a temporary output folder inside TripoSR
triposr_temp_out = os.path.join(TRIPOSR_DIR, "output_temp_" + run_id)
# Command to run TripoSR's inference script
command = [
sys.executable,
"run.py",
filename,
"--output-dir", triposr_temp_out,
"--model-save-format", "obj"
]
print(f"🚀 Starting TripoSR inference ({suffix})...")
subprocess.run(command, cwd=TRIPOSR_DIR)
# Search for the generated mesh.obj in the temporary folder
found_file = None
for root, dirs, files in os.walk(triposr_temp_out):
for f in files:
if f.endswith(".obj"):
found_file = os.path.join(root, f)
break
if found_file: break
if not found_file:
raise Exception(f"TripoSR failed to generate a mesh for {suffix}")
# Move the result to our project's output directory and rename it uniquely
new_filename = f"{suffix}_{run_id}.obj"
final_path = os.path.join(local_output_dir, new_filename)
shutil.move(found_file, final_path)
# Cleanup: remove temp folders and the copied input image
shutil.rmtree(triposr_temp_out, ignore_errors=True)
if os.path.exists(triposr_input_path):
os.remove(triposr_input_path)
print(f"✅ Mesh successfully moved to: {final_path}")
# Return relative path for web access
return f"outputs/{run_id}/{new_filename}"
async def generate_mesh_logic(image_path: str, run_id: str):
"""
Main asynchronous pipeline:
1. Generates mesh from original image.
2. Generates depth map.
3. Generates mesh from depth map.
"""
try:
mesh_tasks[run_id] = {"status": "processing"}
current_outputs_dir = os.path.join("outputs", run_id)
os.makedirs(current_outputs_dir, exist_ok=True)
# Step 1: Create mesh from the original image
rel_mesh_original = run_triposr(image_path, run_id, "original")
# Step 2: Extract Depth Map
depth = get_depth(image_path)
depth_filename = f"depth_{run_id}.png"
depth_path = os.path.join(current_outputs_dir, depth_filename)
save_depth_image(depth, depth_path)
# Step 3: Create mesh specifically from the depth map visualization
rel_mesh_depth = run_triposr(depth_path, run_id, "depth")
# Save results in the task dictionary
mesh_tasks[run_id] = {
"status": "completed",
"original_image_url": f"/outputs/{run_id}/{os.path.basename(image_path)}",
"depth_image_url": f"/outputs/{run_id}/{depth_filename}",
"original_mesh_url": f"/{rel_mesh_original}",
"depth_mesh_url": f"/{rel_mesh_depth}"
}
print(f"✅ Task {run_id} completed successfully.")
except Exception as e:
print(f"❌ Error in generate_mesh_logic: {e}")
mesh_tasks[run_id] = {"status": "error", "error": str(e)}
@app.post("/generate")
def generate(req: GenerateRequest):
""" Calls the Stable Diffusion API (Automatic1111) to generate an image from text. """
print(f"Generating image - Prompt: {req.prompt}")
url = "http://127.0.0.1:7860/sdapi/v1/txt2img"
payload = {
"prompt": req.prompt,
"negative_prompt": req.negative_prompt,
"steps": 10
}
try:
response = requests.post(url, json=payload)
if response.status_code != 200:
return {
"status": "error",
"message": "Stable Diffusion API error",
"status_code": response.status_code,
"text": response.text
}
data = response.json()
if "images" not in data:
return {
"status": "error",
"message": "No images received from Stable Diffusion",
"raw": data
}
return {"images": data["images"]}
except Exception as e:
return {"status": "error", "error": str(e)}
@app.post("/mesh")
async def create_mesh(request: Request, background_tasks: BackgroundTasks):
""" Receives a Base64 image from the frontend and triggers the async 3D pipeline. """
run_id = uuid.uuid4().hex
data = await request.json()
image_data = data.get("image")
# Split Base64 header and data
header, encoded = image_data.split(",", 1)
image_bytes = base64.b64decode(encoded)
# Setup unique output folder for this request
current_outputs_dir = os.path.join("outputs", run_id)
os.makedirs(current_outputs_dir, exist_ok=True)
# Save original image as PNG
original_filename = f"input_{run_id}.png"
original_path = os.path.join(current_outputs_dir, original_filename)
with open(original_path, "wb") as f:
f.write(image_bytes)
print(f"📸 Original image saved: {original_path}")
# Start the CPU/GPU intensive mesh logic in the background
background_tasks.add_task(generate_mesh_logic, original_path, run_id)
return {"status": "ok", "run_id": run_id}
@app.post("/generate-texture")
async def generate_texture(request: Request):
"""
Uses img2img via Stable Diffusion to transform the original image into
a flat, seamless albedo texture map.
"""
try:
data = await request.json()
image_data = data.get("image")
if "," in image_data:
encoded = image_data.split(",")[1]
else:
encoded = image_data
tex_id = uuid.uuid4().hex
tex_dir = os.path.join("outputs", "textures", tex_id)
os.makedirs(tex_dir, exist_ok=True)
# Request to SD for texture creation
sd_url = "http://127.0.0.1:7860/sdapi/v1/img2img"
payload = {
"init_images": [encoded],
"prompt": "flat texture, albedo map, (seamless:1.3), high resolution, neutral lighting, centered, no shadows",
"negative_prompt": "shadows, 3d, perspective, depth, dark edges, vignette, blur, glossy",
"steps": 20,
"cfg_scale": 7,
"denoising_strength": 0.5,
"width": 512,
"height": 512,
"sampler_name": "Euler a"
}
print(f"🚀 Sending img2img request for texture generation...")
response = requests.post(sd_url, json=payload, timeout=120)
response.raise_for_status()
data_sd = response.json()
texture_base64 = data_sd["images"][0]
texture_bytes = base64.b64decode(texture_base64)
texture_filename = f"texture_{tex_id}.png"
texture_path = os.path.join(tex_dir, texture_filename)
with open(texture_path, "wb") as f:
f.write(texture_bytes)
print(f"✅ Texture generated and saved: {texture_path}")
return {
"status": "ok",
"texture_url": f"/outputs/textures/{tex_id}/{texture_filename}"
}
except Exception as e:
import traceback
traceback.print_exc()
return {"status": "error", "message": str(e)}
@app.post("/upload")
async def upload_image(background_tasks: BackgroundTasks, image: UploadFile = File(...)):
"""
Handles direct file uploads from the user.
Includes security checks for file type and image integrity.
"""
print(f"Processing uploaded file: {image.filename}")
extension = image.filename.split(".")[-1].lower()
if extension not in ALLOWED_EXTENSIONS:
raise HTTPException(status_code=400, detail="File type not allowed!")
contents = await image.read()
print(f"Received data size: {len(contents)} bytes")
# Image integrity check using Pillow
try:
img = Image.open(io.BytesIO(contents))
img.verify() # Validates that the file is indeed an image
print(f"Verified image format: {img.format}")
except Exception:
raise HTTPException(status_code=400, detail="Invalid image file detected!")
run_id = uuid.uuid4().hex
current_outputs_dir = os.path.join("outputs", run_id)
os.makedirs(current_outputs_dir, exist_ok=True)
unique_filename = f"{run_id}.{extension}"
file_path = os.path.join(current_outputs_dir, unique_filename)
# Store the uploaded file
with open(file_path, "wb") as f:
f.write(contents)
background_tasks.add_task(generate_mesh_logic, file_path, run_id)
return {"status": "ok", "run_id": run_id}
@app.get("/status/{run_id}")
async def get_status(run_id: str):
""" Endpoint for the frontend to poll the current state of a mesh generation task. """
return mesh_tasks.get(run_id, {"status": "not_found"})
letztes Update: 03.04.2026