2026-05-18-17-55-29 增加运行确认和演示录制

This commit is contained in:
2026-05-18 17:56:22 +08:00
parent debd0bfd50
commit dd2a49ad91
7 changed files with 292 additions and 0 deletions

1
.gitignore vendored
View File

@@ -7,4 +7,5 @@ __pycache__/
venv/ venv/
storage/jobs/ storage/jobs/
storage/uploads/ storage/uploads/
storage/demos/
*.log *.log

View File

@@ -23,12 +23,32 @@ bash scripts/run_dev.sh
http://127.0.0.1:8000 http://127.0.0.1:8000
``` ```
如 8000 端口已被占用,可使用:
```bash
PORT=8001 bash scripts/run_dev.sh
```
## 测试 ## 测试
```bash ```bash
pytest -q pytest -q
``` ```
## 录制演示视频
服务运行后执行:
```bash
python3 scripts/record_demo.py
```
输出文件:
```text
storage/demos/isiseg_usage_demo.mp4
```
## 目录 ## 目录
- `backend/`FastAPI 服务与分割算法。 - `backend/`FastAPI 服务与分割算法。

189
scripts/record_demo.py Normal file
View File

@@ -0,0 +1,189 @@
from __future__ import annotations
import json
import shutil
import subprocess
import textwrap
import time
import urllib.request
from pathlib import Path
import cv2
import numpy as np
ROOT = Path(__file__).resolve().parents[1]
DEMO_DIR = ROOT / "storage" / "demos"
SAMPLE_VIDEO = ROOT / "storage" / "samples" / "synthetic_guidewire.mp4"
HOME_SCREENSHOT = DEMO_DIR / "01_home.png"
RESPONSE_JSON = DEMO_DIR / "latest_demo_response.json"
OUTPUT_VIDEO = DEMO_DIR / "isiseg_usage_demo.mp4"
BASE_URL = "http://127.0.0.1:8001"
def run(command: list[str], timeout: int = 120) -> subprocess.CompletedProcess[str]:
return subprocess.run(
command,
cwd=ROOT,
check=True,
text=True,
capture_output=True,
timeout=timeout,
)
def wait_for_service() -> None:
deadline = time.time() + 20
while time.time() < deadline:
try:
with urllib.request.urlopen(f"{BASE_URL}/api/health", timeout=2) as response:
if response.status == 200:
return
except OSError:
time.sleep(0.5)
raise RuntimeError(f"ISISeg service is not reachable at {BASE_URL}")
def ensure_sample() -> None:
if not SAMPLE_VIDEO.exists():
run(["bash", "scripts/generate_sample.sh"])
def capture_homepage() -> None:
chrome = shutil.which("google-chrome") or shutil.which("chromium") or shutil.which("chromium-browser")
if not chrome:
raise RuntimeError("Chrome/Chromium is required to capture the UI screenshot")
run(
[
chrome,
"--headless=new",
"--no-sandbox",
"--disable-gpu",
"--hide-scrollbars",
"--window-size=1440,1000",
f"--screenshot={HOME_SCREENSHOT}",
BASE_URL,
],
timeout=60,
)
def call_segmentation() -> dict:
command = [
"curl",
"-s",
"-X",
"POST",
"-F",
f"file=@{SAMPLE_VIDEO}",
"-F",
"method=fusion",
"-F",
"sensitivity=0.68",
"-F",
"frame_stride=12",
"-F",
"max_frames=4",
f"{BASE_URL}/api/segment",
]
completed = run(command, timeout=180)
payload = json.loads(completed.stdout)
RESPONSE_JSON.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
return payload
def fit_image(image: np.ndarray, box_w: int, box_h: int) -> np.ndarray:
h, w = image.shape[:2]
scale = min(box_w / w, box_h / h)
resized = cv2.resize(image, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA)
canvas = np.full((box_h, box_w, 3), 18, dtype=np.uint8)
y = (box_h - resized.shape[0]) // 2
x = (box_w - resized.shape[1]) // 2
canvas[y : y + resized.shape[0], x : x + resized.shape[1]] = resized
return canvas
def put_lines(frame: np.ndarray, lines: list[str], x: int, y: int, size: float = 0.82, color=(238, 245, 239)) -> None:
for line in lines:
cv2.putText(frame, line, (x, y), cv2.FONT_HERSHEY_SIMPLEX, size, color, 2, cv2.LINE_AA)
y += int(34 * size / 0.82)
def wrap(text: str, width: int = 44) -> list[str]:
return textwrap.wrap(text, width=width, break_long_words=False)
def slate(title: str, subtitle: str, width: int = 1280, height: int = 720) -> np.ndarray:
frame = np.zeros((height, width, 3), dtype=np.uint8)
frame[:, :] = (18, 20, 18)
cv2.rectangle(frame, (0, 0), (width, height), (48, 64, 56), 18)
cv2.line(frame, (70, 90), (1210, 90), (102, 209, 185), 3)
cv2.putText(frame, title, (70, 185), cv2.FONT_HERSHEY_SIMPLEX, 1.35, (255, 209, 102), 3, cv2.LINE_AA)
put_lines(frame, wrap(subtitle, 58), 74, 260, size=0.82, color=(226, 234, 226))
cv2.putText(frame, BASE_URL, (74, 640), cv2.FONT_HERSHEY_SIMPLEX, 0.78, (102, 209, 185), 2, cv2.LINE_AA)
return frame
def screenshot_slide() -> np.ndarray:
image = cv2.imread(str(HOME_SCREENSHOT), cv2.IMREAD_COLOR)
frame = slate("1. Open Web Console", "Browser opens the ISISeg guidewire segmentation workspace.")
fitted = fit_image(image, 1120, 500)
frame[160:660, 80:1200] = fitted
return frame
def result_slide(payload: dict, frame_payload: dict, index: int) -> np.ndarray:
frame = slate(
f"2.{index} Segmentation Result",
"The demo uploads the synthetic guidewire video and renders real fusion masks returned by the API.",
)
overlay = cv2.imread(str(ROOT / frame_payload["overlay_url"].lstrip("/")), cv2.IMREAD_COLOR)
mask = cv2.imread(str(ROOT / frame_payload["mask_url"].lstrip("/")), cv2.IMREAD_GRAYSCALE)
mask_color = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
frame[178:598, 75:635] = fit_image(overlay, 560, 420)
frame[178:598, 645:1205] = fit_image(mask_color, 560, 420)
metrics = frame_payload["metrics"]
lines = [
f"job: {payload['job_id']} frame: {frame_payload['frame_index']}",
f"coverage: {metrics['coverage'] * 100:.3f}% skeleton: {metrics['skeleton_length']} components: {metrics['components']}",
]
put_lines(frame, lines, 75, 650, size=0.62, color=(238, 245, 239))
return frame
def write_video(slides: list[np.ndarray]) -> None:
writer = cv2.VideoWriter(str(OUTPUT_VIDEO), cv2.VideoWriter_fourcc(*"mp4v"), 24.0, (1280, 720))
for slide in slides:
for _ in range(72):
writer.write(slide)
writer.release()
def main() -> None:
DEMO_DIR.mkdir(parents=True, exist_ok=True)
wait_for_service()
ensure_sample()
capture_homepage()
payload = call_segmentation()
slides = [
slate(
"ISISeg Usage Demo",
"A runnable Web system for guidewire segmentation: open the console, upload a fluoroscopy-like video, choose fusion mode, and inspect overlay/mask outputs.",
),
screenshot_slide(),
]
for index, frame_payload in enumerate(payload["frames"][:4], start=1):
slides.append(result_slide(payload, frame_payload, index))
slides.append(
slate(
"Ready to Use",
f"Demo output video and API artifacts are stored under storage/demos and storage/jobs. Overlay video URL: {payload.get('video_url')}",
)
)
write_video(slides)
print(OUTPUT_VIDEO)
print(RESPONSE_JSON)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,24 @@
# 实现方案
开始时间2026-05-18-17-55-29
## 方案
新增 `scripts/record_demo.py`,在不引入额外大型依赖的情况下生成演示视频:
1. 等待并检查 `http://127.0.0.1:8001/api/health`
2. 如样例视频不存在,则调用 `scripts/generate_sample.sh` 生成合成导丝视频。
3. 使用本机 Chrome headless 对 Web 首页截图。
4. 使用 `curl` 调用 `POST /api/segment` 上传 `storage/samples/synthetic_guidewire.mp4`
5. 读取 API 返回的真实叠加图和掩膜图。
6. 使用 OpenCV 将首页截图、分割结果和指标合成为 mp4。
## 输出路径
- 演示视频:`storage/demos/isiseg_usage_demo.mp4`
- API 响应记录:`storage/demos/latest_demo_response.json`
## 设计选择
- 当前环境没有显示器会话也没有可立即使用的浏览器自动化库Playwright 下载耗时异常,因此改用系统已有 Chrome headless 和 OpenCV 合成可复现演示。
- 演示视频不提交到 git避免仓库体积快速膨胀脚本提交任何时候都能重新录制。

