update web workflow and preview behavior
This commit is contained in:
125
web_backend.py
125
web_backend.py
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user