def export_model(self, export_format="both", progress=gr.Progress()): """Export model - supports point cloud, mesh, or both""" progress(0.90, desc="💾 Exporting 3D model...") try: # Find the latest model models_dir = os.path.join(self.output_dir, "models") if not os.path.exists(models_dir): return {"status": "error", "message": "No trained model found"} # Get the most recent model directory model_dirs = [d for d in os.listdir(models_dir) if os.path.isdir(os.path.join(models_dir, d))] if not model_dirs: return {"status": "error", "message": "No model directories found"} latest_model = sorted(model_dirs)[-1] model_path = os.path.join(models_dir, latest_model) config_path = os.path.join(model_path, "config.yml") if not os.path.exists(config_path): return {"status": "error", "message": "Model config not found"} exports = {} # Export textured mesh (like the Mantis example!) if export_format in ["mesh", "both"]: progress(0.90, desc="🎨 Exporting textured mesh...") output_mesh = os.path.join(self.output_dir, "textured_mesh.ply") # Use Poisson surface reconstruction to create mesh result = subprocess.run([ "ns-export", "poisson", "--load-config", config_path, "--output-dir", self.output_dir, "--target-num-faces", "500000", # 500k faces for good detail "--num-pixels-per-side", "2048", # 2K texture resolution "--normal-method", "model_output", "--save-point-cloud", "False" ], capture_output=True, text=True, timeout=900) if result.returncode == 0 and os.path.exists(output_mesh): file_size = os.path.getsize(output_mesh) / (1024 * 1024) exports["mesh"] = { "path": output_mesh, "size_mb": round(file_size, 2), "type": "Textured Mesh (Poisson)" } # Also try to export as OBJ with textures (more compatible) output_obj = os.path.join(self.output_dir, "textured_mesh.obj") result_obj = subprocess.run([ "ns-export", "poisson", "--load-config", config_path, "--output-dir", self.output_dir, "--target-num-faces", "500000", "--num-pixels-per-side", "2048", "--normal-method", "model_output", "--save-point-cloud", "False", "--output-format", "obj" ], capture_output=True, text=True, timeout=900) if result_obj.returncode == 0 and os.path.exists(output_obj): exports["mesh_obj"] = { "path": output_obj, "type": "OBJ with Texture" } # Export point cloud (your current method) if export_format in ["pointcloud", "both"]: progress(0.92, desc="☁️ Exporting point cloud...") output_ply = os.path.join(self.output_dir, "point_cloud.ply") result = subprocess.run([ "ns-export", "pointcloud", "--load-config", config_path, "--output-dir", self.output_dir, "--num-points", "1000000", "--remove-outliers", "True", "--use-bounding-box", "True" ], capture_output=True, text=True, timeout=600) if result.returncode == 0 and os.path.exists(output_ply): file_size = os.path.getsize(output_ply) / (1024 * 1024) exports["pointcloud"] = { "path": output_ply, "size_mb": round(file_size, 2), "type": "Point Cloud" } if exports: return {"status": "success", "exports": exports} # Fallback to COLMAP export colmap_sparse = os.path.join(self.colmap_dir, "sparse", "0") if os.path.exists(colmap_sparse): output_ply = os.path.join(self.output_dir, "colmap_points.ply") result = subprocess.run([ "colmap", "model_converter", "--input_path", colmap_sparse, "--output_path", output_ply, "--output_type", "PLY" ], capture_output=True, text=True, timeout=300) if os.path.exists(output_ply): file_size = os.path.getsize(output_ply) / (1024 * 1024) return { "status": "success", "exports": { "pointcloud": { "path": output_ply, "size_mb": round(file_size, 2), "type": "COLMAP Points" } } } return {"status": "error", "message": "All export methods failed"} except Exception as e: return {"status": "error", "message": str(e)}