View File

@@ -0,0 +1,25 @@
# 测试方案
开始时间2026-05-18-17-55-29
## 测试步骤
1. 服务健康检查:
- `curl -s http://127.0.0.1:8001/api/health`
2. 演示视频录制:
- `python3 scripts/record_demo.py`
3. 视频文件校验:
- `ls -lh storage/demos/isiseg_usage_demo.mp4`
- `ffprobe -v error -show_entries format=duration,size -of default=noprint_wrappers=1 storage/demos/isiseg_usage_demo.mp4`
4. 自动化回归:
- `pytest -q`
## 执行结果
- 健康检查:通过,返回 `{"status":"ok","service":"ISISeg","version":"0.1.0"}`
- 演示视频录制:通过,生成 `storage/demos/isiseg_usage_demo.mp4`
- 视频校验:通过,文件大小约 3.4 MB时长 21 秒。
- 自动化回归通过4 个测试全部通过。

View File

@@ -33,3 +33,15 @@ B. 产生问题原因:时序差分在无前帧时退化为 Hessian融合投
C. 解决问题方案:融合模式统一使用两票通过,低响应时才回退到一票;同时将边缘形态学改成暗线候选约束下的边缘检测。 C. 解决问题方案:融合模式统一使用两票通过,低响应时才回退到一票;同时将边缘形态学改成暗线候选约束下的边缘检测。
D. 后续如何避免问题:视频算法要单独检查首帧、丢帧和单图退化路径;融合策略必须记录每个子方法的置信约束。 D. 后续如何避免问题:视频算法要单独检查首帧、丢帧和单图退化路径;融合策略必须记录每个子方法的置信约束。
## 2026-05-18-17-55-29 运行确认与演示视频录制
### 1. Playwright 安装下载卡住
A. 具体问题:尝试安装 `playwright==1.55.0` 用于浏览器录屏时pip 长时间停留在大型 wheel 下载阶段,没有继续输出。
B. 产生问题原因Playwright 包体积较大,当前网络下载速度或连接稳定性不足;继续等待会影响本次“确保可运行”的收口。
C. 解决问题方案:终止该安装进程,改用系统已有 `google-chrome --headless` 截取首页,并使用当前已安装的 OpenCV 将真实页面截图和真实 API 分割结果合成为演示 mp4。
D. 后续如何避免问题:演示录制优先复用本机已有工具;只有确实需要完整浏览器交互录屏时,再单独安装 Playwright 并预留下载时间。

View File

@@ -0,0 +1,21 @@
# 需求分析
开始时间2026-05-18-17-55-29
## 用户目标
在第一版导丝分割 Web 系统完成后,进一步确认系统最终一定能够运行,并生成一段使用演示视频,方便后续查看或展示系统操作流程。
## 需求范围
- 复核当前服务是否仍在运行。
- 保留可重复执行的演示视频录制方式。
- 录制一段本地演示视频,素材必须来自当前系统真实页面和真实 API 分割结果。
- 完成后再次测试系统可用性,并提交到 Gitea 备份。
## 验收点
- `http://127.0.0.1:8001/api/health` 返回 `ok`
- `pytest -q` 通过。
- 存在演示视频 `storage/demos/isiseg_usage_demo.mp4`
- 演示脚本路径写入 README后续可重复录制。