api_server_with_result.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 房间分割 & 家具检测 FastAPI 服务(返回合并后的家具检测数据)
  5. 启动:
  6. python api_server_with_result.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 json
  13. import os
  14. import sys
  15. import traceback
  16. from contextlib import asynccontextmanager
  17. from pathlib import Path
  18. import uvicorn
  19. from fastapi import FastAPI, HTTPException
  20. from pydantic import BaseModel, Field
  21. # ============================================================================
  22. # 确保 pipeline 模块可用
  23. # ============================================================================
  24. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  25. import pipeline # noqa: E402
  26. # ============================================================================
  27. # 请求 / 响应模型
  28. # ============================================================================
  29. class ProcessRequest(BaseModel):
  30. scene_path: str = Field(
  31. ...,
  32. description="场景文件夹的绝对路径(包含 images/、depthmap/、vision.txt 等)",
  33. examples=["/data/scenes/SG-n6nV8B2oW95"]
  34. )
  35. class ProcessResponse(BaseModel):
  36. status: str = Field(..., description="success / error")
  37. message: str
  38. scene_path: str
  39. output_dirs: dict = Field(
  40. default_factory=dict,
  41. description="输出目录路径映射"
  42. )
  43. furniture_data: list = Field(
  44. default_factory=list,
  45. description="合并后的家具检测数据,每个元素对应一张图片的检测结果"
  46. )
  47. class HealthResponse(BaseModel):
  48. status: str
  49. model_dir: str
  50. # ============================================================================
  51. # 启动 / 关闭
  52. # ============================================================================
  53. @asynccontextmanager
  54. async def lifespan(app: FastAPI):
  55. model_dir = os.path.dirname(os.path.abspath(__file__))
  56. print(f"[API] 模型目录:{model_dir}")
  57. for name, filename in [
  58. ("BiRefNet 权重", "initial_mask.onnx"),
  59. ("房间分类模型", "room_cls.pt"),
  60. ("家具检测模型", "100_cls_seg.pt"),
  61. ]:
  62. path = os.path.join(model_dir, filename)
  63. if not os.path.exists(path):
  64. print(f"[API WARNING] 模型文件不存在:{name} -> {path}")
  65. else:
  66. print(f"[API OK] 模型就绪:{filename}")
  67. yield
  68. print("[API] 服务已关闭")
  69. app = FastAPI(
  70. title="Room & Furniture Pipeline API",
  71. description="房间分割 → 家具检测 → 家具映射到房间(返回合并后的家具检测结果)",
  72. version="1.0.0",
  73. lifespan=lifespan,
  74. )
  75. # ============================================================================
  76. # 辅助函数
  77. # ============================================================================
  78. def merge_furniture_jsons(detect_dir: str) -> list:
  79. """读取 detect_dir 下所有 .json 文件,按文件名排序后合并为一个 list 返回。
  80. 每个元素保留原始 JSON 的完整结构(shapes、imagePath、imageWidth、
  81. imageHeight、version 以及 Step3 注入的 belong_room 等字段)。
  82. """
  83. result = []
  84. json_files = sorted(
  85. Path(detect_dir).glob("*.json"),
  86. key=lambda p: _natural_sort_key(p.stem),
  87. )
  88. for jf in json_files:
  89. with open(jf, "r", encoding="utf-8") as f:
  90. result.append(json.load(f))
  91. return result
  92. def _natural_sort_key(name: str):
  93. """将字符串中的数字部分转为 int 元组,用于自然排序(0, 1, 2, ..., 10, 11)。"""
  94. import re
  95. return [
  96. int(part) if part.isdigit() else part.lower()
  97. for part in re.split(r"(\d+)", name)
  98. ]
  99. # ============================================================================
  100. # 路由
  101. # ============================================================================
  102. @app.get("/health", response_model=HealthResponse)
  103. def health_check():
  104. model_dir = os.path.dirname(os.path.abspath(__file__))
  105. return HealthResponse(status="ok", model_dir=model_dir)
  106. @app.post("/api/process")
  107. def process_scene(req: ProcessRequest):
  108. scene_path = req.scene_path.strip()
  109. # --- 验证 ---
  110. if not os.path.isabs(scene_path):
  111. raise HTTPException(status_code=400, detail="scene_path 必须是绝对路径")
  112. if not os.path.isdir(scene_path):
  113. raise HTTPException(status_code=400, detail=f"文件夹不存在:{scene_path}")
  114. for sub, name in [("images", "图片文件夹"), ("depthmap", "深度图文件夹")]:
  115. p = os.path.join(scene_path, sub)
  116. if not os.path.exists(p):
  117. raise HTTPException(status_code=400, detail=f"{name}不存在:{p}")
  118. vision_file = os.path.join(scene_path, "vision.txt")
  119. if not os.path.exists(vision_file):
  120. raise HTTPException(status_code=400, detail=f"位姿文件不存在:{vision_file}")
  121. model_dir = os.path.dirname(os.path.abspath(__file__))
  122. weights_path = os.path.join(model_dir, "initial_mask.onnx")
  123. room_cls_path = os.path.join(model_dir, "room_cls.pt")
  124. detect_model_path = os.path.join(model_dir, "100_cls_seg.pt")
  125. for model_p, model_name in [(weights_path, "BiRefNet 权重"), (room_cls_path, "房间分类模型"), (detect_model_path, "家具检测模型")]:
  126. if not os.path.exists(model_p):
  127. raise HTTPException(status_code=500, detail=f"模型文件不存在:{model_name} -> {model_p}")
  128. # --- 输出目录 ---
  129. out_room_dir = os.path.join(scene_path, "out_room")
  130. out_detect_dir = os.path.join(scene_path, "out_detect_furniture")
  131. # --- 执行 ---
  132. try:
  133. # Step 1: 房间分割
  134. print(f"\n[API] >>> Step 1: 房间分割 | {scene_path}")
  135. pipeline.step1_extract_room(scene_path, out_room_dir, weights_path, room_cls_path)
  136. # Step 2: 家具检测
  137. print(f"\n[API] >>> Step 2: 家具检测")
  138. pipeline.step2_detect_furniture(scene_path, out_detect_dir, detect_model_path)
  139. # Step 3: 家具映射到房间
  140. print(f"\n[API] >>> Step 3: 家具映射到房间")
  141. pipeline.step3_map_furniture_to_room(
  142. scene_folder=scene_path,
  143. images_folder=os.path.join(scene_path, "images"),
  144. depth_folder=os.path.join(scene_path, "depthmap"),
  145. vision_file=vision_file,
  146. furniture_folder=out_detect_dir,
  147. room_folder=out_room_dir,
  148. )
  149. except Exception as e:
  150. print(f"\n[API ERROR] 处理失败:{scene_path}")
  151. traceback.print_exc()
  152. raise HTTPException(status_code=500, detail=f"处理失败:{str(e)}")
  153. # --- 合并家具检测 JSON 并返回 ---
  154. print(f"\n[API] >>> 合并家具检测数据:{out_detect_dir}")
  155. furniture_data = merge_furniture_jsons(out_detect_dir)
  156. print(f"\n[API] >>> 处理完成:{scene_path},共 {len(furniture_data)} 张图片的检测结果")
  157. return {
  158. "status": "success",
  159. "message": "处理完成",
  160. "scene_path": scene_path,
  161. "output_dirs": {
  162. "room_output": out_room_dir,
  163. "furniture_output": out_detect_dir,
  164. },
  165. "furniture_data": furniture_data,
  166. }
  167. @app.get("/docs")
  168. def swagger_ui():
  169. from fastapi.responses import RedirectResponse
  170. return RedirectResponse(url="/docs")
  171. # ============================================================================
  172. # 启动入口
  173. # ============================================================================
  174. def parse_args():
  175. import argparse
  176. parser = argparse.ArgumentParser(description="启动 Pipeline FastAPI 服务(返回合并后的家具检测结果)")
  177. parser.add_argument("--host", default="0.0.0.0", help="监听地址 (默认 0.0.0.0)")
  178. parser.add_argument("--port", type=int, default=8000, help="监听端口 (默认 8000)")
  179. parser.add_argument("--workers", type=int, default=1, help="工作进程数 (默认 1,长任务建议用 1)")
  180. return parser.parse_args()
  181. if __name__ == "__main__":
  182. args = parse_args()
  183. uvicorn.run(
  184. "api_server_with_result:app",
  185. host=args.host,
  186. port=args.port,
  187. workers=args.workers,
  188. log_level="info",
  189. )