@hybridlabor-api/bdb-hardware-pcb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/config/mcp/antigravity.json +22 -0
- package/config/mcp/claude.json +22 -0
- package/config/mcp/codex.toml +25 -0
- package/docs/adr/ADR-001-KICAD-OPENSCAD-MCP-STRATEGY.md +334 -0
- package/docs/review_documentation.md +121 -0
- package/installer.js +358 -0
- package/mcp_servers/kicad-mcp-server/.env.example +22 -0
- package/mcp_servers/kicad-mcp-server/.github/workflows/ci.yml +36 -0
- package/mcp_servers/kicad-mcp-server/CLAUDE.md +487 -0
- package/mcp_servers/kicad-mcp-server/README.md +316 -0
- package/mcp_servers/kicad-mcp-server/docs/DEVICE_TREE.md +416 -0
- package/mcp_servers/kicad-mcp-server/docs/INSTALLATION.md +332 -0
- package/mcp_servers/kicad-mcp-server/docs/PIN_ANALYSIS.md +332 -0
- package/mcp_servers/kicad-mcp-server/docs/README.md +240 -0
- package/mcp_servers/kicad-mcp-server/docs/TESTING.md +613 -0
- package/mcp_servers/kicad-mcp-server/docs/VALIDATION.md +268 -0
- package/mcp_servers/kicad-mcp-server/pyproject.toml +96 -0
- package/mcp_servers/kicad-mcp-server/requirements-dev.txt +16 -0
- package/mcp_servers/kicad-mcp-server/requirements-test.txt +24 -0
- package/mcp_servers/kicad-mcp-server/requirements.txt +13 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__init__.py +3 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__main__.py +17 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/config.py +46 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/types.py +87 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/netlist_parser.py +234 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser.py +375 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser_kicad.py +327 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/schematic_parser.py +902 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/server.py +71 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/arduino/connectivity_test.cpp.j2 +189 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/atmega.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/esp32.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/nrf52.dts.j2 +77 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/stm32f4.dts.j2 +89 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/esp_idf/test_suite.c.j2 +340 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/pytest/test_connectivity.py.j2 +147 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/st_hal/hal_test.c.j2 +313 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_gpio_test.py.j2 +99 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_i2c_test.py.j2 +117 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_pinmux_test.py.j2 +43 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_spi_test.py.j2 +94 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_gpio_test.c.j2 +113 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_i2c_test.c.j2 +101 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_spi_test.c.j2 +94 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/unittest/test_schematic.py.j2 +172 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/__init__.py +36 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/device_tree.py +1187 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/hierarchical_analysis.py +211 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/netlist.py +320 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/parts_registry.py +142 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb.py +955 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb_layout.py +308 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pin_analysis.py +765 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/project.py +196 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic.py +319 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_editor.py +674 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_search.py +158 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/validation.py +866 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/visualization.py +225 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/file_handlers.py +65 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_cli.py +103 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_version.py +103 -0
- package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/parts_registry.py +197 -0
- package/mcp_servers/kicad-mcp-server/tests/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/examples/ESP32S3_TEST.md +219 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/README.md +65 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/example_pcb.kicad_pcb +177 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/example_schematic.kicad_sch +145 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/child.kicad_sch +24 -0
- package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/root.kicad_sch +38 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/__init__.py +1 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_hierarchical_labels.py +195 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_kicad_cli.py +116 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_netlist_cache_path.py +29 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic.py +189 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic_hierarchy.py +69 -0
- package/mcp_servers/kicad-mcp-server/tests/test_tools/test_visualization.py +136 -0
- package/mcp_servers/kicad-mcp-server/uv.lock +2873 -0
- package/mcp_servers/openscad-mcp-server/.dockerignore +9 -0
- package/mcp_servers/openscad-mcp-server/.github/workflows/test.yml +40 -0
- package/mcp_servers/openscad-mcp-server/Dockerfile +29 -0
- package/mcp_servers/openscad-mcp-server/LICENSE +21 -0
- package/mcp_servers/openscad-mcp-server/README.md +154 -0
- package/mcp_servers/openscad-mcp-server/docs/audit.md +56 -0
- package/mcp_servers/openscad-mcp-server/docs/docker.md +66 -0
- package/mcp_servers/openscad-mcp-server/docs/issue-followup.md +19 -0
- package/mcp_servers/openscad-mcp-server/docs/jetson.md +17 -0
- package/mcp_servers/openscad-mcp-server/glama.json +4 -0
- package/mcp_servers/openscad-mcp-server/legacy/README.md +15 -0
- package/mcp_servers/openscad-mcp-server/legacy/README.original.md +294 -0
- package/mcp_servers/openscad-mcp-server/legacy/implementation_plan.md +100 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/download_sam2_checkpoint.py +115 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/ai/sam_segmentation.py +209 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/models/threestudio_generator.py +231 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/src/workflow/image_to_model_pipeline.py +260 -0
- package/mcp_servers/openscad-mcp-server/legacy/old/test_sam2_segmentation.py +96 -0
- package/mcp_servers/openscad-mcp-server/legacy/requirements.txt +57 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/README.md +39 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/ai-driven-code-generation.md +122 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/export-formats.md +76 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/ai/ai_service.py.md +51 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/main.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/models/code_generator.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/nlp/parameter_extractor.py.md +63 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/ai/natural-language-processing.md +78 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/nlp/parameter-extraction.md +173 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/export-formats.md +91 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/openscad-basics.md +66 -0
- package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/primitive-testing.md +79 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/ai_service.py +257 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/gemini_api.py +161 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/ai/venice_api.py +203 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/config.py +121 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main.py +1456 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main.py.new +404 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/main_remote.py +401 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/code_generator.py +321 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/cuda_mvs.py +209 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/models/scad_templates/basic_shapes.scad +144 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/nlp/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/nlp/parameter_extractor.py +388 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/wrapper.py +418 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/__init__.py +1 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/printer_discovery.py +471 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/connection_manager.py +537 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_client.py +435 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_server.py +787 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/remote/error_handling.py +415 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/primitive_tester.py +203 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/testing/test_primitives.py +98 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/__init__.py +1 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/cad_exporter.py +241 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/format_validator.py +206 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_exporter.py +140 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_repair.py +91 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_validator.py +123 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/__init__.py +0 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/headless_renderer.py +52 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/renderer.py +177 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/visualization/web_interface.py +639 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/workflow/image_approval.py +148 -0
- package/mcp_servers/openscad-mcp-server/legacy/src/workflow/multi_view_to_model_pipeline.py +338 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_complete_workflow.py +374 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_cuda_mvs.py +191 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_gemini_api.py +168 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_approval.py +192 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_approval_workflow.py +251 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_image_to_model_pipeline.py +145 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_model_selection.py +41 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_multi_view_pipeline.py +290 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_primitives.sh +13 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_rabbit_direct.py +71 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_remote_cuda_mvs.py +283 -0
- package/mcp_servers/openscad-mcp-server/legacy/test_venice_example.py +69 -0
- package/mcp_servers/openscad-mcp-server/pyproject.toml +33 -0
- package/mcp_servers/openscad-mcp-server/requirements.txt +2 -0
- package/mcp_servers/openscad-mcp-server/scad/simple_cube.scad +2 -0
- package/mcp_servers/openscad-mcp-server/scripts/test_docker.py +220 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__init__.py +3 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__main__.py +3 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/engine.py +94 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/geometry.py +242 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/server.py +245 -0
- package/mcp_servers/openscad-mcp-server/src/openscad_mcp/service.py +190 -0
- package/mcp_servers/openscad-mcp-server/tests/conftest.py +17 -0
- package/mcp_servers/openscad-mcp-server/tests/test_engine.py +37 -0
- package/mcp_servers/openscad-mcp-server/tests/test_geometry.py +106 -0
- package/mcp_servers/openscad-mcp-server/tests/test_integration.py +172 -0
- package/mcp_servers/openscad-mcp-server/tests/test_transports.py +314 -0
- package/mcp_servers/openscad-mcp-server/uv.lock +1123 -0
- package/package.json +44 -0
- package/scripts/install_mcps.sh +86 -0
- package/scripts/openscad_wrapper.sh +62 -0
- package/scripts/run_kicad_mcp.sh +25 -0
- package/scripts/run_openscad_mcp.sh +41 -0
- package/scripts/test_mcp_connection.py +626 -0
- package/scripts/test_mcp_connection.sh +168 -0
- package/skills/code-first-hardware-design/SKILL.md +260 -0
- package/skills/pcb-constraint-definition/SKILL.md +236 -0
- package/skills/pcb-layout-routing-automation/SKILL.md +153 -0
- package/skills/pcb-validation-dfm-signoff/SKILL.md +194 -0
- package/skills/schematic-datasheet-analysis/SKILL.md +202 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Server for remote CUDA Multi-View Stereo processing.
|
|
3
|
+
|
|
4
|
+
This module provides a server implementation that can be deployed on a machine
|
|
5
|
+
with CUDA capabilities to process multi-view images into 3D models remotely.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import json
|
|
11
|
+
import uuid
|
|
12
|
+
import logging
|
|
13
|
+
import argparse
|
|
14
|
+
import tempfile
|
|
15
|
+
import subprocess
|
|
16
|
+
from typing import Dict, List, Optional, Any, Union
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import shutil
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
from fastapi import FastAPI, File, UploadFile, Form, HTTPException, BackgroundTasks, Depends
|
|
22
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
23
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
24
|
+
from fastapi.security import APIKeyHeader
|
|
25
|
+
import uvicorn
|
|
26
|
+
from pydantic import BaseModel, Field
|
|
27
|
+
from zeroconf import ServiceInfo, Zeroconf
|
|
28
|
+
|
|
29
|
+
# Configure logging
|
|
30
|
+
logging.basicConfig(level=logging.INFO)
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
# Constants
|
|
34
|
+
DEFAULT_PORT = 8765
|
|
35
|
+
DEFAULT_HOST = "0.0.0.0"
|
|
36
|
+
DEFAULT_CUDA_MVS_PATH = "/opt/cuda-multi-view-stereo"
|
|
37
|
+
DEFAULT_OUTPUT_DIR = "output"
|
|
38
|
+
DEFAULT_MAX_JOBS = 5
|
|
39
|
+
DEFAULT_MAX_IMAGES_PER_JOB = 50
|
|
40
|
+
DEFAULT_JOB_TIMEOUT = 3600 # 1 hour
|
|
41
|
+
|
|
42
|
+
# Models
|
|
43
|
+
class JobStatus(BaseModel):
|
|
44
|
+
"""Job status model."""
|
|
45
|
+
job_id: str
|
|
46
|
+
status: str = "created" # created, uploading, processing, completed, failed
|
|
47
|
+
created_at: float = Field(default_factory=time.time)
|
|
48
|
+
updated_at: float = Field(default_factory=time.time)
|
|
49
|
+
num_images: int = 0
|
|
50
|
+
images_uploaded: int = 0
|
|
51
|
+
progress: float = 0.0
|
|
52
|
+
error_message: Optional[str] = None
|
|
53
|
+
model_id: Optional[str] = None
|
|
54
|
+
output_dir: Optional[str] = None
|
|
55
|
+
point_cloud_file: Optional[str] = None
|
|
56
|
+
obj_file: Optional[str] = None
|
|
57
|
+
processing_time: Optional[float] = None
|
|
58
|
+
|
|
59
|
+
class ServerConfig(BaseModel):
|
|
60
|
+
"""Server configuration model."""
|
|
61
|
+
cuda_mvs_path: str = DEFAULT_CUDA_MVS_PATH
|
|
62
|
+
output_dir: str = DEFAULT_OUTPUT_DIR
|
|
63
|
+
max_jobs: int = DEFAULT_MAX_JOBS
|
|
64
|
+
max_images_per_job: int = DEFAULT_MAX_IMAGES_PER_JOB
|
|
65
|
+
job_timeout: int = DEFAULT_JOB_TIMEOUT
|
|
66
|
+
api_key: Optional[str] = None
|
|
67
|
+
server_name: str = "CUDA MVS Server"
|
|
68
|
+
advertise_service: bool = True
|
|
69
|
+
gpu_info: Optional[str] = None
|
|
70
|
+
|
|
71
|
+
class JobRequest(BaseModel):
|
|
72
|
+
"""Job creation request model."""
|
|
73
|
+
num_images: int
|
|
74
|
+
|
|
75
|
+
class ProcessRequest(BaseModel):
|
|
76
|
+
"""Process job request model."""
|
|
77
|
+
reconstruction_quality: str = "normal" # low, normal, high
|
|
78
|
+
output_formats: List[str] = ["obj", "ply"]
|
|
79
|
+
|
|
80
|
+
class ServerInfo(BaseModel):
|
|
81
|
+
"""Server information model."""
|
|
82
|
+
server_id: str
|
|
83
|
+
name: str
|
|
84
|
+
version: str = "1.0.0"
|
|
85
|
+
status: str = "running"
|
|
86
|
+
capabilities: Dict[str, Any]
|
|
87
|
+
jobs: Dict[str, Any]
|
|
88
|
+
uptime: float
|
|
89
|
+
|
|
90
|
+
# Server implementation
|
|
91
|
+
class CUDAMVSServer:
|
|
92
|
+
"""
|
|
93
|
+
Server for remote CUDA Multi-View Stereo processing.
|
|
94
|
+
|
|
95
|
+
This server:
|
|
96
|
+
1. Accepts image uploads
|
|
97
|
+
2. Processes images using CUDA MVS
|
|
98
|
+
3. Provides 3D model downloads
|
|
99
|
+
4. Advertises itself on the local network
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, config: ServerConfig):
|
|
103
|
+
"""
|
|
104
|
+
Initialize the CUDA MVS server.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
config: Server configuration
|
|
108
|
+
"""
|
|
109
|
+
self.config = config
|
|
110
|
+
self.app = FastAPI(title="CUDA MVS Server", description="Remote CUDA Multi-View Stereo processing server")
|
|
111
|
+
self.jobs: Dict[str, JobStatus] = {}
|
|
112
|
+
self.server_id = str(uuid.uuid4())
|
|
113
|
+
self.start_time = time.time()
|
|
114
|
+
self.zeroconf = None
|
|
115
|
+
|
|
116
|
+
# Create output directory
|
|
117
|
+
os.makedirs(config.output_dir, exist_ok=True)
|
|
118
|
+
|
|
119
|
+
# Configure CORS
|
|
120
|
+
self.app.add_middleware(
|
|
121
|
+
CORSMiddleware,
|
|
122
|
+
allow_origins=["*"],
|
|
123
|
+
allow_credentials=True,
|
|
124
|
+
allow_methods=["*"],
|
|
125
|
+
allow_headers=["*"],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Detect GPU info
|
|
129
|
+
self.detect_gpu_info()
|
|
130
|
+
|
|
131
|
+
# Register routes
|
|
132
|
+
self.register_routes()
|
|
133
|
+
|
|
134
|
+
# Advertise service if enabled
|
|
135
|
+
if config.advertise_service:
|
|
136
|
+
self.advertise_service()
|
|
137
|
+
|
|
138
|
+
def detect_gpu_info(self):
|
|
139
|
+
"""Detect GPU information."""
|
|
140
|
+
if not self.config.gpu_info:
|
|
141
|
+
try:
|
|
142
|
+
# Try to get GPU info using nvidia-smi
|
|
143
|
+
result = subprocess.run(
|
|
144
|
+
["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader"],
|
|
145
|
+
capture_output=True,
|
|
146
|
+
text=True,
|
|
147
|
+
check=True
|
|
148
|
+
)
|
|
149
|
+
self.config.gpu_info = result.stdout.strip()
|
|
150
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
151
|
+
# If nvidia-smi fails, try lspci
|
|
152
|
+
try:
|
|
153
|
+
result = subprocess.run(
|
|
154
|
+
["lspci", "-v", "|", "grep", "-i", "vga"],
|
|
155
|
+
capture_output=True,
|
|
156
|
+
text=True,
|
|
157
|
+
shell=True
|
|
158
|
+
)
|
|
159
|
+
self.config.gpu_info = result.stdout.strip()
|
|
160
|
+
except subprocess.SubprocessError:
|
|
161
|
+
self.config.gpu_info = "Unknown GPU"
|
|
162
|
+
|
|
163
|
+
def register_routes(self):
|
|
164
|
+
"""Register API routes."""
|
|
165
|
+
|
|
166
|
+
# Authentication dependency
|
|
167
|
+
api_key_header = APIKeyHeader(name="Authorization", auto_error=False)
|
|
168
|
+
|
|
169
|
+
async def verify_api_key(api_key: str = Depends(api_key_header)):
|
|
170
|
+
if self.config.api_key:
|
|
171
|
+
if not api_key:
|
|
172
|
+
raise HTTPException(status_code=401, detail="API key required")
|
|
173
|
+
|
|
174
|
+
# Check if the API key is in the format "Bearer <key>"
|
|
175
|
+
if api_key.startswith("Bearer "):
|
|
176
|
+
api_key = api_key[7:]
|
|
177
|
+
|
|
178
|
+
if api_key != self.config.api_key:
|
|
179
|
+
raise HTTPException(status_code=401, detail="Invalid API key")
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
# Status endpoint
|
|
183
|
+
@self.app.get("/api/status")
|
|
184
|
+
async def get_status():
|
|
185
|
+
return self.get_server_info()
|
|
186
|
+
|
|
187
|
+
# Job management endpoints
|
|
188
|
+
@self.app.post("/api/jobs", status_code=201, dependencies=[Depends(verify_api_key)])
|
|
189
|
+
async def create_job(job_request: JobRequest):
|
|
190
|
+
return self.create_job(job_request.num_images)
|
|
191
|
+
|
|
192
|
+
@self.app.get("/api/jobs", dependencies=[Depends(verify_api_key)])
|
|
193
|
+
async def list_jobs():
|
|
194
|
+
return {"jobs": self.jobs}
|
|
195
|
+
|
|
196
|
+
@self.app.get("/api/jobs/{job_id}", dependencies=[Depends(verify_api_key)])
|
|
197
|
+
async def get_job(job_id: str):
|
|
198
|
+
if job_id not in self.jobs:
|
|
199
|
+
raise HTTPException(status_code=404, detail="Job not found")
|
|
200
|
+
return self.jobs[job_id]
|
|
201
|
+
|
|
202
|
+
@self.app.delete("/api/jobs/{job_id}", dependencies=[Depends(verify_api_key)])
|
|
203
|
+
async def cancel_job(job_id: str):
|
|
204
|
+
if job_id not in self.jobs:
|
|
205
|
+
raise HTTPException(status_code=404, detail="Job not found")
|
|
206
|
+
|
|
207
|
+
# Cancel the job
|
|
208
|
+
job = self.jobs[job_id]
|
|
209
|
+
if job.status in ["created", "uploading", "processing"]:
|
|
210
|
+
job.status = "cancelled"
|
|
211
|
+
job.updated_at = time.time()
|
|
212
|
+
|
|
213
|
+
# Clean up job directory
|
|
214
|
+
job_dir = os.path.join(self.config.output_dir, job_id)
|
|
215
|
+
if os.path.exists(job_dir):
|
|
216
|
+
shutil.rmtree(job_dir)
|
|
217
|
+
|
|
218
|
+
return {"status": "success", "message": "Job cancelled"}
|
|
219
|
+
else:
|
|
220
|
+
return {"status": "error", "message": f"Cannot cancel job in {job.status} state"}
|
|
221
|
+
|
|
222
|
+
# Image upload endpoint
|
|
223
|
+
@self.app.post("/api/jobs/{job_id}/images", dependencies=[Depends(verify_api_key)])
|
|
224
|
+
async def upload_image(
|
|
225
|
+
job_id: str,
|
|
226
|
+
file: UploadFile = File(...),
|
|
227
|
+
metadata: str = Form(None)
|
|
228
|
+
):
|
|
229
|
+
if job_id not in self.jobs:
|
|
230
|
+
raise HTTPException(status_code=404, detail="Job not found")
|
|
231
|
+
|
|
232
|
+
job = self.jobs[job_id]
|
|
233
|
+
|
|
234
|
+
# Check if job is in a valid state for uploads
|
|
235
|
+
if job.status not in ["created", "uploading"]:
|
|
236
|
+
raise HTTPException(
|
|
237
|
+
status_code=400,
|
|
238
|
+
detail=f"Cannot upload images to job in {job.status} state"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Check if we've reached the maximum number of images
|
|
242
|
+
if job.images_uploaded >= job.num_images:
|
|
243
|
+
raise HTTPException(
|
|
244
|
+
status_code=400,
|
|
245
|
+
detail=f"Maximum number of images ({job.num_images}) already uploaded"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Update job status
|
|
249
|
+
job.status = "uploading"
|
|
250
|
+
job.updated_at = time.time()
|
|
251
|
+
|
|
252
|
+
# Create job directory if it doesn't exist
|
|
253
|
+
job_dir = os.path.join(self.config.output_dir, job_id, "images")
|
|
254
|
+
os.makedirs(job_dir, exist_ok=True)
|
|
255
|
+
|
|
256
|
+
# Parse metadata
|
|
257
|
+
image_metadata = {}
|
|
258
|
+
if metadata:
|
|
259
|
+
try:
|
|
260
|
+
image_metadata = json.loads(metadata)
|
|
261
|
+
except json.JSONDecodeError:
|
|
262
|
+
logger.warning(f"Invalid metadata format: {metadata}")
|
|
263
|
+
|
|
264
|
+
# Save the file
|
|
265
|
+
image_index = job.images_uploaded
|
|
266
|
+
file_extension = os.path.splitext(file.filename)[1]
|
|
267
|
+
image_path = os.path.join(job_dir, f"image_{image_index:04d}{file_extension}")
|
|
268
|
+
|
|
269
|
+
with open(image_path, "wb") as f:
|
|
270
|
+
shutil.copyfileobj(file.file, f)
|
|
271
|
+
|
|
272
|
+
# Update job status
|
|
273
|
+
job.images_uploaded += 1
|
|
274
|
+
job.updated_at = time.time()
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
"status": "success",
|
|
278
|
+
"job_id": job_id,
|
|
279
|
+
"image_index": image_index,
|
|
280
|
+
"image_path": image_path,
|
|
281
|
+
"images_uploaded": job.images_uploaded,
|
|
282
|
+
"total_images": job.num_images
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# Process job endpoint
|
|
286
|
+
@self.app.post("/api/jobs/{job_id}/process", status_code=202, dependencies=[Depends(verify_api_key)])
|
|
287
|
+
async def process_job(
|
|
288
|
+
job_id: str,
|
|
289
|
+
process_request: ProcessRequest,
|
|
290
|
+
background_tasks: BackgroundTasks
|
|
291
|
+
):
|
|
292
|
+
if job_id not in self.jobs:
|
|
293
|
+
raise HTTPException(status_code=404, detail="Job not found")
|
|
294
|
+
|
|
295
|
+
job = self.jobs[job_id]
|
|
296
|
+
|
|
297
|
+
# Check if job is in a valid state for processing
|
|
298
|
+
if job.status != "uploading":
|
|
299
|
+
raise HTTPException(
|
|
300
|
+
status_code=400,
|
|
301
|
+
detail=f"Cannot process job in {job.status} state"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Check if all images have been uploaded
|
|
305
|
+
if job.images_uploaded < job.num_images:
|
|
306
|
+
raise HTTPException(
|
|
307
|
+
status_code=400,
|
|
308
|
+
detail=f"Not all images uploaded ({job.images_uploaded}/{job.num_images})"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Update job status
|
|
312
|
+
job.status = "processing"
|
|
313
|
+
job.updated_at = time.time()
|
|
314
|
+
job.progress = 0.0
|
|
315
|
+
|
|
316
|
+
# Start processing in the background
|
|
317
|
+
background_tasks.add_task(
|
|
318
|
+
self.process_job_task,
|
|
319
|
+
job_id,
|
|
320
|
+
process_request.reconstruction_quality,
|
|
321
|
+
process_request.output_formats
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
"status": "success",
|
|
326
|
+
"job_id": job_id,
|
|
327
|
+
"message": "Processing started"
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Model download endpoint
|
|
331
|
+
@self.app.get("/api/jobs/{job_id}/model", dependencies=[Depends(verify_api_key)])
|
|
332
|
+
async def download_model(job_id: str, format: str = "obj"):
|
|
333
|
+
if job_id not in self.jobs:
|
|
334
|
+
raise HTTPException(status_code=404, detail="Job not found")
|
|
335
|
+
|
|
336
|
+
job = self.jobs[job_id]
|
|
337
|
+
|
|
338
|
+
# Check if job is completed
|
|
339
|
+
if job.status != "completed":
|
|
340
|
+
raise HTTPException(
|
|
341
|
+
status_code=400,
|
|
342
|
+
detail=f"Job is not completed. Current status: {job.status}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Check if the requested format is available
|
|
346
|
+
if format == "obj" and job.obj_file:
|
|
347
|
+
return FileResponse(job.obj_file, filename=f"{job.model_id}.obj")
|
|
348
|
+
elif format == "ply" and job.point_cloud_file:
|
|
349
|
+
return FileResponse(job.point_cloud_file, filename=f"{job.model_id}.ply")
|
|
350
|
+
else:
|
|
351
|
+
raise HTTPException(
|
|
352
|
+
status_code=404,
|
|
353
|
+
detail=f"Model in {format} format not available"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def create_job(self, num_images: int) -> Dict[str, Any]:
|
|
357
|
+
"""
|
|
358
|
+
Create a new job.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
num_images: Number of images to be uploaded
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Dictionary with job information
|
|
365
|
+
"""
|
|
366
|
+
# Check if we've reached the maximum number of jobs
|
|
367
|
+
active_jobs = sum(1 for job in self.jobs.values() if job.status in ["created", "uploading", "processing"])
|
|
368
|
+
if active_jobs >= self.config.max_jobs:
|
|
369
|
+
raise HTTPException(
|
|
370
|
+
status_code=429,
|
|
371
|
+
detail=f"Maximum number of active jobs ({self.config.max_jobs}) reached"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Check if the number of images is valid
|
|
375
|
+
if num_images <= 0 or num_images > self.config.max_images_per_job:
|
|
376
|
+
raise HTTPException(
|
|
377
|
+
status_code=400,
|
|
378
|
+
detail=f"Number of images must be between 1 and {self.config.max_images_per_job}"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Create a new job
|
|
382
|
+
job_id = str(uuid.uuid4())
|
|
383
|
+
job = JobStatus(
|
|
384
|
+
job_id=job_id,
|
|
385
|
+
num_images=num_images
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Add job to the list
|
|
389
|
+
self.jobs[job_id] = job
|
|
390
|
+
|
|
391
|
+
# Create job directory
|
|
392
|
+
job_dir = os.path.join(self.config.output_dir, job_id)
|
|
393
|
+
os.makedirs(job_dir, exist_ok=True)
|
|
394
|
+
|
|
395
|
+
return job.dict()
|
|
396
|
+
|
|
397
|
+
async def process_job_task(
|
|
398
|
+
self,
|
|
399
|
+
job_id: str,
|
|
400
|
+
reconstruction_quality: str,
|
|
401
|
+
output_formats: List[str]
|
|
402
|
+
):
|
|
403
|
+
"""
|
|
404
|
+
Process a job in the background.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
job_id: ID of the job to process
|
|
408
|
+
reconstruction_quality: Quality of the reconstruction (low, normal, high)
|
|
409
|
+
output_formats: List of output formats to generate
|
|
410
|
+
"""
|
|
411
|
+
if job_id not in self.jobs:
|
|
412
|
+
logger.error(f"Job {job_id} not found")
|
|
413
|
+
return
|
|
414
|
+
|
|
415
|
+
job = self.jobs[job_id]
|
|
416
|
+
job_dir = os.path.join(self.config.output_dir, job_id)
|
|
417
|
+
images_dir = os.path.join(job_dir, "images")
|
|
418
|
+
output_dir = os.path.join(job_dir, "output")
|
|
419
|
+
|
|
420
|
+
# Create output directory
|
|
421
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
422
|
+
|
|
423
|
+
# Generate a model ID
|
|
424
|
+
model_id = f"model_{job_id[:8]}"
|
|
425
|
+
job.model_id = model_id
|
|
426
|
+
|
|
427
|
+
# Update job status
|
|
428
|
+
job.status = "processing"
|
|
429
|
+
job.progress = 0.0
|
|
430
|
+
job.updated_at = time.time()
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
# Start timing
|
|
434
|
+
start_time = time.time()
|
|
435
|
+
|
|
436
|
+
# Run CUDA MVS
|
|
437
|
+
await self.run_cuda_mvs(
|
|
438
|
+
job,
|
|
439
|
+
images_dir,
|
|
440
|
+
output_dir,
|
|
441
|
+
model_id,
|
|
442
|
+
reconstruction_quality
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Convert output formats if needed
|
|
446
|
+
if "obj" in output_formats and not job.obj_file:
|
|
447
|
+
# Convert PLY to OBJ if needed
|
|
448
|
+
ply_file = job.point_cloud_file
|
|
449
|
+
if ply_file and os.path.exists(ply_file):
|
|
450
|
+
obj_file = os.path.join(output_dir, f"{model_id}.obj")
|
|
451
|
+
await self.convert_ply_to_obj(ply_file, obj_file)
|
|
452
|
+
job.obj_file = obj_file
|
|
453
|
+
|
|
454
|
+
# Calculate processing time
|
|
455
|
+
job.processing_time = time.time() - start_time
|
|
456
|
+
|
|
457
|
+
# Update job status
|
|
458
|
+
job.status = "completed"
|
|
459
|
+
job.progress = 100.0
|
|
460
|
+
job.updated_at = time.time()
|
|
461
|
+
job.output_dir = output_dir
|
|
462
|
+
|
|
463
|
+
logger.info(f"Job {job_id} completed successfully in {job.processing_time:.2f} seconds")
|
|
464
|
+
|
|
465
|
+
except Exception as e:
|
|
466
|
+
# Update job status
|
|
467
|
+
job.status = "failed"
|
|
468
|
+
job.error_message = str(e)
|
|
469
|
+
job.updated_at = time.time()
|
|
470
|
+
|
|
471
|
+
logger.error(f"Job {job_id} failed: {e}")
|
|
472
|
+
|
|
473
|
+
async def run_cuda_mvs(
|
|
474
|
+
self,
|
|
475
|
+
job: JobStatus,
|
|
476
|
+
images_dir: str,
|
|
477
|
+
output_dir: str,
|
|
478
|
+
model_id: str,
|
|
479
|
+
reconstruction_quality: str
|
|
480
|
+
):
|
|
481
|
+
"""
|
|
482
|
+
Run CUDA MVS on the uploaded images.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
job: Job status object
|
|
486
|
+
images_dir: Directory containing the images
|
|
487
|
+
output_dir: Directory to save the output
|
|
488
|
+
model_id: ID of the model
|
|
489
|
+
reconstruction_quality: Quality of the reconstruction
|
|
490
|
+
"""
|
|
491
|
+
# Check if CUDA MVS is installed
|
|
492
|
+
cuda_mvs_executable = os.path.join(self.config.cuda_mvs_path, "build", "app_patch_match_mvs")
|
|
493
|
+
if not os.path.exists(cuda_mvs_executable):
|
|
494
|
+
raise FileNotFoundError(f"CUDA MVS executable not found at {cuda_mvs_executable}")
|
|
495
|
+
|
|
496
|
+
# Create a list of image paths
|
|
497
|
+
image_files = []
|
|
498
|
+
for file in os.listdir(images_dir):
|
|
499
|
+
if file.lower().endswith((".jpg", ".jpeg", ".png")):
|
|
500
|
+
image_files.append(os.path.join(images_dir, file))
|
|
501
|
+
|
|
502
|
+
if not image_files:
|
|
503
|
+
raise ValueError("No valid image files found")
|
|
504
|
+
|
|
505
|
+
# Sort image files to ensure consistent order
|
|
506
|
+
image_files.sort()
|
|
507
|
+
|
|
508
|
+
# Create a camera parameter file
|
|
509
|
+
camera_params_file = os.path.join(output_dir, "cameras.txt")
|
|
510
|
+
await self.generate_camera_params(image_files, camera_params_file)
|
|
511
|
+
|
|
512
|
+
# Set quality parameters
|
|
513
|
+
if reconstruction_quality == "low":
|
|
514
|
+
num_iterations = 3
|
|
515
|
+
max_resolution = 1024
|
|
516
|
+
elif reconstruction_quality == "normal":
|
|
517
|
+
num_iterations = 5
|
|
518
|
+
max_resolution = 2048
|
|
519
|
+
elif reconstruction_quality == "high":
|
|
520
|
+
num_iterations = 7
|
|
521
|
+
max_resolution = 4096
|
|
522
|
+
else:
|
|
523
|
+
num_iterations = 5
|
|
524
|
+
max_resolution = 2048
|
|
525
|
+
|
|
526
|
+
# Prepare output files
|
|
527
|
+
point_cloud_file = os.path.join(output_dir, f"{model_id}.ply")
|
|
528
|
+
|
|
529
|
+
# Build the command
|
|
530
|
+
cmd = [
|
|
531
|
+
cuda_mvs_executable,
|
|
532
|
+
"--input_folder", images_dir,
|
|
533
|
+
"--camera_file", camera_params_file,
|
|
534
|
+
"--output_folder", output_dir,
|
|
535
|
+
"--output_file", point_cloud_file,
|
|
536
|
+
"--num_iterations", str(num_iterations),
|
|
537
|
+
"--max_resolution", str(max_resolution)
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
# Run the command
|
|
541
|
+
logger.info(f"Running CUDA MVS: {' '.join(cmd)}")
|
|
542
|
+
|
|
543
|
+
process = subprocess.Popen(
|
|
544
|
+
cmd,
|
|
545
|
+
stdout=subprocess.PIPE,
|
|
546
|
+
stderr=subprocess.PIPE,
|
|
547
|
+
text=True
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Monitor progress
|
|
551
|
+
while True:
|
|
552
|
+
if process.poll() is not None:
|
|
553
|
+
break
|
|
554
|
+
|
|
555
|
+
# Read output line by line
|
|
556
|
+
output = process.stdout.readline()
|
|
557
|
+
if output:
|
|
558
|
+
# Try to parse progress information
|
|
559
|
+
if "Progress:" in output:
|
|
560
|
+
try:
|
|
561
|
+
progress_str = output.split("Progress:")[1].strip().rstrip("%")
|
|
562
|
+
progress = float(progress_str)
|
|
563
|
+
job.progress = progress
|
|
564
|
+
job.updated_at = time.time()
|
|
565
|
+
except (ValueError, IndexError):
|
|
566
|
+
pass
|
|
567
|
+
|
|
568
|
+
# Sleep briefly to avoid CPU spinning
|
|
569
|
+
await asyncio.sleep(0.1)
|
|
570
|
+
|
|
571
|
+
# Get the final output
|
|
572
|
+
stdout, stderr = process.communicate()
|
|
573
|
+
|
|
574
|
+
# Check if the process was successful
|
|
575
|
+
if process.returncode != 0:
|
|
576
|
+
raise RuntimeError(f"CUDA MVS failed with error: {stderr}")
|
|
577
|
+
|
|
578
|
+
# Check if the output file was created
|
|
579
|
+
if not os.path.exists(point_cloud_file):
|
|
580
|
+
raise FileNotFoundError(f"Output file not created: {point_cloud_file}")
|
|
581
|
+
|
|
582
|
+
# Update job with the output file
|
|
583
|
+
job.point_cloud_file = point_cloud_file
|
|
584
|
+
|
|
585
|
+
async def generate_camera_params(self, image_files: List[str], output_file: str):
|
|
586
|
+
"""
|
|
587
|
+
Generate camera parameters for CUDA MVS.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
image_files: List of image files
|
|
591
|
+
output_file: Output file for camera parameters
|
|
592
|
+
"""
|
|
593
|
+
# For now, use a simple camera model with default parameters
|
|
594
|
+
# In a real implementation, this would use structure from motion
|
|
595
|
+
# to estimate camera parameters from the images
|
|
596
|
+
|
|
597
|
+
with open(output_file, "w") as f:
|
|
598
|
+
f.write(f"# Camera parameters for {len(image_files)} images\n")
|
|
599
|
+
f.write("# Format: image_name width height fx fy cx cy\n")
|
|
600
|
+
|
|
601
|
+
for i, image_file in enumerate(image_files):
|
|
602
|
+
# Get image dimensions
|
|
603
|
+
from PIL import Image
|
|
604
|
+
with Image.open(image_file) as img:
|
|
605
|
+
width, height = img.size
|
|
606
|
+
|
|
607
|
+
# Use default camera parameters
|
|
608
|
+
fx = width * 1.2 # Focal length x
|
|
609
|
+
fy = height * 1.2 # Focal length y
|
|
610
|
+
cx = width / 2 # Principal point x
|
|
611
|
+
cy = height / 2 # Principal point y
|
|
612
|
+
|
|
613
|
+
# Write camera parameters
|
|
614
|
+
f.write(f"{os.path.basename(image_file)} {width} {height} {fx} {fy} {cx} {cy}\n")
|
|
615
|
+
|
|
616
|
+
async def convert_ply_to_obj(self, ply_file: str, obj_file: str):
|
|
617
|
+
"""
|
|
618
|
+
Convert PLY file to OBJ format.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
ply_file: Input PLY file
|
|
622
|
+
obj_file: Output OBJ file
|
|
623
|
+
"""
|
|
624
|
+
try:
|
|
625
|
+
import open3d as o3d
|
|
626
|
+
|
|
627
|
+
# Load the PLY file
|
|
628
|
+
mesh = o3d.io.read_triangle_mesh(ply_file)
|
|
629
|
+
|
|
630
|
+
# Save as OBJ
|
|
631
|
+
o3d.io.write_triangle_mesh(obj_file, mesh)
|
|
632
|
+
|
|
633
|
+
logger.info(f"Converted {ply_file} to {obj_file}")
|
|
634
|
+
|
|
635
|
+
except ImportError:
|
|
636
|
+
# If open3d is not available, use a subprocess
|
|
637
|
+
try:
|
|
638
|
+
# Try using meshlab
|
|
639
|
+
subprocess.run(
|
|
640
|
+
["meshlabserver", "-i", ply_file, "-o", obj_file],
|
|
641
|
+
check=True,
|
|
642
|
+
capture_output=True
|
|
643
|
+
)
|
|
644
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
645
|
+
# If meshlab is not available, try using assimp
|
|
646
|
+
try:
|
|
647
|
+
subprocess.run(
|
|
648
|
+
["assimp", "export", ply_file, obj_file],
|
|
649
|
+
check=True,
|
|
650
|
+
capture_output=True
|
|
651
|
+
)
|
|
652
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
653
|
+
raise RuntimeError("No suitable tool found to convert PLY to OBJ")
|
|
654
|
+
|
|
655
|
+
def get_server_info(self) -> Dict[str, Any]:
|
|
656
|
+
"""
|
|
657
|
+
Get server information.
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
Dictionary with server information
|
|
661
|
+
"""
|
|
662
|
+
# Count active jobs
|
|
663
|
+
active_jobs = sum(1 for job in self.jobs.values() if job.status in ["created", "uploading", "processing"])
|
|
664
|
+
|
|
665
|
+
# Get server capabilities
|
|
666
|
+
capabilities = {
|
|
667
|
+
"max_jobs": self.config.max_jobs,
|
|
668
|
+
"max_images_per_job": self.config.max_images_per_job,
|
|
669
|
+
"job_timeout": self.config.job_timeout,
|
|
670
|
+
"supported_formats": ["obj", "ply"],
|
|
671
|
+
"gpu_info": self.config.gpu_info
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
# Get job summary
|
|
675
|
+
job_summary = {
|
|
676
|
+
"total": len(self.jobs),
|
|
677
|
+
"active": active_jobs,
|
|
678
|
+
"completed": sum(1 for job in self.jobs.values() if job.status == "completed"),
|
|
679
|
+
"failed": sum(1 for job in self.jobs.values() if job.status == "failed")
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
"server_id": self.server_id,
|
|
684
|
+
"name": self.config.server_name,
|
|
685
|
+
"version": "1.0.0",
|
|
686
|
+
"status": "running",
|
|
687
|
+
"capabilities": capabilities,
|
|
688
|
+
"jobs": job_summary,
|
|
689
|
+
"uptime": time.time() - self.start_time
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
def advertise_service(self):
|
|
693
|
+
"""Advertise the server on the local network using Zeroconf."""
|
|
694
|
+
try:
|
|
695
|
+
import socket
|
|
696
|
+
from zeroconf import ServiceInfo, Zeroconf
|
|
697
|
+
|
|
698
|
+
# Get local IP address
|
|
699
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
700
|
+
s.connect(("8.8.8.8", 80))
|
|
701
|
+
local_ip = s.getsockname()[0]
|
|
702
|
+
s.close()
|
|
703
|
+
|
|
704
|
+
# Create service info
|
|
705
|
+
capabilities = {
|
|
706
|
+
"max_jobs": self.config.max_jobs,
|
|
707
|
+
"max_images_per_job": self.config.max_images_per_job,
|
|
708
|
+
"supported_formats": ["obj", "ply"],
|
|
709
|
+
"gpu_info": self.config.gpu_info
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
service_info = ServiceInfo(
|
|
713
|
+
"_cudamvs._tcp.local.",
|
|
714
|
+
f"{self.server_id}._cudamvs._tcp.local.",
|
|
715
|
+
addresses=[socket.inet_aton(local_ip)],
|
|
716
|
+
port=DEFAULT_PORT,
|
|
717
|
+
properties={
|
|
718
|
+
b"name": self.config.server_name.encode("utf-8"),
|
|
719
|
+
b"capabilities": json.dumps(capabilities).encode("utf-8")
|
|
720
|
+
}
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
# Register service
|
|
724
|
+
self.zeroconf = Zeroconf()
|
|
725
|
+
self.zeroconf.register_service(service_info)
|
|
726
|
+
|
|
727
|
+
logger.info(f"Advertising CUDA MVS service on {local_ip}:{DEFAULT_PORT}")
|
|
728
|
+
|
|
729
|
+
except Exception as e:
|
|
730
|
+
logger.error(f"Failed to advertise service: {e}")
|
|
731
|
+
|
|
732
|
+
def run(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT):
|
|
733
|
+
"""
|
|
734
|
+
Run the server.
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
host: Host to bind to
|
|
738
|
+
port: Port to bind to
|
|
739
|
+
"""
|
|
740
|
+
uvicorn.run(self.app, host=host, port=port)
|
|
741
|
+
|
|
742
|
+
def cleanup(self):
|
|
743
|
+
"""Clean up resources."""
|
|
744
|
+
if self.zeroconf:
|
|
745
|
+
self.zeroconf.close()
|
|
746
|
+
|
|
747
|
+
# Main entry point
|
|
748
|
+
def main():
|
|
749
|
+
"""Main entry point."""
|
|
750
|
+
parser = argparse.ArgumentParser(description="CUDA MVS Server")
|
|
751
|
+
parser.add_argument("--host", default=DEFAULT_HOST, help="Host to bind to")
|
|
752
|
+
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="Port to bind to")
|
|
753
|
+
parser.add_argument("--cuda-mvs-path", default=DEFAULT_CUDA_MVS_PATH, help="Path to CUDA MVS installation")
|
|
754
|
+
parser.add_argument("--output-dir", default=DEFAULT_OUTPUT_DIR, help="Output directory")
|
|
755
|
+
parser.add_argument("--max-jobs", type=int, default=DEFAULT_MAX_JOBS, help="Maximum number of concurrent jobs")
|
|
756
|
+
parser.add_argument("--max-images", type=int, default=DEFAULT_MAX_IMAGES_PER_JOB, help="Maximum images per job")
|
|
757
|
+
parser.add_argument("--job-timeout", type=int, default=DEFAULT_JOB_TIMEOUT, help="Job timeout in seconds")
|
|
758
|
+
parser.add_argument("--api-key", help="API key for authentication")
|
|
759
|
+
parser.add_argument("--server-name", default="CUDA MVS Server", help="Server name")
|
|
760
|
+
parser.add_argument("--no-advertise", action="store_true", help="Don't advertise service on the network")
|
|
761
|
+
|
|
762
|
+
args = parser.parse_args()
|
|
763
|
+
|
|
764
|
+
# Create server configuration
|
|
765
|
+
config = ServerConfig(
|
|
766
|
+
cuda_mvs_path=args.cuda_mvs_path,
|
|
767
|
+
output_dir=args.output_dir,
|
|
768
|
+
max_jobs=args.max_jobs,
|
|
769
|
+
max_images_per_job=args.max_images,
|
|
770
|
+
job_timeout=args.job_timeout,
|
|
771
|
+
api_key=args.api_key,
|
|
772
|
+
server_name=args.server_name,
|
|
773
|
+
advertise_service=not args.no_advertise
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
# Create and run server
|
|
777
|
+
server = CUDAMVSServer(config)
|
|
778
|
+
|
|
779
|
+
try:
|
|
780
|
+
server.run(host=args.host, port=args.port)
|
|
781
|
+
finally:
|
|
782
|
+
server.cleanup()
|
|
783
|
+
|
|
784
|
+
if __name__ == "__main__":
|
|
785
|
+
# Add asyncio import for async/await support
|
|
786
|
+
import asyncio
|
|
787
|
+
main()
|