service.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """与 HTTP 无关的单图调用封装;每次调用独立目录与计算进程。"""
  2. from __future__ import annotations
  3. import importlib.util
  4. import json
  5. import logging
  6. import os
  7. from pathlib import Path
  8. import subprocess
  9. import sys
  10. import tempfile
  11. import threading
  12. from typing import Literal
  13. import zipfile
  14. import io
  15. import config
  16. MAX_UPLOAD_BYTES = 20 * 1024 * 1024
  17. MAX_IMAGE_PIXELS = 16_000_000
  18. TIMEOUT_SECONDS = 180
  19. _slots = threading.BoundedSemaphore(config.ALGORITHM_MAX_PARALLEL)
  20. _ROOT = Path(__file__).resolve().parents[2]
  21. logger = logging.getLogger(__name__)
  22. class AlgorithmError(Exception):
  23. def __init__(self, code: str, message: str, status_code: int):
  24. super().__init__(message)
  25. self.code = code
  26. self.status_code = status_code
  27. def validate_image(content: bytes) -> str:
  28. if len(content) > MAX_UPLOAD_BYTES:
  29. raise AlgorithmError("image_too_large", "图片不能超过 20 MiB", 413)
  30. if content.startswith(b"\x89PNG\r\n\x1a\n"):
  31. suffix = ".png"
  32. elif content.startswith(b"\xff\xd8\xff"):
  33. suffix = ".jpg"
  34. else:
  35. raise AlgorithmError("invalid_image", "请上传有效的 PNG 或 JPEG 图片", 422)
  36. return suffix
  37. def check_dependencies() -> None:
  38. if any(importlib.util.find_spec(name) is None for name in ("numpy", "cv2")):
  39. raise AlgorithmError("algorithm_unavailable", "算法依赖未安装,请按项目锁文件安装依赖", 503)
  40. def execute_image(image_path: Path, result_dir: Path, *, detailed: bool = False,
  41. wait_for_slot: bool = False, stage_path: Path | None = None) -> dict:
  42. """在共享并行闸下计算单张图;批量任务可等待空位。"""
  43. check_dependencies()
  44. if not _slots.acquire(blocking=wait_for_slot):
  45. raise AlgorithmError("algorithm_busy", "算法正在处理其他图片,请稍后重试", 429)
  46. try:
  47. env = dict(os.environ, OPENCV_IO_MAX_IMAGE_PIXELS=str(MAX_IMAGE_PIXELS),
  48. OPENBLAS_NUM_THREADS="1", OMP_NUM_THREADS="1")
  49. command = [sys.executable, "-B", "-m", "algorithms.characteristic_curve_recognition.worker",
  50. str(image_path), str(result_dir), "1" if detailed else "0"]
  51. if stage_path is not None:
  52. command.append(str(stage_path))
  53. try:
  54. process = subprocess.run(command, cwd=_ROOT, env=env, capture_output=True, timeout=TIMEOUT_SECONDS)
  55. except subprocess.TimeoutExpired as exc:
  56. raise AlgorithmError("algorithm_timeout", "图片处理超过 180 秒,请缩小图片后重试", 504) from exc
  57. if process.returncode:
  58. logger.error("特性曲线识别失败:%s", process.stderr.decode("utf-8", errors="replace")[-4000:])
  59. if process.returncode == 2:
  60. raise AlgorithmError("invalid_image", "图片损坏、尺寸超限或小于 32×32 像素", 422)
  61. if process.returncode == 3:
  62. raise AlgorithmError("detection_failed", "无法识别图表,请确认图片含清晰的坐标轴和曲线", 422)
  63. raise AlgorithmError("algorithm_failed", "算法处理失败,请通过服务日志排查", 500)
  64. result = {
  65. "algorithm": "characteristic_curve_recognition",
  66. "model": json.loads((result_dir / "final_model.json").read_text()),
  67. "geometry": json.loads((result_dir / "diagnostics/geometry.json").read_text()),
  68. "timing": json.loads((result_dir / "diagnostics/timing.json").read_text()),
  69. }
  70. (result_dir / "result.json").write_text(json.dumps(result, ensure_ascii=False, allow_nan=False), encoding="utf-8")
  71. return result
  72. finally:
  73. _slots.release()
  74. def analyze_image(content: bytes, *, detailed: bool = False,
  75. output: Literal["json", "zip"] = "json") -> dict | bytes:
  76. """上传 PNG/JPEG,返回模型与几何结果,或可离线查看的 ZIP。"""
  77. if output not in {"json", "zip"}:
  78. raise ValueError("output 必须为 json 或 zip")
  79. suffix = validate_image(content)
  80. with tempfile.TemporaryDirectory(prefix="vent-characteristic-curve-") as temporary:
  81. root = Path(temporary)
  82. image_path = root / ("input" + suffix)
  83. image_path.write_bytes(content)
  84. result_dir = root / "result"
  85. result = execute_image(image_path, result_dir, detailed=detailed)
  86. if output == "json":
  87. return result
  88. archive = io.BytesIO()
  89. with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zip_file:
  90. for path in sorted(result_dir.rglob("*")):
  91. if path.is_file():
  92. zip_file.write(path, path.relative_to(result_dir).as_posix())
  93. return archive.getvalue()