feat: 算法输出文件measurement.json接口调试

This commit is contained in:
mozzie 2024-09-23 16:23:39 +08:00
parent db8ba53589
commit 595e144a13
9 changed files with 368 additions and 14 deletions

View File

@ -59,3 +59,9 @@ pnpm config set virtual-store-dir-max-length 70
• 方向:冠状面,与矢状面垂直,沿身体的左右方向。
• 描述:将身体分为前(腹侧)和后(背侧)部分。
• 应用:适用于查看从前面到后面的结构,如面部和脑部的解剖。
## 区分片子的类型
头部扫描通常总长度在100毫米以下。
主动脉根部平扫总长度通常在100毫米到200毫米之间。
腹部扫描总长度通常超过200毫米。

View File

@ -179,3 +179,57 @@ export const downloadSeriesDicomFiles = async (
throw error;
}
};
export /**
* SeriesInstanceUID获取总的扫描长度
* @param seriesInstanceUID UID
* @returns
*/
const getTotalScanLength = async (
seriesInstanceUID: string
): Promise<number | null> => {
try {
// 1. 查找序列ID
const findResponse = await axios.post(`${OrthancServerRoot}/tools/find`, {
Level: "Series",
Query: {
SeriesInstanceUID: seriesInstanceUID,
},
});
const seriesIds = findResponse.data;
if (seriesIds.length === 0) {
console.error("Series not found");
return null;
}
const seriesId = seriesIds[0];
// 2. 获取共享标签提取SliceThickness
const tagsResponse = await axios.get(
`${OrthancServerRoot}/series/${seriesId}/shared-tags`
);
const tags = tagsResponse.data;
const sliceThickness = parseFloat(tags["0018,0050"]["Value"]); // (0018,0050) SliceThickness
if (isNaN(sliceThickness)) {
console.error("SliceThickness not found");
return null;
}
// 3. 获取实例列表,计算层数
const instancesResponse = await axios.get(
`${OrthancServerRoot}/series/${seriesId}/instances`
);
const instances = instancesResponse.data;
const numberOfSlices = instances.length;
console.log("numberOfSlices", numberOfSlices);
console.log("sliceThickness", sliceThickness);
// 4. 计算总的扫描长度
const totalLength = Number(sliceThickness) * numberOfSlices;
return totalLength;
} catch (error) {
console.error("Error:", error);
return null;
}
};

View File

