From bef76448f70adbca42e12e5bf71ef726615820e6 Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Sat, 2 May 2026 23:49:43 +0800 Subject: [PATCH] speed up DICOM slice previews --- WebSite/src/App.tsx | 21 ++++++++++++++---- web_backend.py | 54 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/WebSite/src/App.tsx b/WebSite/src/App.tsx index 17a06f7..81a13c5 100644 --- a/WebSite/src/App.tsx +++ b/WebSite/src/App.tsx @@ -72,26 +72,39 @@ const API_BASE = typeof window === 'undefined' function LibraryDicomPreview({ item }: { item: LibraryItem }) { const [sliceIndex, setSliceIndex] = useState(Math.max(0, Math.floor((item.fileCount || 1) / 2))); + const [requestedSliceIndex, setRequestedSliceIndex] = useState(sliceIndex); const [previewImage, setPreviewImage] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); const count = Math.max(1, item.fileCount || 1); useEffect(() => { - setSliceIndex(Math.max(0, Math.floor((item.fileCount || 1) / 2))); + const middleIndex = Math.max(0, Math.floor((item.fileCount || 1) / 2)); + setSliceIndex(middleIndex); + setRequestedSliceIndex(middleIndex); }, [item.id, item.fileCount]); + useEffect(() => { + const timer = window.setTimeout(() => { + setRequestedSliceIndex(sliceIndex); + }, 120); + return () => window.clearTimeout(timer); + }, [sliceIndex]); + useEffect(() => { const controller = new AbortController(); setIsLoading(true); setError(''); - fetch(`${API_BASE}/api/library/preview?id=${encodeURIComponent(item.id)}&index=${sliceIndex}`, { + fetch(`${API_BASE}/api/library/preview?id=${encodeURIComponent(item.id)}&index=${requestedSliceIndex}`, { signal: controller.signal }) .then(async response => { const data = await response.json(); if (!response.ok) throw new Error(data.error || '预览生成失败'); - setPreviewImage(data.image); + setPreviewImage(`${API_BASE}${data.imageUrl}`); + (data.neighbors || []).forEach((url: string) => { + fetch(`${API_BASE}${url}`, { signal: controller.signal }).catch(() => {}); + }); }) .catch(error => { if ((error as Error).name !== 'AbortError') setError((error as Error).message); @@ -100,7 +113,7 @@ function LibraryDicomPreview({ item }: { item: LibraryItem }) { if (!controller.signal.aborted) setIsLoading(false); }); return () => controller.abort(); - }, [item.id, sliceIndex]); + }, [item.id, requestedSliceIndex]); return (
diff --git a/web_backend.py b/web_backend.py index 24ef35a..5f14988 100644 --- a/web_backend.py +++ b/web_backend.py @@ -12,7 +12,7 @@ from email.policy import default from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from io import BytesIO from pathlib import Path -from urllib.parse import parse_qs, unquote, urlparse +from urllib.parse import parse_qs, quote, unquote, urlparse os.environ.setdefault("MPLCONFIGDIR", "/tmp/head_ct_morph_matplotlib") @@ -37,9 +37,11 @@ HOST = "0.0.0.0" PORT = 8787 JOBS = {} JOBS_LOCK = threading.Lock() +DICOM_FILE_CACHE = {} LIBRARY_DIR = APP_DIR / "web_library" LIBRARY_META = LIBRARY_DIR / "library.json" RESULT_DIR = APP_DIR / "web_results" +PREVIEW_CACHE_DIR = LIBRARY_DIR / "_preview_cache" def json_default(value): @@ -124,7 +126,23 @@ def sort_key_for_dicom(path): def sorted_dicom_files(dicom_dir): - return sorted(Path(dicom_dir).glob("*.dcm"), key=sort_key_for_dicom) + dicom_dir = Path(dicom_dir).resolve() + files = list(dicom_dir.glob("*.dcm")) + signature = ( + str(dicom_dir), + len(files), + max((file_path.stat().st_mtime for file_path in files), default=0), + ) + cached = DICOM_FILE_CACHE.get(str(dicom_dir)) + if cached and cached["signature"] == signature: + return cached["files"] + + sorted_files = sorted(files, key=sort_key_for_dicom) + DICOM_FILE_CACHE[str(dicom_dir)] = { + "signature": signature, + "files": sorted_files, + } + return sorted_files def find_library_item(item_id): @@ -142,22 +160,30 @@ def make_library_slice_preview(item_id, index): count = len(dicom_files) index = max(0, min(int(index), count - 1)) - ds = pydicom.dcmread(str(dicom_files[index]), force=True) - image = ds.pixel_array.astype("float32") - image = image * float(getattr(ds, "RescaleSlope", 1)) - image = image + float(getattr(ds, "RescaleIntercept", 0)) + cache_dir = PREVIEW_CACHE_DIR / item_id + safe_mkdir(cache_dir) + preview_path = cache_dir / f"slice_{index:04d}.png" + if not preview_path.exists(): + ds = pydicom.dcmread(str(dicom_files[index]), force=True) + image = ds.pixel_array.astype("float32") + image = image * float(getattr(ds, "RescaleSlope", 1)) + image = image + float(getattr(ds, "RescaleIntercept", 0)) - preview = Image.fromarray(ct_window(image)).convert("RGB") - preview = fit_image(preview, 720, 520) - canvas = BytesIO() - preview.save(canvas, format="PNG") - encoded = base64.b64encode(canvas.getvalue()).decode("ascii") + preview = Image.fromarray(ct_window(image)).convert("RGB") + preview = fit_image(preview, 720, 520) + preview.save(preview_path, format="PNG") + + neighbors = [value for value in [index - 1, index + 1] if 0 <= value < count] return { - "image": f"data:image/png;base64,{encoded}", + "imageUrl": f"/api/file?path={quote(str(preview_path.resolve()), safe='')}", "index": index, "count": count, "file": dicom_files[index].name, "patientId": item["patientId"], + "neighbors": [ + f"/api/library/preview?id={item_id}&index={neighbor}" + for neighbor in neighbors + ], } @@ -522,7 +548,11 @@ class Handler(BaseHTTPRequestHandler): write_library_meta(remaining) upload_root = Path(target["dicomPath"]).resolve().parent if upload_root.exists() and upload_root.is_relative_to(LIBRARY_DIR.resolve()): + DICOM_FILE_CACHE.pop(str(Path(target["dicomPath"]).resolve()), None) shutil.rmtree(upload_root) + preview_cache = PREVIEW_CACHE_DIR / item_id + if preview_cache.exists(): + shutil.rmtree(preview_cache) self.send_json({"ok": True, "items": remaining}) def read_bytes(self):