diff --git a/apps/phoenix/src/App.tsx b/apps/phoenix/src/App.tsx index 20b66de..6e46a8a 100644 --- a/apps/phoenix/src/App.tsx +++ b/apps/phoenix/src/App.tsx @@ -1,42 +1,85 @@ -import { - readImageDicomFileSeries, - structuredReportToHtml, -} from "@itk-wasm/dicom"; +import { useEffect, useRef, useState } from "react"; +import { readImageDicomFileSeries } from "@itk-wasm/dicom"; import vtkITKHelper from "@kitware/vtk.js/Common/DataModel/ITKHelper"; import vtkVolume from "@kitware/vtk.js/Rendering/Core/Volume"; import vtkFullScreenRenderWindow from "@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow"; import "@kitware/vtk.js/Rendering/Profiles/Volume"; -import { useEffect, useRef, useState } from "react"; import vtkRenderer from "@kitware/vtk.js/Rendering/Core/Renderer"; import vtkVolumeMapper from "@kitware/vtk.js/Rendering/Core/VolumeMapper"; import vtkRenderWindow from "@kitware/vtk.js/Rendering/Core/RenderWindow"; +import "./app.css"; +import { + checkExistenceInIndexedDB, + loadImageFromIndexedDB, + saveImageToIndexedDB, +} from "./util"; +import { extractAxialSliceAsImage } from "./extract"; + function App() { const [files, setFiles] = useState([]); const inputRef = useRef(null); + + const axialRef = useRef(null); + const sagittalRef = useRef(null); + const coronalRef = useRef(null); + const rendererRef = useRef(); const mapperRef = useRef(); const actorRef = useRef(); const renderWindowRef = useRef(); + const [image, setImage] = useState(); + useEffect(() => { - if (inputRef.current) { - inputRef.current.webkitdirectory = true; - } + if (inputRef.current) inputRef.current.webkitdirectory = true; + }, []); + + useEffect(() => { + checkExistenceInIndexedDB("outputImageKey").then((exist) => { + if (exist) { + console.time("start load from cache"); + loadImageFromIndexedDB().then((outputImage) => { + const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ + container: axialRef.current, + }); + rendererRef.current = fullScreenRenderer.getRenderer(); + renderWindowRef.current = fullScreenRenderer.getRenderWindow(); + actorRef.current = vtkVolume.newInstance(); + mapperRef.current = vtkVolumeMapper.newInstance({ + sampleDistance: 1.1, + }); + const vtkImage = vtkITKHelper.convertItkToVtkImage(outputImage); + mapperRef.current?.setInputData(vtkImage); + mapperRef.current && actorRef.current?.setMapper(mapperRef.current); + actorRef.current && rendererRef.current?.addVolume(actorRef.current); + rendererRef.current?.resetCamera(); + renderWindowRef.current?.render(); + console.timeEnd("start load from cache"); + + const imgSource = extractAxialSliceAsImage(vtkImage, 1); + setImage(imgSource); + }); + } + }); }, []); useEffect(() => { if (files.length > 0) { - const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({}); + const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ + container: axialRef.current, + }); rendererRef.current = fullScreenRenderer.getRenderer(); renderWindowRef.current = fullScreenRenderer.getRenderWindow(); actorRef.current = vtkVolume.newInstance(); mapperRef.current = vtkVolumeMapper.newInstance({ sampleDistance: 1.1 }); - + console.time("itk-wasm parse vtkVolumeImageData"); readImageDicomFileSeries(null, { inputImages: files, }).then((image) => { const vtkImage = vtkITKHelper.convertItkToVtkImage(image.outputImage); + console.timeEnd("itk-wasm parse vtkVolumeImageData"); + saveImageToIndexedDB(image.outputImage); mapperRef.current?.setInputData(vtkImage); mapperRef.current && actorRef.current?.setMapper(mapperRef.current); actorRef.current && rendererRef.current?.addVolume(actorRef.current); @@ -72,6 +115,12 @@ function App() { ref={inputRef} style={{ position: "fixed", zIndex: 100 }} /> +
+
+ {/*
+
*/} +
+ ); } diff --git a/apps/phoenix/src/app.css b/apps/phoenix/src/app.css new file mode 100644 index 0000000..973d646 --- /dev/null +++ b/apps/phoenix/src/app.css @@ -0,0 +1,8 @@ +.mpr { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/apps/phoenix/src/extract.ts b/apps/phoenix/src/extract.ts new file mode 100644 index 0000000..6109d13 --- /dev/null +++ b/apps/phoenix/src/extract.ts @@ -0,0 +1,38 @@ +import vtkImageData from "@kitware/vtk.js/Common/DataModel/ImageData"; + +export const extractAxialSliceAsImage = ( + volumeData: vtkImageData, + sliceIndex: number +): string => { + const dimensions = volumeData.getDimensions(); + + if (sliceIndex < 0 || sliceIndex >= dimensions[2]) { + throw new Error("Invalid slice index"); + } + + const scalars = volumeData.getPointData().getScalars(); + const slice = scalars + .getData() + .slice( + sliceIndex * dimensions[0] * dimensions[1], + (sliceIndex + 1) * dimensions[0] * dimensions[1] + ); + + const canvas = document.createElement("canvas"); + canvas.width = dimensions[0]; + canvas.height = dimensions[1]; + const context = canvas.getContext("2d")!; + const imageDataObj = context.createImageData(dimensions[0], dimensions[1]); + + for (let i = 0; i < slice.length; i++) { + const value = slice[i]; + imageDataObj.data[i * 4] = value; + imageDataObj.data[i * 4 + 1] = value; + imageDataObj.data[i * 4 + 2] = value; + imageDataObj.data[i * 4 + 3] = 255; + } + + context.putImageData(imageDataObj, 0, 0); + + return canvas.toDataURL(); +}; diff --git a/apps/phoenix/src/util.ts b/apps/phoenix/src/util.ts new file mode 100644 index 0000000..ce78ba0 --- /dev/null +++ b/apps/phoenix/src/util.ts @@ -0,0 +1,91 @@ +const dbName = "myDatabase"; +const storeName = "images"; + +export const openDatabase = async (): Promise => { + return new Promise((resolve, reject) => { + const request = indexedDB.open(dbName); + + request.onupgradeneeded = (event: IDBVersionChangeEvent) => { + const db = request.result; + if (!db.objectStoreNames.contains(storeName)) { + db.createObjectStore(storeName); + } + }; + + request.onerror = (event) => reject(request.error); + request.onsuccess = (event) => resolve(request.result); + }); +}; + +export const saveImageToIndexedDB = async (outputImage: any) => { + try { + const db = await openDatabase(); + + const transaction = db.transaction(storeName, "readwrite"); + const store = transaction.objectStore(storeName); + + const request = store.put(outputImage, "outputImageKey"); + request.onerror = (event) => + console.error("Error saving to IndexedDB", request.error); + request.onsuccess = (event) => console.log("Image saved to IndexedDB"); + } catch (error) { + console.error("Error with IndexedDB operation", error); + } +}; + +export const loadImageFromIndexedDB = async (): Promise => { + try { + const db = await openDatabase(); + + const transaction = db.transaction(storeName, "readonly"); + const store = transaction.objectStore(storeName); + + const request = store.get("outputImageKey"); + return new Promise((resolve, reject) => { + request.onerror = (event) => { + console.error("Error fetching from IndexedDB", request.error); + reject(request.error); + }; + request.onsuccess = (event) => { + if (request.result) { + console.log("Image loaded from IndexedDB"); + resolve(request.result); + } else { + console.log("No data found in IndexedDB"); + resolve(null); + } + }; + }); + } catch (error) { + console.error("Error with IndexedDB operation", error); + throw error; + } +}; + +export const checkExistenceInIndexedDB = async ( + key: string +): Promise => { + try { + const dbName = "myDatabase"; + const storeName = "images"; + const db = await openDatabase(); + + const transaction = db.transaction(storeName, "readonly"); + const store = transaction.objectStore(storeName); + + const request = store.get(key); + + return new Promise((resolve, reject) => { + request.onerror = (event) => { + console.error("Error fetching from IndexedDB", request.error); + reject(request.error); + }; + request.onsuccess = (event) => { + resolve(request.result !== undefined); + }; + }); + } catch (error) { + console.error("Error with IndexedDB operation", error); + throw error; + } +};