diff --git a/WebSite/src/App.tsx b/WebSite/src/App.tsx index 4364098..17a06f7 100644 --- a/WebSite/src/App.tsx +++ b/WebSite/src/App.tsx @@ -70,6 +70,77 @@ const API_BASE = typeof window === 'undefined' ? 'http://127.0.0.1:8787' : `${window.location.protocol}//${window.location.hostname}:8787`; +function LibraryDicomPreview({ item }: { item: LibraryItem }) { + const [sliceIndex, setSliceIndex] = useState(Math.max(0, Math.floor((item.fileCount || 1) / 2))); + 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))); + }, [item.id, item.fileCount]); + + useEffect(() => { + const controller = new AbortController(); + setIsLoading(true); + setError(''); + fetch(`${API_BASE}/api/library/preview?id=${encodeURIComponent(item.id)}&index=${sliceIndex}`, { + signal: controller.signal + }) + .then(async response => { + const data = await response.json(); + if (!response.ok) throw new Error(data.error || '预览生成失败'); + setPreviewImage(data.image); + }) + .catch(error => { + if ((error as Error).name !== 'AbortError') setError((error as Error).message); + }) + .finally(() => { + if (!controller.signal.aborted) setIsLoading(false); + }); + return () => controller.abort(); + }, [item.id, sliceIndex]); + + return ( +
+
+
+
+
+
+ + {item.status.toUpperCase()} + +
+ + {previewImage && !error ? ( + + ) : ( +
+ +

{error || (isLoading ? '正在生成预览...' : '等待预览')}

+
+ )} + +
+
+ Axial DICOM + {sliceIndex + 1} / {count} +
+ setSliceIndex(parseInt(event.target.value, 10))} + className="w-full h-1 accent-blue-500 cursor-pointer" + /> +
+
+ ); +} + export default function App() { // --- Authentication State --- const [isLoggedIn, setIsLoggedIn] = useState(false); @@ -798,28 +869,7 @@ export default function App() {
{libraryData.map(item => (
- {/* Simulated Preview Box */} -
-
-
-
-
- {/* Abstract DICOM Scan Visualization */} -
-
-
-
-
-
-
- Axial View :: ID_{item.id.toUpperCase()} -
-
- - {item.status.toUpperCase()} - -
-
+
diff --git a/web_backend.py b/web_backend.py index ed09285..24ef35a 100644 --- a/web_backend.py +++ b/web_backend.py @@ -16,10 +16,14 @@ from urllib.parse import parse_qs, unquote, urlparse os.environ.setdefault("MPLCONFIGDIR", "/tmp/head_ct_morph_matplotlib") +import pydicom +from PIL import Image + from generate_head_extension_video import generate_video from head_extension_app import ( APP_DIR, crop_head_neck, + ct_window, fit_image, load_dicom_volume, preview_deform_2d, @@ -109,6 +113,54 @@ def list_library(): return live_items +def sort_key_for_dicom(path): + path = Path(path) + try: + ds = pydicom.dcmread(str(path), stop_before_pixels=True, force=True) + return (0, int(getattr(ds, "InstanceNumber", 0)), path.name) + except Exception: + stem = path.stem + return (1, int(stem) if stem.isdigit() else 0, path.name) + + +def sorted_dicom_files(dicom_dir): + return sorted(Path(dicom_dir).glob("*.dcm"), key=sort_key_for_dicom) + + +def find_library_item(item_id): + return next((item for item in list_library() if item["id"] == item_id), None) + + +def make_library_slice_preview(item_id, index): + item = find_library_item(item_id) + if not item: + raise RuntimeError("影像库中没有找到该数据。") + + dicom_files = sorted_dicom_files(item["dicomPath"]) + if not dicom_files: + raise RuntimeError("该影像数据没有可预览的 .dcm 文件。") + + 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)) + + 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") + return { + "image": f"data:image/png;base64,{encoded}", + "index": index, + "count": count, + "file": dicom_files[index].name, + "patientId": item["patientId"], + } + + def parse_multipart(headers, body): content_type = headers.get("content-type", "") message = BytesParser(policy=default).parsebytes( @@ -333,6 +385,13 @@ class Handler(BaseHTTPRequestHandler): self.send_json({"items": list_library()}) return + if parsed.path == "/api/library/preview": + params = parse_qs(parsed.query) + item_id = params.get("id", [""])[0] + index = params.get("index", ["0"])[0] + self.send_json(make_library_slice_preview(item_id, index)) + return + if parsed.path == "/api/job": params = parse_qs(parsed.query) job_id = params.get("id", [""])[0]