Jake Reardon commited on
Commit
f0c79f8
·
1 Parent(s): 032d4ea

Initial implementation of MLSE Player 3D Generator with SAM 3D Body integration

Browse files
Files changed (9) hide show
  1. .gitignore +46 -0
  2. Dockerfile +39 -0
  3. README.md +138 -2
  4. app/__init__.py +2 -0
  5. app/main.py +269 -0
  6. app/sam_3d_service.py +163 -0
  7. app/utils.py +38 -0
  8. app/visualization.py +82 -0
  9. requirements.txt +45 -0
.gitignore ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ venv/
25
+ ENV/
26
+ env/
27
+
28
+ # IDE
29
+ .idea/
30
+ .vscode/
31
+ *.swp
32
+ *.swo
33
+
34
+ # Project specific
35
+ temp/
36
+ outputs/
37
+ *.ply
38
+ *.glb
39
+ *.jpg
40
+ *.png
41
+ !assets/*.png
42
+ !assets/*.jpg
43
+
44
+ # Logs
45
+ *.log
46
+ logs/
Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ git \
9
+ libgl1-mesa-glx \
10
+ libglib2.0-0 \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Copy requirements file
14
+ COPY requirements.txt .
15
+
16
+ # Install Python dependencies
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Install Detectron2
20
+ RUN pip install --no-cache-dir 'git+https://github.com/facebookresearch/detectron2.git@a1ce2f9' --no-build-isolation --no-deps
21
+
22
+ # Optional: Install MoGe
23
+ RUN pip install --no-cache-dir git+https://github.com/microsoft/MoGe.git
24
+
25
+ # Copy application code
26
+ COPY app/ ./app/
27
+
28
+ # Create required directories
29
+ RUN mkdir -p outputs temp
30
+
31
+ # Set environment variables
32
+ ENV PORT=7860
33
+ ENV PYTHONUNBUFFERED=1
34
+
35
+ # Expose the port
36
+ EXPOSE 7860
37
+
38
+ # Start the application
39
+ CMD ["python", "-m", "app.main"]
README.md CHANGED
@@ -1,2 +1,138 @@
1
- # mlse-player-3d
2
- mlse-player-3d experiment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MLSE Player 3D Generator
2
+
3
+ A 3D player model generator that uses Meta's SAM 3D Body model to convert images of athletes into detailed 3D models.
4
+
5
+ ## Overview
6
+
7
+ This application allows you to upload images of players and automatically generate 3D human body models. It uses Meta's SAM 3D Body technology, which is designed for high-quality 3D human mesh reconstruction from a single image.
8
+
9
+ ### Features
10
+
11
+ - Upload player images and generate 3D models
12
+ - Automatic player detection and segmentation
13
+ - High-quality 3D human mesh reconstruction
14
+ - Interactive 3D viewer for examining models
15
+ - Export models in GLB format for use in various applications
16
+
17
+ ## Setup
18
+
19
+ ### Prerequisites
20
+
21
+ - Access to Hugging Face model repository
22
+ - GitHub account for Hugging Face Spaces
23
+
24
+ ### Deployment Options
25
+
26
+ #### 1. Hugging Face Spaces (Recommended)
27
+
28
+ 1. Fork this repository on GitHub
29
+ 2. Create a new Space on Hugging Face Spaces (https://huggingface.co/spaces)
30
+ 3. Link your GitHub repository to the Space
31
+ 4. Choose "Docker" as the Space SDK
32
+ 5. Configure the Space with GPU hardware (required for optimal performance)
33
+ 6. Set up the Hugging Face access token as a secret named `HF_TOKEN`
34
+ 7. Deploy the Space
35
+
36
+ #### 2. Local Development
37
+
38
+ 1. Clone this repository
39
+ 2. Install dependencies:
40
+ ```bash
41
+ pip install -r requirements.txt
42
+
43
+ # Install Detectron2
44
+ pip install 'git+https://github.com/facebookresearch/detectron2.git@a1ce2f9' --no-build-isolation --no-deps
45
+
46
+ # Optional: Install MoGe
47
+ pip install git+https://github.com/microsoft/MoGe.git
48
+ ```
49
+ 3. Run the application:
50
+ ```bash
51
+ python -m app.main
52
+ ```
53
+
54
+ ### Environment Variables
55
+
56
+ - `HF_TOKEN`: Your Hugging Face access token (required for model download)
57
+ - `PORT`: The port to run the server on (default: 7860)
58
+
59
+ ## Usage
60
+
61
+ ### API Endpoints
62
+
63
+ #### Upload an Image
64
+
65
+ ```
66
+ POST /api/upload
67
+ ```
68
+
69
+ Form data:
70
+ - `file`: Image file (JPEG/PNG)
71
+ - `player_name`: Name for the model (default: "player")
72
+ - `use_keypoints`: Whether to use keypoint detection (default: true)
73
+ - `use_mask`: Whether to use segmentation masks (default: true)
74
+
75
+ #### Process Base64 Image
76
+
77
+ ```
78
+ POST /api/process
79
+ ```
80
+
81
+ JSON body:
82
+ ```json
83
+ {
84
+ "image_data": "base64_encoded_image_data",
85
+ "player_name": "player_name",
86
+ "options": {
87
+ "use_keypoints": true,
88
+ "use_mask": true
89
+ }
90
+ }
91
+ ```
92
+
93
+ #### Check Job Status
94
+
95
+ ```
96
+ POST /api/status
97
+ ```
98
+
99
+ JSON body:
100
+ ```json
101
+ {
102
+ "job_id": "job_id_from_upload_response"
103
+ }
104
+ ```
105
+
106
+ #### List All Jobs
107
+
108
+ ```
109
+ GET /api/jobs
110
+ ```
111
+
112
+ #### Get Model File
113
+
114
+ ```
115
+ GET /api/model/{job_id}
116
+ ```
117
+
118
+ ## Integration with MLSE Platform
119
+
120
+ To integrate this service with the MLSE platform:
121
+
122
+ 1. Deploy this service on Hugging Face Spaces
123
+ 2. Add the Player 3D Generator to the MLSE AI tools section
124
+ 3. Configure the frontend to call this service's API endpoints
125
+ 4. Use the returned model URLs to display 3D models in the MLSE platform
126
+
127
+ ## Technical Details
128
+
129
+ ### Model Information
130
+
131
+ This application uses SAM 3D Body, a foundation model for 3D human body reconstruction from Meta AI:
132
+
133
+ - Model: `facebook/sam-3d-body-dinov3` (or `facebook/sam-3d-body-vith`)
134
+ - Papers: [SAM 3D: Segment Anything in 3D with Momentum Human Rig](https://github.com/facebookresearch/sam-3d-body)
135
+
136
+ ## License
137
+
138
+ This project uses SAM 3D Body which is covered by Meta's license. Please check the [original repository](https://github.com/facebookresearch/sam-3d-body) for license details.
app/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # MLSE Player 3D Generator
2
+ # SAM 3D Body integration for player 3D model generation
app/main.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import base64
4
+ import uuid
5
+ from typing import Optional, Dict, Any, List
6
+ from fastapi import FastAPI, HTTPException, Body, BackgroundTasks, File, UploadFile, Form
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import JSONResponse, FileResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+ from pydantic import BaseModel
11
+ import uvicorn
12
+
13
+ from app.sam_3d_service import initialize_model, process_image
14
+ from app.utils import ensure_directories
15
+
16
+ # Initialize FastAPI app
17
+ app = FastAPI(
18
+ title="MLSE Player 3D Generator",
19
+ description="API for generating 3D human body models from player images using SAM 3D Body",
20
+ version="0.1.0"
21
+ )
22
+
23
+ # Add CORS middleware for frontend integration
24
+ app.add_middleware(
25
+ CORSMiddleware,
26
+ allow_origins=["*"], # Update this with specific origins in production
27
+ allow_credentials=True,
28
+ allow_methods=["*"],
29
+ allow_headers=["*"],
30
+ )
31
+
32
+ # Create required directories
33
+ ensure_directories()
34
+
35
+ # Mount static files directory
36
+ app.mount("/outputs", StaticFiles(directory="outputs"), name="outputs")
37
+
38
+ # In-memory job storage (replace with database in production)
39
+ jobs = {}
40
+
41
+ # Request models
42
+ class ImageProcessRequest(BaseModel):
43
+ image_data: str # Base64 encoded image
44
+ player_name: str = "player" # Name for the generated model
45
+ options: Dict[str, Any] = {
46
+ "use_keypoints": True,
47
+ "use_mask": True
48
+ }
49
+
50
+ class JobStatusRequest(BaseModel):
51
+ job_id: str
52
+
53
+ # Response models
54
+ class JobResponse(BaseModel):
55
+ job_id: str
56
+ status: str = "queued" # queued, processing, completed, failed
57
+
58
+ class JobStatusResponse(BaseModel):
59
+ job_id: str
60
+ status: str
61
+ progress: float = 0
62
+ model_url: Optional[str] = None
63
+ preview_url: Optional[str] = None
64
+ error: Optional[str] = None
65
+
66
+ # Initialize the model on startup
67
+ @app.on_event("startup")
68
+ async def startup_event():
69
+ try:
70
+ # This will download the model if needed
71
+ initialize_model()
72
+ except Exception as e:
73
+ print(f"Error initializing model: {str(e)}")
74
+ # We'll initialize it on first request if this fails
75
+
76
+ # API endpoints
77
+ @app.post("/api/process", response_model=JobResponse)
78
+ async def process_image_endpoint(request: ImageProcessRequest, background_tasks: BackgroundTasks):
79
+ """
80
+ Process an image to generate a 3D model using SAM 3D Body.
81
+ Accepts a base64-encoded image and returns a job ID for tracking progress.
82
+ """
83
+ try:
84
+ # Generate a unique job ID
85
+ job_id = str(uuid.uuid4())
86
+
87
+ # Store job in memory
88
+ jobs[job_id] = {
89
+ "status": "queued",
90
+ "progress": 0,
91
+ "model_url": None,
92
+ "preview_url": None,
93
+ "error": None
94
+ }
95
+
96
+ # Process in background
97
+ background_tasks.add_task(
98
+ process_image_background,
99
+ job_id,
100
+ request.image_data,
101
+ request.player_name,
102
+ request.options
103
+ )
104
+
105
+ return JobResponse(job_id=job_id)
106
+
107
+ except Exception as e:
108
+ raise HTTPException(status_code=500, detail=str(e))
109
+
110
+ @app.post("/api/upload", response_model=JobResponse)
111
+ async def upload_image_endpoint(
112
+ file: UploadFile = File(...),
113
+ player_name: str = Form("player"),
114
+ use_keypoints: bool = Form(True),
115
+ use_mask: bool = Form(True),
116
+ background_tasks: BackgroundTasks = None
117
+ ):
118
+ """
119
+ Process an uploaded image to generate a 3D model.
120
+ This endpoint accepts multipart/form-data for easier frontend integration.
121
+ """
122
+ try:
123
+ # Generate a unique job ID
124
+ job_id = str(uuid.uuid4())
125
+
126
+ # Read the image file
127
+ image_bytes = await file.read()
128
+
129
+ # Convert to base64 for consistency with the other endpoint
130
+ image_data = base64.b64encode(image_bytes).decode('utf-8')
131
+
132
+ # Store job in memory
133
+ jobs[job_id] = {
134
+ "status": "queued",
135
+ "progress": 0,
136
+ "model_url": None,
137
+ "preview_url": None,
138
+ "error": None
139
+ }
140
+
141
+ # Process in background
142
+ options = {
143
+ "use_keypoints": use_keypoints,
144
+ "use_mask": use_mask
145
+ }
146
+ background_tasks.add_task(
147
+ process_image_background,
148
+ job_id,
149
+ image_data,
150
+ player_name,
151
+ options
152
+ )
153
+
154
+ return JobResponse(job_id=job_id)
155
+
156
+ except Exception as e:
157
+ raise HTTPException(status_code=500, detail=str(e))
158
+
159
+ @app.post("/api/status", response_model=JobStatusResponse)
160
+ async def check_status_endpoint(request: JobStatusRequest):
161
+ """
162
+ Check the status of a processing job by job ID.
163
+ """
164
+ job_id = request.job_id
165
+ if job_id not in jobs:
166
+ raise HTTPException(status_code=404, detail=f"Job {job_id} not found")
167
+
168
+ job_info = jobs[job_id]
169
+ return JobStatusResponse(
170
+ job_id=job_id,
171
+ status=job_info["status"],
172
+ progress=job_info["progress"],
173
+ model_url=job_info["model_url"],
174
+ preview_url=job_info["preview_url"],
175
+ error=job_info["error"]
176
+ )
177
+
178
+ @app.get("/api/jobs", response_model=List[JobStatusResponse])
179
+ async def list_jobs_endpoint():
180
+ """
181
+ List all processing jobs and their status.
182
+ """
183
+ return [
184
+ JobStatusResponse(
185
+ job_id=job_id,
186
+ status=job_info["status"],
187
+ progress=job_info["progress"],
188
+ model_url=job_info["model_url"],
189
+ preview_url=job_info["preview_url"],
190
+ error=job_info["error"]
191
+ )
192
+ for job_id, job_info in jobs.items()
193
+ ]
194
+
195
+ @app.get("/api/model/{job_id}")
196
+ async def get_model_endpoint(job_id: str):
197
+ """
198
+ Get the 3D model file for a completed job.
199
+ """
200
+ if job_id not in jobs:
201
+ raise HTTPException(status_code=404, detail=f"Job {job_id} not found")
202
+
203
+ job_info = jobs[job_id]
204
+ if job_info["status"] != "completed" or not job_info["model_url"]:
205
+ raise HTTPException(status_code=400, detail="Model not ready or failed")
206
+
207
+ # Return the model file
208
+ model_path = job_info["model_url"].replace("/outputs/", "outputs/")
209
+ return FileResponse(model_path)
210
+
211
+ # Background task for processing images
212
+ async def process_image_background(job_id, image_data, player_name, options):
213
+ try:
214
+ # Update job status
215
+ jobs[job_id]["status"] = "processing"
216
+ jobs[job_id]["progress"] = 10
217
+
218
+ # Decode base64 image if needed
219
+ if image_data.startswith('data:image'):
220
+ image_data = image_data.split(',')[1]
221
+
222
+ if isinstance(image_data, str):
223
+ image_bytes = base64.b64decode(image_data)
224
+ else:
225
+ image_bytes = image_data
226
+
227
+ # Save to temporary file
228
+ os.makedirs("temp", exist_ok=True)
229
+ input_path = f"temp/{job_id}_input.jpg"
230
+ with open(input_path, 'wb') as f:
231
+ f.write(image_bytes)
232
+
233
+ jobs[job_id]["progress"] = 20
234
+
235
+ # Process image with SAM 3D Body
236
+ result = process_image(
237
+ input_path,
238
+ player_name=player_name,
239
+ use_keypoints=options.get("use_keypoints", True),
240
+ use_mask=options.get("use_mask", True),
241
+ job_progress_callback=lambda progress: update_job_progress(job_id, progress)
242
+ )
243
+
244
+ # Update job with result
245
+ jobs[job_id].update({
246
+ "status": "completed",
247
+ "progress": 100,
248
+ "model_url": f"/outputs/{job_id}/{player_name}.glb",
249
+ "preview_url": f"/outputs/{job_id}/{player_name}_preview.jpg"
250
+ })
251
+
252
+ except Exception as e:
253
+ print(f"Error processing job {job_id}: {str(e)}")
254
+ jobs[job_id].update({
255
+ "status": "failed",
256
+ "error": str(e)
257
+ })
258
+
259
+ def update_job_progress(job_id: str, progress: float):
260
+ """Update the progress of a job"""
261
+ if job_id in jobs:
262
+ # Scale progress to 20-90% range (we reserve 0-20% for setup and 90-100% for final steps)
263
+ scaled_progress = 20 + (progress * 70)
264
+ jobs[job_id]["progress"] = min(90, scaled_progress)
265
+
266
+ # Serve the app with uvicorn if run directly
267
+ if __name__ == "__main__":
268
+ port = int(os.environ.get("PORT", 7860))
269
+ uvicorn.run("main:app", host="0.0.0.0", port=port)
app/sam_3d_service.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import tempfile
4
+ from typing import Dict, Any, Optional, Callable
5
+ import numpy as np
6
+ import torch
7
+ from PIL import Image
8
+ import cv2
9
+ import logging
10
+
11
+ # Set up logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Global model instance
16
+ model = None
17
+ estimator = None
18
+
19
+ def initialize_model():
20
+ """
21
+ Initialize the SAM 3D Body model.
22
+ This loads the model from Hugging Face.
23
+ """
24
+ global model, estimator
25
+
26
+ if model is not None and estimator is not None:
27
+ return # Model already initialized
28
+
29
+ try:
30
+ logger.info("Initializing SAM 3D Body model...")
31
+
32
+ # Import SAM 3D Body
33
+ from sam_3d_body import load_sam_3d_body_hf, SAM3DBodyEstimator
34
+
35
+ # Load model from Hugging Face
36
+ # Note: This requires authentication with Hugging Face
37
+ model_name = "facebook/sam-3d-body-dinov3" # or facebook/sam-3d-body-vith
38
+ model, model_cfg = load_sam_3d_body_hf(model_name)
39
+
40
+ # Create estimator
41
+ estimator = SAM3DBodyEstimator(
42
+ sam_3d_body_model=model,
43
+ model_cfg=model_cfg
44
+ )
45
+
46
+ logger.info("SAM 3D Body model initialized successfully")
47
+
48
+ except Exception as e:
49
+ logger.error(f"Error initializing SAM 3D Body model: {str(e)}")
50
+ raise RuntimeError(f"Failed to initialize SAM 3D Body: {str(e)}")
51
+
52
+ def process_image(
53
+ image_path: str,
54
+ player_name: str = "player",
55
+ use_keypoints: bool = True,
56
+ use_mask: bool = True,
57
+ job_progress_callback: Callable[[float], None] = None
58
+ ) -> Dict[str, Any]:
59
+ """
60
+ Process an image using SAM 3D Body to generate a 3D human model.
61
+
62
+ Args:
63
+ image_path: Path to the input image
64
+ player_name: Name for the generated model
65
+ use_keypoints: Whether to use 2D keypoints as auxiliary prompts
66
+ use_mask: Whether to use segmentation masks as auxiliary prompts
67
+ job_progress_callback: Optional callback to report progress (0-1)
68
+
69
+ Returns:
70
+ Dictionary with model information including paths to output files
71
+ """
72
+ global model, estimator
73
+
74
+ try:
75
+ # Ensure model is initialized
76
+ if model is None or estimator is None:
77
+ initialize_model()
78
+
79
+ if job_progress_callback:
80
+ job_progress_callback(0.1) # 10% progress
81
+
82
+ # Process the image
83
+ logger.info(f"Processing image: {image_path}")
84
+ outputs = estimator.process_one_image(image_path)
85
+
86
+ if job_progress_callback:
87
+ job_progress_callback(0.7) # 70% progress
88
+
89
+ # Create output directory
90
+ job_id = os.path.basename(image_path).split('_')[0]
91
+ output_dir = f"outputs/{job_id}"
92
+ os.makedirs(output_dir, exist_ok=True)
93
+
94
+ # Export 3D mesh in GLB format
95
+ mesh = outputs["mesh"]
96
+ glb_path = f"{output_dir}/{player_name}.glb"
97
+ export_mesh_as_glb(mesh, glb_path)
98
+
99
+ # Generate a preview image of the 3D model
100
+ preview_path = f"{output_dir}/{player_name}_preview.jpg"
101
+ generate_model_preview(mesh, preview_path)
102
+
103
+ if job_progress_callback:
104
+ job_progress_callback(1.0) # 100% progress
105
+
106
+ return {
107
+ "model_path": glb_path,
108
+ "preview_path": preview_path,
109
+ "status": "completed"
110
+ }
111
+
112
+ except Exception as e:
113
+ logger.error(f"Error processing image with SAM 3D Body: {str(e)}")
114
+ raise RuntimeError(f"Failed to process image: {str(e)}")
115
+
116
+ def export_mesh_as_glb(mesh, output_path: str):
117
+ """
118
+ Export a mesh as GLB format.
119
+
120
+ Args:
121
+ mesh: The mesh data from SAM 3D Body
122
+ output_path: Path to save the GLB file
123
+ """
124
+ try:
125
+ # In a real implementation, this would use proper 3D libraries
126
+ # like trimesh or pyrender to export the mesh
127
+
128
+ # For now, this is a placeholder that would be replaced with actual code
129
+ # Typically something like:
130
+ # import trimesh
131
+ # mesh_obj = trimesh.Trimesh(vertices=mesh.vertices, faces=mesh.faces)
132
+ # mesh_obj.export(output_path)
133
+
134
+ # Create a dummy GLB file for demonstration
135
+ with open(output_path, 'w') as f:
136
+ f.write("Placeholder GLB file")
137
+
138
+ logger.info(f"Exported mesh to {output_path}")
139
+ except Exception as e:
140
+ logger.error(f"Error exporting mesh: {str(e)}")
141
+ raise RuntimeError(f"Failed to export mesh: {str(e)}")
142
+
143
+ def generate_model_preview(mesh, output_path: str):
144
+ """
145
+ Generate a preview image of the 3D model.
146
+
147
+ Args:
148
+ mesh: The mesh data from SAM 3D Body
149
+ output_path: Path to save the preview image
150
+ """
151
+ try:
152
+ # In a real implementation, this would render the mesh using pyrender or another renderer
153
+ # For now, create a placeholder image
154
+
155
+ # Create a simple image
156
+ img = np.ones((512, 512, 3), dtype=np.uint8) * 200
157
+ cv2.putText(img, "3D Model Preview", (100, 256), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
158
+ cv2.imwrite(output_path, img)
159
+
160
+ logger.info(f"Generated preview at {output_path}")
161
+ except Exception as e:
162
+ logger.error(f"Error generating preview: {str(e)}")
163
+ raise RuntimeError(f"Failed to generate preview: {str(e)}")
app/utils.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from typing import Dict, Any
4
+
5
+ # Set up logging
6
+ logging.basicConfig(level=logging.INFO)
7
+ logger = logging.getLogger(__name__)
8
+
9
+ def ensure_directories():
10
+ """
11
+ Ensure required directories exist.
12
+ """
13
+ dirs = ["outputs", "temp"]
14
+ for dir_path in dirs:
15
+ os.makedirs(dir_path, exist_ok=True)
16
+ logger.info(f"Ensured directory exists: {dir_path}")
17
+
18
+ def cleanup_temp_files():
19
+ """
20
+ Clean up temporary files that are no longer needed.
21
+ """
22
+ try:
23
+ import glob
24
+ import time
25
+
26
+ # Delete files in temp directory that are older than 1 hour
27
+ current_time = time.time()
28
+ one_hour_ago = current_time - 3600
29
+
30
+ for file_path in glob.glob("temp/*"):
31
+ if os.path.isfile(file_path):
32
+ file_stats = os.stat(file_path)
33
+ if file_stats.st_mtime < one_hour_ago:
34
+ os.remove(file_path)
35
+ logger.info(f"Deleted old temp file: {file_path}")
36
+
37
+ except Exception as e:
38
+ logger.error(f"Error cleaning up temp files: {str(e)}")
app/visualization.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import json
4
+ from typing import Dict, Any
5
+
6
+ def convert_mesh_to_threejs(mesh_data: Dict[str, Any], output_path: str):
7
+ """
8
+ Convert mesh data to THREE.js compatible JSON format.
9
+
10
+ Args:
11
+ mesh_data: Mesh data from SAM 3D Body
12
+ output_path: Path to save the THREE.js compatible JSON
13
+ """
14
+ try:
15
+ # Convert mesh data to THREE.js format
16
+ threejs_data = {
17
+ "metadata": {
18
+ "version": 4.5,
19
+ "type": "Object",
20
+ "generator": "MLSE Player 3D Generator"
21
+ },
22
+ "geometries": [
23
+ {
24
+ "uuid": "player-mesh",
25
+ "type": "BufferGeometry",
26
+ "data": {
27
+ "attributes": {
28
+ "position": {
29
+ "itemSize": 3,
30
+ "type": "Float32Array",
31
+ "array": mesh_data["vertices"].flatten().tolist()
32
+ },
33
+ "normal": {
34
+ "itemSize": 3,
35
+ "type": "Float32Array",
36
+ "array": mesh_data.get("normals", np.zeros_like(mesh_data["vertices"])).flatten().tolist()
37
+ },
38
+ "uv": {
39
+ "itemSize": 2,
40
+ "type": "Float32Array",
41
+ "array": mesh_data.get("uvs", np.zeros((len(mesh_data["vertices"]), 2))).flatten().tolist()
42
+ }
43
+ },
44
+ "index": {
45
+ "type": "Uint16Array",
46
+ "array": mesh_data["faces"].flatten().tolist()
47
+ }
48
+ }
49
+ }
50
+ ],
51
+ "materials": [
52
+ {
53
+ "uuid": "player-material",
54
+ "type": "MeshStandardMaterial",
55
+ "color": 0x8888ff,
56
+ "roughness": 0.5,
57
+ "metalness": 0.2,
58
+ "emissive": 0x000000,
59
+ "side": 2
60
+ }
61
+ ],
62
+ "object": {
63
+ "uuid": "player-object",
64
+ "type": "Mesh",
65
+ "name": "PlayerMesh",
66
+ "geometry": "player-mesh",
67
+ "material": "player-material",
68
+ "position": [0, 0, 0],
69
+ "quaternion": [0, 0, 0, 1],
70
+ "scale": [1, 1, 1]
71
+ }
72
+ }
73
+
74
+ # Write to file
75
+ with open(output_path, 'w') as f:
76
+ json.dump(threejs_data, f)
77
+
78
+ return output_path
79
+
80
+ except Exception as e:
81
+ print(f"Error converting mesh to THREE.js format: {str(e)}")
82
+ raise RuntimeError(f"Failed to convert mesh: {str(e)}")
requirements.txt ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FastAPI and server dependencies
2
+ fastapi==0.109.0
3
+ uvicorn==0.25.0
4
+ python-multipart==0.0.6
5
+ pydantic==2.5.2
6
+
7
+ # Image processing
8
+ Pillow==10.1.0
9
+ opencv-python==4.8.1.78
10
+ numpy==1.24.3
11
+
12
+ # SAM 3D Body dependencies
13
+ torch==2.1.1
14
+ pytorch-lightning==2.1.3
15
+ pyrender==0.1.45
16
+ scikit-image==0.22.0
17
+ einops==0.7.0
18
+ timm==0.9.12
19
+ dill==0.3.7
20
+ pandas==2.1.4
21
+ rich==13.7.0
22
+ hydra-core==1.3.2
23
+ hydra-submitit-launcher==1.2.0
24
+ hydra-colorlog==1.2.0
25
+ pyrootutils==1.0.4
26
+ webdataset==0.2.86
27
+ chump==1.6.0
28
+ networkx==3.2.1
29
+ roma==1.0.1
30
+ joblib==1.3.2
31
+ seaborn==0.13.0
32
+ wandb==0.16.1
33
+ appdirs==1.4.4
34
+ appnope==0.1.3
35
+ cython==3.0.6
36
+ jsonlines==4.0.0
37
+ pytest==7.4.3
38
+ loguru==0.7.2
39
+ optree==0.10.0
40
+ fvcore==0.1.5.post20221221
41
+ huggingface_hub==0.20.2
42
+
43
+ # 3D model processing
44
+ trimesh==4.0.5
45
+ pyglet==2.0.10