2026-05-18-17-55-29 增加运行确认和演示录制
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,4 +7,5 @@ __pycache__/
|
||||
venv/
|
||||
storage/jobs/
|
||||
storage/uploads/
|
||||
storage/demos/
|
||||
*.log
|
||||
|
||||
20
README.md
20
README.md
@@ -23,12 +23,32 @@ bash scripts/run_dev.sh
|
||||
http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
如 8000 端口已被占用,可使用:
|
||||
|
||||
```bash
|
||||
PORT=8001 bash scripts/run_dev.sh
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
pytest -q
|
||||
```
|
||||
|
||||
## 录制演示视频
|
||||
|
||||
服务运行后执行:
|
||||
|
||||
```bash
|
||||
python3 scripts/record_demo.py
|
||||
```
|
||||
|
||||
输出文件:
|
||||
|
||||
```text
|
||||
storage/demos/isiseg_usage_demo.mp4
|
||||
```
|
||||
|
||||
## 目录
|
||||
|
||||
- `backend/`:FastAPI 服务与分割算法。
|
||||
|
||||
189
scripts/record_demo.py
Normal file
189
scripts/record_demo.py
Normal 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()
|
||||
24
工程分析/实现方案-2026-05-18-17-55-29.md
Normal file
24
工程分析/实现方案-2026-05-18-17-55-29.md
Normal 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,避免仓库体积快速膨胀;脚本提交,任何时候都能重新录制。
|
||||
25
工程分析/测试方案-2026-05-18-17-55-29.md
Normal file
25
工程分析/测试方案-2026-05-18-17-55-29.md
Normal 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 个测试全部通过。
|
||||
12
工程分析/经验记录.md
12
工程分析/经验记录.md
@@ -33,3 +33,15 @@ B. 产生问题原因:时序差分在无前帧时退化为 Hessian,融合投
|
||||
C. 解决问题方案:融合模式统一使用两票通过,低响应时才回退到一票;同时将边缘形态学改成暗线候选约束下的边缘检测。
|
||||
|
||||
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 并预留下载时间。
|
||||
|
||||
21
工程分析/需求分析-2026-05-18-17-55-29.md
Normal file
21
工程分析/需求分析-2026-05-18-17-55-29.md
Normal 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,后续可重复录制。
|
||||
Reference in New Issue
Block a user