api_server.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 房间分割 & 家具检测 FastAPI 服务
  5. 启动:
  6. python api_server.py --host 0.0.0.0 --port 8000
  7. 调用示例:
  8. curl -X POST http://localhost:8000/api/process \
  9. -H "Content-Type: application/json" \
  10. -d '{"scene_path": "/absolute/path/to/SG-xxxxx"}'
  11. """
  12. import asyncio
  13. import json
  14. import os
  15. import sys
  16. import traceback
  17. from contextlib import asynccontextmanager
  18. from typing import Optional
  19. import uvicorn
  20. from fastapi import FastAPI, HTTPException
  21. from pydantic import BaseModel, Field
  22. # ============================================================================
  23. # 确保 pipeline 模块可用
  24. # ============================================================================
  25. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  26. import pipeline # noqa: E402
  27. # ============================================================================
  28. # 请求 / 响应模型
  29. # ============================================================================
  30. class ProcessRequest(BaseModel):
  31. scene_path: str = Field(
  32. ...,
  33. description="场景文件夹的绝对路径(包含 images/、depthmap/、vision.txt 等)",
  34. examples=["/data/scenes/SG-n6nV8B2oW95"]
  35. )
  36. class ProcessResponse(BaseModel):
  37. status: str = Field(..., description="success / error")
  38. message: str
  39. scene_path: str
  40. output_dirs: dict = Field(
  41. default_factory=dict,
  42. description="输出目录路径映射"
  43. )
  44. class HealthResponse(BaseModel):
  45. status: str
  46. model_dir: str
  47. # ============================================================================
  48. # 启动 / 关闭
  49. # ============================================================================
  50. @asynccontextmanager
  51. async def lifespan(app: FastAPI):
  52. model_dir = os.path.dirname(os.path.abspath(__file__))
  53. print(f"[API] 模型目录:{model_dir}")
  54. for name, filename in [
  55. ("BiRefNet 权重", "initial_mask.onnx"),
  56. ("房间分类模型", "room_cls.pt"),
  57. ("家具检测模型", "100_cls_seg.pt"),
  58. ]:
  59. path = os.path.join(model_dir, filename)
  60. if not os.path.exists(path):
  61. print(f"[API WARNING] 模型文件不存在:{name} -> {path}")
  62. else:
  63. print(f"[API OK] 模型就绪:{filename}")
  64. yield
  65. print("[API] 服务已关闭")
  66. app = FastAPI(
  67. title="Room & Furniture Pipeline API",
  68. description="房间分割 → 家具检测 → 家具映射到房间",
  69. version="1.0.0",
  70. lifespan=lifespan,
  71. )
  72. # ============================================================================
  73. # 路由
  74. # ============================================================================
  75. @app.get("/health", response_model=HealthResponse)
  76. def health_check():
  77. model_dir = os.path.dirname(os.path.abspath(__file__))
  78. return HealthResponse(status="ok", model_dir=model_dir)
  79. @app.post("/api/process", response_model=ProcessResponse)
  80. def process_scene(req: ProcessRequest):
  81. scene_path = req.scene_path.strip()
  82. # --- 验证 ---
  83. if not os.path.isabs(scene_path):
  84. raise HTTPException(status_code=400, detail="scene_path 必须是绝对路径")
  85. if not os.path.isdir(scene_path):
  86. raise HTTPException(status_code=400, detail=f"文件夹不存在:{scene_path}")
  87. for sub, name in [("images", "图片文件夹"), ("depthmap", "深度图文件夹")]:
  88. p = os.path.join(scene_path, sub)
  89. if not os.path.exists(p):
  90. raise HTTPException(status_code=400, detail=f"{name}不存在:{p}")
  91. vision_file = os.path.join(scene_path, "vision.txt")
  92. if not os.path.exists(vision_file):
  93. raise HTTPException(status_code=400, detail=f"位姿文件不存在:{vision_file}")
  94. model_dir = os.path.dirname(os.path.abspath(__file__))
  95. weights_path = os.path.join(model_dir, "initial_mask.onnx")
  96. room_cls_path = os.path.join(model_dir, "room_cls.pt")
  97. detect_model_path = os.path.join(model_dir, "100_cls_seg.pt")
  98. for model_p, model_name in [(weights_path, "BiRefNet 权重"), (room_cls_path, "房间分类模型"), (detect_model_path, "家具检测模型")]:
  99. if not os.path.exists(model_p):
  100. raise HTTPException(status_code=500, detail=f"模型文件不存在:{model_name} -> {model_p}")
  101. # --- 输出目录 ---
  102. out_room_dir = os.path.join(scene_path, "out_room")
  103. out_detect_dir = os.path.join(scene_path, "out_detect_furniture")
  104. # --- 执行 ---
  105. try:
  106. # Step 1
  107. print(f"\n[API] >>> Step 1: 房间分割 | {scene_path}")
  108. pipeline.step1_extract_room(scene_path, out_room_dir, weights_path, room_cls_path)
  109. # Step 2
  110. print(f"\n[API] >>> Step 2: 家具检测")
  111. pipeline.step2_detect_furniture(scene_path, out_detect_dir, detect_model_path)
  112. # Step 3
  113. print(f"\n[API] >>> Step 3: 家具映射到房间")
  114. pipeline.step3_map_furniture_to_room(
  115. scene_folder=scene_path,
  116. images_folder=os.path.join(scene_path, "images"),
  117. depth_folder=os.path.join(scene_path, "depthmap"),
  118. vision_file=vision_file,
  119. furniture_folder=out_detect_dir,
  120. room_folder=out_room_dir,
  121. )
  122. except Exception as e:
  123. print(f"\n[API ERROR] 处理失败:{scene_path}")
  124. traceback.print_exc()
  125. raise HTTPException(status_code=500, detail=f"处理失败:{str(e)}")
  126. print(f"\n[API] >>> 处理完成:{scene_path}")
  127. return ProcessResponse(
  128. status="success",
  129. message="处理完成",
  130. scene_path=scene_path,
  131. output_dirs={
  132. "room_output": out_room_dir,
  133. "furniture_output": out_detect_dir,
  134. }
  135. )
  136. @app.get("/docs")
  137. def swagger_ui():
  138. # FastAPI 自动提供 /docs,这里留个兜底
  139. from fastapi.responses import RedirectResponse
  140. return RedirectResponse(url="/docs")
  141. # ============================================================================
  142. # 启动入口
  143. # ============================================================================
  144. def parse_args():
  145. import argparse
  146. parser = argparse.ArgumentParser(description="启动 Pipeline FastAPI 服务")
  147. parser.add_argument("--host", default="0.0.0.0", help="监听地址 (默认 0.0.0.0)")
  148. parser.add_argument("--port", type=int, default=8000, help="监听端口 (默认 8000)")
  149. parser.add_argument("--workers", type=int, default=1, help="工作进程数 (默认 1,长任务建议用 1)")
  150. return parser.parse_args()
  151. if __name__ == "__main__":
  152. args = parse_args()
  153. uvicorn.run(
  154. "api_server:app",
  155. host=args.host,
  156. port=args.port,
  157. workers=args.workers,
  158. log_level="info",
  159. )