feat: vite
This commit is contained in:
parent
6b188b5354
commit
b31b0c422c
|
@ -1,42 +1,85 @@
|
||||||
import {
|
import { useEffect, useRef, useState } from "react";
|
||||||
readImageDicomFileSeries,
|
import { readImageDicomFileSeries } from "@itk-wasm/dicom";
|
||||||
structuredReportToHtml,
|
|
||||||
} from "@itk-wasm/dicom";
|
|
||||||
import vtkITKHelper from "@kitware/vtk.js/Common/DataModel/ITKHelper";
|
import vtkITKHelper from "@kitware/vtk.js/Common/DataModel/ITKHelper";
|
||||||
import vtkVolume from "@kitware/vtk.js/Rendering/Core/Volume";
|
import vtkVolume from "@kitware/vtk.js/Rendering/Core/Volume";
|
||||||
import vtkFullScreenRenderWindow from "@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow";
|
import vtkFullScreenRenderWindow from "@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow";
|
||||||
import "@kitware/vtk.js/Rendering/Profiles/Volume";
|
import "@kitware/vtk.js/Rendering/Profiles/Volume";
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import vtkRenderer from "@kitware/vtk.js/Rendering/Core/Renderer";
|
import vtkRenderer from "@kitware/vtk.js/Rendering/Core/Renderer";
|
||||||
import vtkVolumeMapper from "@kitware/vtk.js/Rendering/Core/VolumeMapper";
|
import vtkVolumeMapper from "@kitware/vtk.js/Rendering/Core/VolumeMapper";
|
||||||
import vtkRenderWindow from "@kitware/vtk.js/Rendering/Core/RenderWindow";
|
import vtkRenderWindow from "@kitware/vtk.js/Rendering/Core/RenderWindow";
|
||||||
|
|
||||||
|
import "./app.css";
|
||||||
|
import {
|
||||||
|
checkExistenceInIndexedDB,
|
||||||
|
loadImageFromIndexedDB,
|
||||||
|
saveImageToIndexedDB,
|
||||||
|
} from "./util";
|
||||||
|
import { extractAxialSliceAsImage } from "./extract";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const axialRef = useRef<HTMLElement | null>(null);
|
||||||
|
const sagittalRef = useRef<HTMLElement | null>(null);
|
||||||
|
const coronalRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
const rendererRef = useRef<vtkRenderer>();
|
const rendererRef = useRef<vtkRenderer>();
|
||||||
const mapperRef = useRef<vtkVolumeMapper>();
|
const mapperRef = useRef<vtkVolumeMapper>();
|
||||||
const actorRef = useRef<vtkVolume>();
|
const actorRef = useRef<vtkVolume>();
|
||||||
const renderWindowRef = useRef<vtkRenderWindow>();
|
const renderWindowRef = useRef<vtkRenderWindow>();
|
||||||
|
|
||||||
|
const [image, setImage] = useState();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current) inputRef.current.webkitdirectory = true;
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({});
|
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
|
||||||
|
container: axialRef.current,
|
||||||
|
});
|
||||||
rendererRef.current = fullScreenRenderer.getRenderer();
|
rendererRef.current = fullScreenRenderer.getRenderer();
|
||||||
renderWindowRef.current = fullScreenRenderer.getRenderWindow();
|
renderWindowRef.current = fullScreenRenderer.getRenderWindow();
|
||||||
actorRef.current = vtkVolume.newInstance();
|
actorRef.current = vtkVolume.newInstance();
|
||||||
mapperRef.current = vtkVolumeMapper.newInstance({ sampleDistance: 1.1 });
|
mapperRef.current = vtkVolumeMapper.newInstance({ sampleDistance: 1.1 });
|
||||||
|
console.time("itk-wasm parse vtkVolumeImageData");
|
||||||
readImageDicomFileSeries(null, {
|
readImageDicomFileSeries(null, {
|
||||||
inputImages: files,
|
inputImages: files,
|
||||||
}).then((image) => {
|
}).then((image) => {
|
||||||
const vtkImage = vtkITKHelper.convertItkToVtkImage(image.outputImage);
|
const vtkImage = vtkITKHelper.convertItkToVtkImage(image.outputImage);
|
||||||
|
console.timeEnd("itk-wasm parse vtkVolumeImageData");
|
||||||
|
saveImageToIndexedDB(image.outputImage);
|
||||||
mapperRef.current?.setInputData(vtkImage);
|
mapperRef.current?.setInputData(vtkImage);
|
||||||
mapperRef.current && actorRef.current?.setMapper(mapperRef.current);
|
mapperRef.current && actorRef.current?.setMapper(mapperRef.current);
|
||||||
actorRef.current && rendererRef.current?.addVolume(actorRef.current);
|
actorRef.current && rendererRef.current?.addVolume(actorRef.current);
|
||||||
|
@ -72,6 +115,12 @@ function App() {
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
style={{ position: "fixed", zIndex: 100 }}
|
style={{ position: "fixed", zIndex: 100 }}
|
||||||
/>
|
/>
|
||||||
|
<div className="mpr">
|
||||||
|
<div ref={axialRef} style={{ width: 500, height: 500 }}></div>
|
||||||
|
{/* <div ref={sagittalRef} style={{ width: 300, height: 300 }}></div>
|
||||||
|
<div ref={coronalRef} style={{ width: 300, height: 300 }}></div> */}
|
||||||
|
</div>
|
||||||
|
<img src={image} style={{ width: 200, height: 200 }} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
8
apps/phoenix/src/app.css
Normal file
8
apps/phoenix/src/app.css
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.mpr {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
38
apps/phoenix/src/extract.ts
Normal file
38
apps/phoenix/src/extract.ts
Normal file
|
@ -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();
|
||||||
|
};
|
91
apps/phoenix/src/util.ts
Normal file
91
apps/phoenix/src/util.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
const dbName = "myDatabase";
|
||||||
|
const storeName = "images";
|
||||||
|
|
||||||
|
export const openDatabase = async (): Promise<IDBDatabase> => {
|
||||||
|
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<any> => {
|
||||||
|
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<boolean> => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user