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):