@ -1,10 +1,12 @@
import { downloadSeriesDicomFiles } from "../../core/pacs";
import { downloadSeriesDicomFiles, getTotalScanLength } from "../../core/pacs";
import { executeInferTask } from "../../core/alg";
import { InferDeviceEnum, InferStructuralEnum } from "../../core/alg.type";
import { db } from "../../core/db";
import log from "electron-log";
import path from "node:path";
import { app, ipcMain } from "electron";
import { findSTLFiles } from "./util";
import { readFileSync } from "node:fs";
export const registerAlgHandler = () => {
ipcMain.handle("device:infer:set", async (_event, inferDevice) => {
@ -21,11 +23,16 @@ export const registerAlgHandler = () => {
ipcMain.on("model:infer", async (event, SeriesInstanceUIDs) => {
// 构造推理任务参数列表
const pu = InferDeviceEnum.GPU;
const module = InferStructuralEnum.AORTA;
const turbo = true;
const seg_schedule = true;
for (let i = 0; i < SeriesInstanceUIDs.length; i++) {
const SeriesInstanceUID = SeriesInstanceUIDs[i];
const physicalLength = await getTotalScanLength(SeriesInstanceUID);
const module =
physicalLength && physicalLength < 200
? InferStructuralEnum.AORTA
: InferStructuralEnum.PERI;
// 下载dicom到本地获取文件夹路径
const img_path = await downloadSeriesDicomFiles(SeriesInstanceUID);
const save_path = path.join(
@ -42,12 +49,38 @@ export const registerAlgHandler = () => {
}
});
ipcMain.handle("alg:assets", (_event, SeriesInstanceUID) => {
const assetsPath = path.join(
ipcMain.handle("alg:assets", async (_event, SeriesInstanceUID) => {
const physicalLength = await getTotalScanLength(SeriesInstanceUID);
const module =
physicalLength && physicalLength < 200
? InferStructuralEnum.AORTA
: InferStructuralEnum.PERI;
const rootPath = path.join(
app.getPath("userData"),
"output",
SeriesInstanceUID
);
console.log("assetsPath", assetsPath);
const stlsPath = path.join(rootPath, module, "visualization");
const stls = await findSTLFiles(stlsPath);
if (stls.length > 0) {
try {
// 读取测量json
const measurementPath = path.join(rootPath, module, "measurement.json");
const measurementData = readFileSync(measurementPath, "utf-8");
// 读取 STL 文件并转换为 ArrayBuffer
const stlFiles = stls.map((file) => {
const filePath = path.join(stlsPath, file);
const data = readFileSync(filePath);
return {
fileName: file,
data: data.buffer,
};
});
return { stlFiles, measurement: JSON.parse(measurementData) };
} catch (error) {
console.error("Error reading measurement.json:", error);
throw new Error("Failed to read or parse measurement.json");
}
}
});
};

View File

@ -0,0 +1,30 @@
import fs from "fs";
import path from "path";
/**
* .stl文件
* @param dirPath
* @returns .stl文件名的数组
*/
export const findSTLFiles = (dirPath: string): Promise<string[]> => {
return new Promise((resolve) => {
// 检查路径是否存在
if (!fs.existsSync(dirPath)) {
resolve([]); // 路径不存在时返回空数组
return;
}
// 异步读取目录内容
fs.readdir(dirPath, (err, files) => {
if (err) {
resolve([]); // 读取错误时返回空数组
} else {
// 过滤出.stl文件
const stlFiles = files.filter(
(file) => path.extname(file).toLowerCase() === ".stl"
);
resolve(stlFiles);
}
});
});
};

View File

@ -77,9 +77,11 @@
"tailwindcss-animate": "^1.0.7",
"zod": "3.23.8",
"mitt": "3.0.1",
"p-limit": "6.1.0"
"p-limit": "6.1.0",
"three": "0.164.1"
},
"devDependencies": {
"@types/three": "0.164.0",
"@radix-ui/react-icons": "^1.3.0",
"@types/lodash": "4.17.7",
"@types/node": "22.5.2",

View File

@ -0,0 +1,133 @@
import React, { useEffect, useRef } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { loadModels } from "./util";
interface AlgAssets {
stlFiles: { fileName: string; data: ArrayBuffer }[];
measurement: Record<string, any>;
}
export const AortaViewer: React.FC<{ SeriesInstanceUID: string }> = ({
SeriesInstanceUID,
}) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const rendererRef = useRef<THREE.WebGLRenderer>();
const sceneRef = useRef<THREE.Scene>(new THREE.Scene());
const cameraRef = useRef<THREE.PerspectiveCamera>();
const controlsRef = useRef<OrbitControls>();
const groupRef = useRef(new THREE.Group());
useEffect(() => {
if (SeriesInstanceUID) {
window.ipcRenderer
.invoke("alg:assets", SeriesInstanceUID)
.then((assets: AlgAssets) => {
const { stlFiles, measurement } = assets;
console.log(measurement);
Promise.all(loadModels(stlFiles)).then((meshes) => {
meshes.forEach((mesh) => {
if (mesh) groupRef.current.add(mesh);
});
});
})
.catch((error) => {
console.error("Error invoking alg:assets:", error);
});
}
}, [SeriesInstanceUID]);
useEffect(() => {
if (!SeriesInstanceUID || !canvasRef.current) return;
const { clientWidth, clientHeight } = canvasRef.current;
if (!rendererRef.current) {
// 初始化渲染器
initRenderer(clientWidth, clientHeight);
initCamera(clientWidth, clientHeight);
initControls();
initLights();
initScene();
// 开始渲染循环
startRenderLoop();
}
const handleResize = () => {
if (rendererRef.current && cameraRef.current) {
const { clientWidth, clientHeight } = canvasRef.current!;
rendererRef.current.setSize(clientWidth, clientHeight);
cameraRef.current.aspect = clientWidth / clientHeight;
cameraRef.current.updateProjectionMatrix();
}
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [SeriesInstanceUID]);
// 初始化渲染器
const initRenderer = (width: number, height: number) => {
const renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: canvasRef.current!,
});
renderer.setSize(width, height);
renderer.setClearColor(0x000000);
rendererRef.current = renderer;
};
// 初始化相机
const initCamera = (width: number, height: number) => {
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 10000);
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);
cameraRef.current = camera;
};
// 初始化控制器
const initControls = () => {
const controls = new OrbitControls(cameraRef.current!, canvasRef.current!);
controls.target.set(0, 0, 0); // 初始目标点
controls.update();
controlsRef.current = controls;
};
// 初始化光源
const initLights = () => {
const ambientLight = new THREE.AmbientLight(0xffffff);
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(1, 1, 1);
sceneRef.current.add(ambientLight, directionalLight);
};
// 初始化场景
const initScene = () => {
const axesHelper = new THREE.AxesHelper(1000);
sceneRef.current.add(axesHelper);
sceneRef.current.add(groupRef.current);
};
// 开始渲染循环
const startRenderLoop = () => {
const animate = () => {
requestAnimationFrame(animate);
controlsRef.current?.update();
rendererRef.current!.render(sceneRef.current!, cameraRef.current!);
};
animate();
};
return <canvas ref={canvasRef} style={{ width: "100%", height: "100%" }} />;
};
// if (canvasRef.current && cameraRef.current && rendererRef.current) {
// const { clientWidth, clientHeight } = canvasRef.current;
// cameraRef.current.aspect = clientWidth / clientHeight;
// rendererRef.current.setPixelRatio(window.devicePixelRatio * 2);
// cameraRef.current.updateProjectionMatrix();
// }

View File

@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { AortaViewer } from "./AortaViewer";
interface Model3DViewerProps {}
@ -9,13 +10,11 @@ export const Model3DViewer = (props: Model3DViewerProps) => {
const queryParams = new URLSearchParams(location.search);
const SeriesInstanceUID = queryParams.get("SeriesInstanceUID");
useEffect(() => {
window.ipcRenderer.invoke("alg:assets", SeriesInstanceUID).then((res) => {
console.log(res);
});
});
return (
<div>{SeriesInstanceUID ? <div>3d</div> : <div>AI分析该数据</div>}</div>
<div className="w-full h-full">
{SeriesInstanceUID && (
<AortaViewer SeriesInstanceUID={SeriesInstanceUID} />
)}
</div>
);
};

View File

@ -0,0 +1,55 @@
import { STLLoader } from "three/examples/jsm/loaders/STLLoader";
import * as THREE from "three";
const createObjectURLFromData = (data: ArrayBuffer): string => {
const blob = new Blob([data], { type: "application/octet-stream" });
return URL.createObjectURL(blob);
};
const loadSTLFile = (
stlLoader: STLLoader,
url: string,
fileName: string
): Promise<THREE.Mesh> => {
return new Promise((resolve, reject) => {
stlLoader.load(
url,
(geometry: THREE.BufferGeometry) => {
const material = new THREE.MeshLambertMaterial({
// color: "red",
transparent: true,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.name = fileName;
resolve(mesh);
},
undefined,
(error: Error) => {
console.error(`Error loading STL file ${fileName}:`, error);
reject(error);
}
);
});
};
export const loadModels = (
stlFiles: { fileName: string; data: ArrayBuffer }[]
) => {
const loadingManager = new THREE.LoadingManager();
const stlLoader = new STLLoader(loadingManager);
const loadPromises = stlFiles.map((file) => {
const { fileName, data } = file;
const url = createObjectURLFromData(data);
try {
return loadSTLFile(stlLoader, url, fileName);
} catch (error) {
console.error(`Failed to load STL file: ${fileName}`, error);
} finally {
URL.revokeObjectURL(url); // Clean up URL after use
}
});
return loadPromises;
};

View File

@ -208,6 +208,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.10)
three:
specifier: 0.164.1
version: 0.164.1
zod:
specifier: 3.23.8
version: 3.23.8
@ -227,6 +230,9 @@ importers:
'@types/react-dom':
specifier: ^18.2.21
version: 18.3.0
'@types/three':
specifier: 0.164.0
version: 0.164.0
'@typescript-eslint/eslint-plugin':
specifier: ^7.1.1
version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint@8.57.0)(typescript@5.5.4)
@ -2125,6 +2131,9 @@ packages:
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
engines: {node: '>= 10'}
'@tweenjs/tween.js@23.1.3':
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@ -2222,6 +2231,12 @@ packages:
'@types/stack-trace@0.0.33':
resolution: {integrity: sha512-O7in6531Bbvlb2KEsJ0dq0CHZvc3iWSR5ZYMtvGgnHA56VgriAN/AU2LorfmcvAl2xc9N5fbCTRyMRRl8nd74g==}
'@types/stats.js@0.17.3':
resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==}
'@types/three@0.164.0':
resolution: {integrity: sha512-SFDofn9dJVrE+1DKta7xj7lc4ru7B3S3yf10NsxOserW57aQlB6GxtAS1UK5To3LfEMN5HUHMu3n5v+M5rApgA==}
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
@ -3315,6 +3330,9 @@ packages:
fflate@0.7.3:
resolution: {integrity: sha512-0Zz1jOzJWERhyhsimS54VTqOteCNwRtIlh8isdL0AXLo0g7xNTfTL7oWrkmCnPhZGocKIkWHBistBrrpoNH3aw==}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@ -3967,6 +3985,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
meshoptimizer@0.18.1:
resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
@ -5197,6 +5218,9 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
three@0.164.1:
resolution: {integrity: sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w==}
throttle-debounce@5.0.2:
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
engines: {node: '>=12.22'}
@ -7594,6 +7618,8 @@ snapshots:
'@tootallnate/once@2.0.0': {}
'@tweenjs/tween.js@23.1.3': {}
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.25.4
@ -7713,6 +7739,16 @@ snapshots:
'@types/stack-trace@0.0.33': {}
'@types/stats.js@0.17.3': {}
'@types/three@0.164.0':
dependencies:
'@tweenjs/tween.js': 23.1.3
'@types/stats.js': 0.17.3
'@types/webxr': 0.5.20
fflate: 0.8.2
meshoptimizer: 0.18.1
'@types/tough-cookie@4.0.5': {}
'@types/verror@1.10.10':
@ -9131,6 +9167,8 @@ snapshots:
fflate@0.7.3: {}
fflate@0.8.2: {}
file-entry-cache@6.0.1:
dependencies:
flat-cache: 3.2.0
@ -9802,6 +9840,8 @@ snapshots:
merge2@1.4.1: {}
meshoptimizer@0.18.1: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
@ -11260,6 +11300,8 @@ snapshots:
dependencies:
any-promise: 1.3.0
three@0.164.1: {}
throttle-debounce@5.0.2: {}
through2@2.0.5: