diff --git a/apps/desktop/electron/core/alg.ts b/apps/desktop/electron/core/alg.ts new file mode 100644 index 0000000..d572251 --- /dev/null +++ b/apps/desktop/electron/core/alg.ts @@ -0,0 +1,51 @@ +import log from "electron-log"; +import { spawn } from "node:child_process"; +import path from "node:path"; +import { InferReq } from "./alg.type"; +import axios from "axios"; + +export const ALGServerRoot = "http://127.0.0.1:5000/root"; + +export const getEntryPath = () => { + // 区分操作系统 + return path.join(process.env.VITE_PUBLIC!, "main.exe"); +}; + +export const startALGServer = () => { + const entryPath = getEntryPath(); + const child_process = spawn(entryPath); + child_process.on("message", (data) => console.log(data)); + child_process.stdout.on("data", (data) => log.info(data.toString())); + child_process.stderr.on("data", (data) => log.error(data.toString())); +}; + +/** + * 执行推理任务 + */ +export const executeInferTask = ( + task: InferReq, + onData: (data: string) => void +) => { + return new Promise((resolve, reject) => { + axios + .post(ALGServerRoot, task, { + responseType: "stream", + headers: { "Content-Type": "application/json" }, + }) + .then((response) => { + response.data.on("data", (chunk: Buffer) => { + const data = chunk.toString(); + onData(data); // 实时处理数据 + }); + + response.data.on("end", () => { + resolve(task); // 数据流结束 + }); + + response.data.on("error", (err: Error) => { + reject(err); + }); + }) + .catch((error) => reject(error)); + }); +}; diff --git a/apps/desktop/electron/core/alg.type.ts b/apps/desktop/electron/core/alg.type.ts new file mode 100644 index 0000000..4a27f53 --- /dev/null +++ b/apps/desktop/electron/core/alg.type.ts @@ -0,0 +1,29 @@ +export enum InferDeviceEnum { + GPU = "GPU", + CPU = "CPU", + NPU = "NPU", +} + +/** + * 分割的结构 + */ +export enum InferStructuralEnum { + PERI = "peripheral", + AORTA = "root", +} + +export type InferReq = { + /** + * .dcm文件夹path + */ + img_path: string; + /** + * 保存推理文件的path + * @description {app.getPath('userData')}/{SeriesInstanceUID}/${xxxx} + */ + save_path: string; + pu: InferDeviceEnum; + module: InferStructuralEnum; + turbo?: boolean; + seg_schedule: boolean; +}; diff --git a/apps/desktop/electron/core/pacs.ts b/apps/desktop/electron/core/pacs.ts index 61e48a3..3812b09 100644 --- a/apps/desktop/electron/core/pacs.ts +++ b/apps/desktop/electron/core/pacs.ts @@ -1,11 +1,13 @@ import { spawn } from "node:child_process"; -import { existsSync } from "node:fs"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import log from "electron-log"; import path from "node:path"; import FormData from "form-data"; import { readFile } from "fs/promises"; import axios from "axios"; -import { PatientInfo, SeriesInfo, StudyInfo } from "./pacs.type"; +import { InstanceInfo, PatientInfo, SeriesInfo, StudyInfo } from "./pacs.type"; +import { app } from "electron"; +import pLimit from "p-limit"; export const OrthancServerRoot = "http://localhost:8042"; @@ -103,3 +105,77 @@ export const selectStructuredDicom = async ( throw error; // or handle it accordingly } }; + +/** + * 根据 SeriesInstanceUID 下载序列的所有 DICOM 文件并返回保存的文件夹路径 + * @param seriesInstanceUID 序列的 SeriesInstanceUID + * @returns 保存的文件夹路径 + */ +export const downloadSeriesDicomFiles = async ( + seriesInstanceUID: string +): Promise => { + try { + // 使用 Orthanc 的查询接口查找匹配的序列 ID + const findResponse = await axios.post( + `${OrthancServerRoot}/tools/find`, + { + Level: "Series", + Query: { + SeriesInstanceUID: seriesInstanceUID, + }, + } + ); + + const seriesIds = findResponse.data; + + if (seriesIds.length === 0) { + throw new Error("在 Orthanc 中未找到指定的 SeriesInstanceUID。"); + } + + const seriesId = seriesIds[0]; // 假设只有一个匹配的序列 + + // 获取该序列中的所有实例(DICOM 文件) + const instancesResponse = await axios.get( + `${OrthancServerRoot}/series/${seriesId}/instances` + ); + + const instances = instancesResponse.data as unknown as InstanceInfo[]; + + // 创建保存 DICOM 文件的本地目录 + const saveDir = path.join( + app.getPath("userData"), + "dicom", + seriesInstanceUID + ); + if (!existsSync(saveDir)) { + mkdirSync(saveDir, { recursive: true }); + } + + // 并发限制 + const limit = pLimit(5); // 限制同时进行的请求数为 5 + + // 并行下载 DICOM 文件 + await Promise.all( + instances.map((instance) => { + const instanceId = instance.ID; + return limit(async () => { + const dicomResponse = await axios.get( + `${OrthancServerRoot}/instances/${instanceId}/file`, + { + responseType: "arraybuffer", + } + ); + + const filePath = path.join(saveDir, `${instanceId}.dcm`); + writeFileSync(filePath, Buffer.from(dicomResponse.data), "binary"); + }); + }) + ); + + console.log(`所有 DICOM 文件已保存至:${saveDir}`); + return saveDir; + } catch (error) { + console.error("下载 DICOM 文件时出错:", error); + throw error; + } +}; diff --git a/apps/desktop/electron/ipcEvent/common/index.ts b/apps/desktop/electron/ipcEvent/common/index.ts index 9e3aaf7..f922011 100644 --- a/apps/desktop/electron/ipcEvent/common/index.ts +++ b/apps/desktop/electron/ipcEvent/common/index.ts @@ -1,8 +1,11 @@ -import { ipcMain, shell } from "electron"; +import { app, ipcMain, shell } from "electron"; import { mkdir, stat } from "fs/promises"; import { db } from "../../core/db"; import log from "electron-log"; import path from "node:path"; +import { downloadSeriesDicomFiles } from "../../core/pacs"; +import { executeInferTask } from "../../core/alg"; +import { InferDeviceEnum, InferStructuralEnum } from "../../core/alg.type"; export const registerCommonHandler = () => { ipcMain.handle("output:open", async () => { @@ -30,4 +33,24 @@ export const registerCommonHandler = () => { return { success: false, msg: `操作失败` }; } }); + + ipcMain.handle("model:infer", async (_event, SeriesInstanceUIDs) => { + // 构造推理任务参数列表 + const save_path = path.join(app.getPath("userData"), "output"); + 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]; + // 下载dicom到本地,获取文件夹路径 + const img_path = await downloadSeriesDicomFiles(SeriesInstanceUID); + const task = { save_path, pu, module, turbo, seg_schedule, img_path }; + console.log(task); + const result = await executeInferTask(task, (data) => { + console.log(data); + }); + console.log("end: ", result); + } + }); }; diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 6065a7b..a5d1c15 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -13,6 +13,7 @@ import { createDatabase } from "./core/db"; import { getMachineId } from "./core/auth"; import { getPacsPath, runOrthancServer } from "./core/pacs"; import { registerIpcMainHandlers } from "./ipcEvent"; +import { startALGServer } from "./core/alg"; // const require = createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -37,7 +38,6 @@ const themeTitleBarStyles = { }; export const platform = process.platform === "darwin" ? "macos" : "windows"; - app.commandLine.appendSwitch("disable-web-security"); app.commandLine.appendSwitch("ignore-gpu-blocklist"); app.commandLine.appendSwitch("use-angle", "gl"); @@ -74,10 +74,7 @@ function createWindow() { win.loadURL(VITE_DEV_SERVER_URL); registerIpcMainHandlers(win); runOrthancServer(getPacsPath(platform, true)); - - // if (platform !== "macos") { - // python_process = spawn(path.join(process.env.VITE_PUBLIC!, "main.exe")); - // } + startALGServer(); } else { // if (platform !== "macos") { // python_process = spawn(path.join(process.env.VITE_PUBLIC!, "main.exe")); diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 306130a..91ccdda 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -76,7 +76,8 @@ "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", "zod": "3.23.8", - "mitt": "3.0.1" + "mitt": "3.0.1", + "p-limit": "6.1.0" }, "devDependencies": { "@radix-ui/react-icons": "^1.3.0", diff --git a/apps/desktop/public/flask_app b/apps/desktop/public/flask_app deleted file mode 100755 index 996e2fb..0000000 Binary files a/apps/desktop/public/flask_app and /dev/null differ diff --git a/apps/desktop/public/main.exe b/apps/desktop/public/main.exe index 907c60a..59a302d 100644 Binary files a/apps/desktop/public/main.exe and b/apps/desktop/public/main.exe differ diff --git a/apps/desktop/src/pages/Boot/index.tsx b/apps/desktop/src/pages/Boot/index.tsx index df5b43a..dd513a1 100644 --- a/apps/desktop/src/pages/Boot/index.tsx +++ b/apps/desktop/src/pages/Boot/index.tsx @@ -15,22 +15,22 @@ import { ScrollArea } from "@/components/ui/scroll-area"; const Boot = () => { const location = useLocation(); - const selectDicoms = - location.state?.selectDicoms ?? - JSON.parse(localStorage.getItem("selectDicoms") ?? "[]"); const [messageText, setMessageText] = useState(["进度信息简化"]); /** * windows系统右键启动应用菜单的入口路径 */ const [bootDirectoryPath, setBootDirectoryPath] = useState(""); - const [tasks, setTasks] = useState(selectDicoms); /** * 上传到electron主进程 */ const handleTasks = () => { - window.ipcRenderer.send("ai:task", { selectDicoms: tasks }); + const SeriesInstanceUIDs = [ + // "1.2.156.112605.14038010222575.230518044041.3.4344.300061", + "1.3.12.2.1107.5.1.4.73399.30000020080900171669200001479", + ]; + window.ipcRenderer.invoke("model:infer", SeriesInstanceUIDs); }; useEffect(() => { @@ -42,30 +42,11 @@ const Boot = () => { useEffect(() => { if (bootDirectoryPath) { - window.ipcRenderer.send("one-step"); + // TODO: 启动分析 } - return () => { - window.ipcRenderer.off("one-step", () => {}); - }; + return () => {}; }, [bootDirectoryPath]); - const handleEmptyTasks = () => { - setTasks([]); - localStorage.removeItem("selectDicoms"); - }; - - useEffect(() => { - window.ipcRenderer.on("tasksFinished", (_event, data) => { - setMessageText((p) => [...p, `总用时: ${data}`]); - }); - }, []); - - useEffect(() => { - window.ipcRenderer.on("taskFinished", (_event, data) => { - setMessageText((p) => [...p, data]); - }); - }, []); - return ( {
-
- {tasks.map((dicom: Series, index: number) => ( - -
-
-
-
- {dicom.PatientName} -
- -
-
- {dicom.PatientSex} -
-
-
- {dicom.PatientAge} -
-
-
- 这里是一些额外的信息,如果可以增加一些AI的判断结果,根据metadata扔给大语言模型,理想情况可以自动鉴别出能够使用哪些模型,优化掉用户手动选择分析的过程 -
-
-
- 主动脉瓣 -
-
- 二尖瓣 -
-
- 外周入路 -
-
-
- ))} -
+
啦啦啦
-