feat: dicom文件hash去重 & 渲染进度
This commit is contained in:
parent
abf1cc06b5
commit
3ada54cf0f
|
@ -1,6 +1,7 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import * as dicomParser from "dicom-parser";
|
import * as dicomParser from "dicom-parser";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
export interface StructuredData {
|
export interface StructuredData {
|
||||||
[SeriesInstanceUID: string]: ExtractMetadata[];
|
[SeriesInstanceUID: string]: ExtractMetadata[];
|
||||||
|
@ -128,35 +129,58 @@ export interface StructuredMetadata {
|
||||||
StudyInstanceUID?: string;
|
StudyInstanceUID?: string;
|
||||||
SeriesInstanceUID?: string;
|
SeriesInstanceUID?: string;
|
||||||
PatientName?: string;
|
PatientName?: string;
|
||||||
|
fileHash: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const structureMetadata = (
|
export interface ScanProgress {
|
||||||
data: ExtractMetadata[]
|
percentage: number
|
||||||
): StructuredMetadata[] => {
|
}
|
||||||
|
|
||||||
|
// 计算文件的哈希值
|
||||||
|
async function calculateFileHash(filePath: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
const stream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
|
stream.on('error', reject);
|
||||||
|
stream.pipe(hash).on('finish', () => resolve(hash.digest('hex')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const structureMetadata = async (
|
||||||
|
data: ExtractMetadata[],
|
||||||
|
progressCallback?: (progress: ScanProgress) => void
|
||||||
|
): Promise<StructuredMetadata[]> => {
|
||||||
const result: StructuredMetadata[] = [];
|
const result: StructuredMetadata[] = [];
|
||||||
|
// 序列级别
|
||||||
|
const keyProp = 'SeriesInstanceUID'
|
||||||
|
|
||||||
data.forEach((item) => {
|
for (let i = 0; i < data.length; i++) {
|
||||||
// 查找是否已经有相同 UID 和 PatientName 的条目
|
const item = data[i]
|
||||||
const existingEntry = result.find(
|
const existItem = result.find((i) => i[keyProp] === item[keyProp]);
|
||||||
(entry) =>
|
const hash = await calculateFileHash(item.filePath);
|
||||||
entry.StudyInstanceUID === item.StudyInstanceUID &&
|
|
||||||
entry.SeriesInstanceUID === item.SeriesInstanceUID &&
|
|
||||||
entry.PatientName === item.PatientName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingEntry) {
|
if (existItem) {
|
||||||
// 如果找到了相同的条目,合并 filePath
|
// 如果找到了相同的条目,合并 filePath
|
||||||
existingEntry.filePaths.push(item.filePath);
|
if (!existItem.fileHash.includes(hash)) {
|
||||||
|
existItem.filePaths.push(item.filePath);
|
||||||
|
existItem.fileHash.push(hash)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果没有找到,创建一个新的条目
|
// 如果没有找到,创建一个新的条目
|
||||||
result.push({
|
result.push({
|
||||||
filePaths: [item.filePath],
|
filePaths: [item.filePath],
|
||||||
|
fileHash: [hash],
|
||||||
StudyInstanceUID: item.StudyInstanceUID,
|
StudyInstanceUID: item.StudyInstanceUID,
|
||||||
SeriesInstanceUID: item.SeriesInstanceUID,
|
SeriesInstanceUID: item.SeriesInstanceUID,
|
||||||
PatientName: item.PatientName,
|
PatientName: item.PatientName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
const progress: ScanProgress = {
|
||||||
|
percentage: ((i + 1) / data.length) * 100
|
||||||
|
};
|
||||||
|
progressCallback?.(progress);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,17 +47,19 @@ const registerIpcMainHandlers = (
|
||||||
running ? pythonManager.startFlask() : pythonManager.stopFlask();
|
running ? pythonManager.startFlask() : pythonManager.stopFlask();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on("scan-dicom", async () => {
|
ipcMain.on("scan-dicom", async (event) => {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
properties: ["openDirectory"],
|
properties: ["openDirectory"],
|
||||||
});
|
});
|
||||||
if (result.canceled) return null;
|
if (result.canceled) return null;
|
||||||
const filePaths = result.filePaths[0];
|
const filePaths = result.filePaths[0];
|
||||||
const dcmPaths = await findDcmFiles(filePaths);
|
const dcmPaths = await findDcmFiles(filePaths)
|
||||||
const items = await processFilesInBatches(dcmPaths, 4);
|
const batchSize = os.cpus().length * 1 || 10;
|
||||||
const structDicom = await structureMetadata(items);
|
const items = await processFilesInBatches(dcmPaths, batchSize);
|
||||||
Database.getDb().data.posts.push("hello world");
|
const structDicom = await structureMetadata(items, (progress) => {
|
||||||
return structDicom;
|
event.reply('scan-progress', progress)
|
||||||
|
});
|
||||||
|
event.reply('scan-progress-done', structDicom)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Cvpilot AI</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
@ -1,12 +1,46 @@
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface ScanProgress {
|
||||||
|
percentage: number
|
||||||
|
}
|
||||||
|
|
||||||
export const Datasource = () => {
|
export const Datasource = () => {
|
||||||
|
const [progress, setProgress] = useState<ScanProgress>()
|
||||||
|
const [result, setResult] = useState()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.ipcRenderer.on('scan-progress', (event, data) => {
|
||||||
|
setProgress(data)
|
||||||
|
if (data.error) return;
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.ipcRenderer.off('scan-progress', () => { });
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.ipcRenderer.on('scan-progress-done', (event, data) => {
|
||||||
|
console.log('%capps\desktop\src\pages\Datasource\index.tsx:25 data', 'color: #007acc;', data);
|
||||||
|
setResult(data)
|
||||||
|
if (data.error) return;
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.ipcRenderer.off('scan-progress-done', () => { });
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={() => window.ipcRenderer.send("scan-dicom")}>
|
<Button onClick={() => window.ipcRenderer.send("scan-dicom")}>
|
||||||
上传
|
上传
|
||||||
</Button>
|
</Button>
|
||||||
|
<div>{progress?.percentage}</div>
|
||||||
|
<div>{JSON.stringify(result)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user