update web workflow and preview behavior

This commit is contained in:
2026-05-03 05:41:22 +08:00
parent f4bde6460a
commit 149cbc95d3
5 changed files with 339 additions and 78 deletions

View File

@@ -360,6 +360,91 @@ def add_dicom_from_zip(target_dir, zip_filename, payload, start_index):
return count
def validate_single_dicom_series(dicom_dir):
series_counts = {}
for dicom_path in Path(dicom_dir).glob("*.dcm"):
try:
ds = pydicom.dcmread(str(dicom_path), stop_before_pixels=True, force=True)
except Exception as exc:
raise RuntimeError(f"{dicom_path.name} 不是可读取的 DICOM 文件。") from exc
series_uid = str(getattr(ds, "SeriesInstanceUID", "") or "NO_SERIES_UID")
series_counts[series_uid] = series_counts.get(series_uid, 0) + 1
if len(series_counts) > 1:
series_summary = ", ".join(str(count) for count in sorted(series_counts.values(), reverse=True))
raise RuntimeError(
"上传内容包含多个 DICOM 序列,不能作为一个影像库数据集导入。"
f"检测到 {len(series_counts)} 个序列,分别约 {series_summary} 张。"
"请上传单个序列文件夹/ZIP。"
)
def reset_demo_environment():
source_dir = APP_DIR / "Ori_Head_CT"
source_zip = APP_DIR / "Ori_Head_CT.zip"
demo_root = LIBRARY_DIR / "demo_ori_head_ct"
demo_dicom_dir = demo_root / "dicom"
if not source_dir.exists() and not source_zip.exists():
raise RuntimeError("未找到 Ori_Head_CT 或 Ori_Head_CT.zip无法恢复演示环境。")
safe_mkdir(LIBRARY_DIR)
for child in LIBRARY_DIR.iterdir():
if child.is_dir():
shutil.rmtree(child)
else:
child.unlink()
safe_mkdir(demo_dicom_dir)
copied = 0
if source_dir.exists():
for dicom_path in sorted(source_dir.glob("*.dcm")):
shutil.copy2(dicom_path, demo_dicom_dir / dicom_path.name)
copied += 1
else:
with zipfile.ZipFile(source_zip) as archive:
for member in archive.infolist():
if member.is_dir():
continue
member_name = member.filename.replace("\\", "/")
if Path(member_name).suffix.lower() != ".dcm":
continue
if Path(member_name).is_absolute() or ".." in Path(member_name).parts:
continue
copied += 1
with archive.open(member) as member_file:
write_dicom_payload(demo_dicom_dir, Path(member_name).name, member_file.read(), copied)
if copied == 0:
shutil.rmtree(demo_root)
raise RuntimeError("Ori_Head_CT 中没有找到 .dcm 文件。")
validate_single_dicom_series(demo_dicom_dir)
record = build_library_record(
"demo_ori_head_ct",
"Ori_Head_CT",
demo_dicom_dir,
source="upload",
version="DICOM-DEMO",
)
write_library_meta([record])
DICOM_FILE_CACHE.clear()
if RESULT_DIR.exists():
shutil.rmtree(RESULT_DIR)
safe_mkdir(RESULT_DIR)
with JOBS_LOCK:
JOBS.clear()
persist_jobs_locked()
write_json_file(USER_TASKS_META, {})
return {
"ok": True,
"message": "演示环境已恢复出厂设置。",
"items": list_library(),
}
def upload_library_item(headers, body):
fields, files = parse_multipart(headers, body)
dicom_files = [
@@ -398,6 +483,11 @@ def upload_library_item(headers, body):
if total_dicom_count == 0:
shutil.rmtree(target_dir.parent)
raise RuntimeError("压缩包里没有找到 .dcm 文件。")
try:
validate_single_dicom_series(target_dir)
except Exception:
shutil.rmtree(target_dir.parent)
raise
record = build_library_record(
item_id,
@@ -547,11 +637,26 @@ def start_job(kind, worker, owner=None, params=None, remember_user_task=True):
return get_job(job_id)
def make_preview(input_dir, angle_degrees, show_cutoff_line=True):
def make_preview(
input_dir,
angle_degrees,
show_cutoff_line=True,
transition_width=90,
mode="soft_transition",
gaussian_sigma=3,
):
if mode not in {"hard_boundary", "gaussian_smooth", "soft_transition"}:
mode = "soft_transition"
volume = load_dicom_volume(input_dir)
before = crop_head_neck(sagittal_mip(volume))
before_display = draw_cutoff_line(before, volume.shape[0]) if show_cutoff_line else before
after = preview_deform_2d(before_display, float(angle_degrees))
after = preview_deform_2d(
before_display,
float(angle_degrees),
mode,
transition_width=float(transition_width),
gaussian_sigma=float(gaussian_sigma),
)
canvas_image = Image.new("RGB", (1440, 520), (0, 0, 0))
canvas_image.paste(fit_image(before_display, 700, 520), (0, 0))
@@ -779,12 +884,19 @@ class Handler(BaseHTTPRequestHandler):
return
body = self.read_json()
if parsed.path == "/api/demo/reset":
self.send_json(reset_demo_environment())
return
if parsed.path == "/api/preview":
self.send_json(
make_preview(
body["inputDir"],
body.get("angleDegrees", 12),
bool(body.get("showCutoffLine", True)),
body.get("transitionWidth", 90),
body.get("mode", "soft_transition"),
body.get("gaussianSigma", 3),
)
)
return
@@ -865,13 +977,16 @@ class Handler(BaseHTTPRequestHandler):
max_angle = float(body.get("maxAngle", 20))
duration = float(body.get("durationSeconds", 6))
show_arrow = bool(body.get("showArrow", True))
mode = body.get("mode", "hard_boundary")
if mode not in {"hard_boundary", "gaussian_smooth", "soft_transition"}:
mode = "hard_boundary"
def worker(job_id):
job_root = RESULT_DIR / job_id
reset_dir(job_root)
output_file = job_root / f"head_extension_{job_id}.mp4"
set_job(job_id, message="正在生成 0° 到目标角度的视频。")
output = generate_video(input_dir, output_file, max_angle, duration, show_arrow)
output_file = job_root / f"head_extension_{mode}_{job_id}.mp4"
set_job(job_id, message=f"正在生成 {mode} 视频。")
output = generate_video(input_dir, output_file, max_angle, duration, show_arrow, mode)
output = Path(output).resolve()
return {
"video": {