Spaces:
Sleeping
Sleeping
File size: 7,911 Bytes
c105678 e5edf92 c105678 e5edf92 c105678 e5edf92 c105678 e5edf92 c105678 e5edf92 c105678 e5edf92 c105678 e5edf92 c105678 e5edf92 c105678 e5edf92 c105678 e5edf92 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
import os
import torch
from flask import Flask, request, jsonify, send_file
from werkzeug.utils import secure_filename
from PIL import Image
import io
import zipfile
import uuid
import traceback
from diffusers import ShapEImg2ImgPipeline
from diffusers.utils import export_to_obj
app = Flask(__name__)
# Configure directories - use /tmp for Hugging Face Spaces which is writable
UPLOAD_FOLDER = '/tmp/uploads'
RESULTS_FOLDER = '/tmp/results'
CACHE_DIR = '/tmp/huggingface'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}
# Create necessary directories
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(RESULTS_FOLDER, exist_ok=True)
os.makedirs(CACHE_DIR, exist_ok=True)
# Set Hugging Face cache environment variables
os.environ['HF_HOME'] = CACHE_DIR
os.environ['TRANSFORMERS_CACHE'] = os.path.join(CACHE_DIR, 'transformers')
os.environ['HF_DATASETS_CACHE'] = os.path.join(CACHE_DIR, 'datasets')
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max
# Lazy loading for the model - only load when needed
device = "cuda" if torch.cuda.is_available() else "cpu"
pipe = None
def load_model():
global pipe
if pipe is None:
pipe = ShapEImg2ImgPipeline.from_pretrained(
"openai/shap-e-img2img",
torch_dtype=torch.float16 if device == "cuda" else torch.float32,
cache_dir=CACHE_DIR # Explicitly set cache directory
)
pipe = pipe.to(device)
return pipe
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/health', methods=['GET'])
def health_check():
return jsonify({"status": "healthy", "model": "Shap-E Image to 3D"}), 200
@app.route('/convert', methods=['POST'])
def convert_image_to_3d():
# Check if image is in the request
if 'image' not in request.files:
return jsonify({"error": "No image provided"}), 400
file = request.files['image']
if file.filename == '':
return jsonify({"error": "No image selected"}), 400
if not allowed_file(file.filename):
return jsonify({"error": f"File type not allowed. Supported types: {', '.join(ALLOWED_EXTENSIONS)}"}), 400
# Get optional parameters
guidance_scale = float(request.form.get('guidance_scale', 3.0))
num_inference_steps = int(request.form.get('num_inference_steps', 64))
output_format = request.form.get('output_format', 'obj').lower()
# Validate output format
if output_format not in ['obj', 'glb']:
return jsonify({"error": "Unsupported output format. Use 'obj' or 'glb'"}), 400
try:
# Process image
filename = secure_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
# Open image
image = Image.open(filepath).convert("RGB")
# Load model (lazy loading)
pipe = load_model()
# Generate 3D model
images = pipe(
image,
guidance_scale=guidance_scale,
num_inference_steps=num_inference_steps,
output_type="mesh",
).images
# Create unique output directory
output_id = str(uuid.uuid4())
output_dir = os.path.join(RESULTS_FOLDER, output_id)
os.makedirs(output_dir, exist_ok=True)
# Export to requested format
if output_format == 'obj':
obj_path = os.path.join(output_dir, "model.obj")
export_to_obj(images[0], obj_path)
# Create a zip file with OBJ and MTL
zip_path = os.path.join(output_dir, "model.zip")
with zipfile.ZipFile(zip_path, 'w') as zipf:
zipf.write(obj_path, arcname="model.obj")
mtl_path = os.path.join(output_dir, "model.mtl")
if os.path.exists(mtl_path):
zipf.write(mtl_path, arcname="model.mtl")
return send_file(zip_path, as_attachment=True, download_name="model.zip")
elif output_format == 'glb':
# For GLB format, we need to convert the mesh
from trimesh import Trimesh
mesh = images[0]
vertices = mesh.verts
faces = mesh.faces
# Create a trimesh object
trimesh_obj = Trimesh(vertices=vertices, faces=faces)
# Export as GLB
glb_path = os.path.join(output_dir, "model.glb")
trimesh_obj.export(glb_path)
return send_file(glb_path, as_attachment=True, download_name="model.glb")
except Exception as e:
# Enhanced error reporting with traceback
error_details = traceback.format_exc()
return jsonify({"error": str(e), "details": error_details}), 500
@app.route('/', methods=['GET'])
def index():
return """
<html>
<head>
<title>Image to 3D Model Converter</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
form { margin: 20px 0; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
label { display: block; margin: 10px 0 5px; }
input, select { margin-bottom: 10px; padding: 8px; width: 100%; }
button { background: #4CAF50; color: white; border: none; padding: 10px 15px; cursor: pointer; }
.api-info { background: #f5f5f5; padding: 15px; border-radius: 5px; }
pre { background: #eee; padding: 10px; overflow-x: auto; }
</style>
</head>
<body>
<h1>Image to 3D Model Converter</h1>
<form action="/convert" method="post" enctype="multipart/form-data">
<label for="image">Upload Image:</label>
<input type="file" id="image" name="image" accept=".png,.jpg,.jpeg" required>
<label for="guidance_scale">Guidance Scale (1.0-5.0):</label>
<input type="number" id="guidance_scale" name="guidance_scale" min="1.0" max="5.0" step="0.1" value="3.0">
<label for="num_inference_steps">Inference Steps (32-128):</label>
<input type="number" id="num_inference_steps" name="num_inference_steps" min="32" max="128" value="64">
<label for="output_format">Output Format:</label>
<select id="output_format" name="output_format">
<option value="obj">OBJ (for Unity)</option>
<option value="glb">GLB (for Three.js/Unreal)</option>
</select>
<button type="submit">Convert to 3D</button>
</form>
<div class="api-info">
<h2>API Documentation</h2>
<p>Endpoint: <code>/convert</code> (POST)</p>
<p>Parameters:</p>
<ul>
<li><code>image</code>: Image file (required)</li>
<li><code>guidance_scale</code>: Float between 1.0-5.0 (default: 3.0)</li>
<li><code>num_inference_steps</code>: Integer between 32-128 (default: 64)</li>
<li><code>output_format</code>: "obj" or "glb" (default: "obj")</li>
</ul>
<p>Example curl request:</p>
<pre>curl -X POST -F "image=@your_image.jpg" -F "output_format=obj" http://localhost:7860/convert -o model.zip</pre>
</div>
</body>
</html>
"""
if __name__ == '__main__':
# Use port 7860 which is standard for Hugging Face Spaces
port = int(os.environ.get('PORT', 7860))
app.run(host='0.0.0.0', port=port)
|