2026-05-20-22-35-42 导入资产与分割导出优化

This commit is contained in:
2026-05-20 22:58:39 +08:00
parent ec4cb1eae7
commit 67295ddd9f
9 changed files with 660 additions and 77 deletions

View File

@@ -11,6 +11,7 @@ type DicomPlane = 'axial' | 'sagittal' | 'coronal';
type DicomDisplayMode = 'default' | 'bone' | 'soft' | 'contrast';
type ProjectExportTarget = 'dicom' | 'segmentation' | 'pose' | 'stl';
type SegmentationExportScope = 'all' | 'visible';
type SegmentationExportMode = 'combined' | 'separate';
type SegmentationDisplayLevel = 'standard' | 'fine' | 'ultra' | 'solid';
type SegmentationDicomOpacityLevel = 'low' | 'medium' | 'high';
@@ -95,16 +96,22 @@ interface AppState {
updatedAt: string;
}
interface UploadedAssetPayload {
name: string;
data: string;
}
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '..');
const dataDir = path.join(__dirname, 'data');
const exportDir = path.join(__dirname, 'exports');
const uploadDir = path.join(dataDir, 'uploads');
const statePath = path.join(dataDir, 'state.json');
const dicomDir = path.join(repoRoot, 'Head_CT_DICOM');
const modelDir = path.join(repoRoot, 'Head_CT_ReConstruct');
const dicomPreviewCache = new Map<string, unknown>();
const dicomVolumeCache = new Map<DicomDisplayMode, {
const dicomVolumeCache = new Map<string, {
frames: Buffer[];
width: number;
height: number;
@@ -209,6 +216,47 @@ function naturalFileCompare(a: string, b: string) {
return a.localeCompare(b, 'zh-Hans-CN', { numeric: true, sensitivity: 'base' });
}
function toRepoRelativePath(dir: string) {
return path.relative(repoRoot, dir).split(path.sep).join('/');
}
function resolveStoredAssetDir(storedPath: string | undefined, fallbackDir: string) {
if (!storedPath) {
return fallbackDir;
}
return path.isAbsolute(storedPath) ? storedPath : path.resolve(repoRoot, storedPath);
}
function getProjectDicomDir(project: ProjectRecord) {
return resolveStoredAssetDir(project.dicomPath, project.id === 'head-ct-demo' ? dicomDir : '');
}
function getProjectModelDir(project: ProjectRecord) {
return resolveStoredAssetDir(project.modelPath, project.id === 'head-ct-demo' ? modelDir : '');
}
function getProjectDicomFilePath(project: ProjectRecord, fileName: string) {
return path.join(getProjectDicomDir(project), fileName);
}
function getProjectModelFilePath(project: ProjectRecord, fileName: string) {
return path.join(getProjectModelDir(project), fileName);
}
function clearProjectRuntimeCaches(projectId: string) {
[...dicomPreviewCache.keys()].forEach((key) => {
if (key.startsWith(`${projectId}:`)) {
dicomPreviewCache.delete(key);
}
});
[...dicomVolumeCache.keys()].forEach((key) => {
if (key.startsWith(`${projectId}:`)) {
dicomVolumeCache.delete(key);
}
});
modelPreviewCache.clear();
}
function publicUser(user: UserRecord) {
const { password: _password, ...rest } = user;
return rest;
@@ -446,20 +494,34 @@ function normalizeState(state: AppState): AppState {
? state.projects
.filter((project) => project.id !== defaultProject.id)
.map((project) => {
const stlFiles = Array.isArray(project.stlFiles) ? project.stlFiles : [];
const dicomPath = typeof project.dicomPath === 'string' ? project.dicomPath : '';
const modelPath = typeof project.modelPath === 'string' ? project.modelPath : '';
const dicomCount = dicomPath ? listFiles(resolveStoredAssetDir(dicomPath, ''), '.dcm').length : project.dicomCount ?? 0;
const stlFiles = modelPath
? listFiles(resolveStoredAssetDir(modelPath, ''), '.stl')
: Array.isArray(project.stlFiles) ? project.stlFiles : [];
const moduleStyles = buildModuleStyles(stlFiles, project.moduleStyles);
return {
...project,
dicomPath,
modelPath,
dicomCount,
stlFiles,
hasModel: stlFiles.length > 0,
modelCount: stlFiles.length,
exportedMaskCount: project.exportedMaskCount ?? 0,
maskFormats: project.maskFormats ?? ['nii', 'nii.gz'],
moduleStyles,
modelPoses: normalizeModelPoses(project.modelPoses),
segmentationResults: normalizeSegmentationResults(project.segmentationResults, stlFiles, moduleStyles, project.dicomCount ?? 0),
segmentationResults: normalizeSegmentationResults(project.segmentationResults, stlFiles, moduleStyles, dicomCount),
};
})
: [];
const defaultModuleStyles = buildModuleStyles(defaultProject.stlFiles, savedDefaultProject?.moduleStyles);
const defaultDicomPath = savedDefaultProject?.dicomPath ?? defaultProject.dicomPath;
const defaultModelPath = savedDefaultProject?.modelPath ?? defaultProject.modelPath;
const defaultDicomCount = listFiles(resolveStoredAssetDir(defaultDicomPath, dicomDir), '.dcm').length;
const defaultStlFiles = listFiles(resolveStoredAssetDir(defaultModelPath, modelDir), '.stl');
const defaultModuleStyles = buildModuleStyles(defaultStlFiles, savedDefaultProject?.moduleStyles);
return {
...state,
@@ -467,14 +529,20 @@ function normalizeState(state: AppState): AppState {
{
...defaultProject,
name: savedDefaultProject?.name ?? defaultProject.name,
dicomPath: defaultDicomPath,
modelPath: defaultModelPath,
dicomCount: defaultDicomCount,
hasModel: defaultStlFiles.length > 0,
modelCount: defaultStlFiles.length,
stlFiles: defaultStlFiles,
exportedMaskCount: savedDefaultProject?.exportedMaskCount ?? 0,
moduleStyles: defaultModuleStyles,
modelPoses: normalizeModelPoses(savedDefaultProject?.modelPoses),
segmentationResults: normalizeSegmentationResults(
savedDefaultProject?.segmentationResults,
defaultProject.stlFiles,
defaultStlFiles,
defaultModuleStyles,
defaultProject.dicomCount,
defaultDicomCount,
),
},
...customProjects,
@@ -558,13 +626,13 @@ interface ExportSceneMetrics {
const exportFusionBaseExtent = 4.6;
function readDicomHuVolume(files: string[]): DicomHuVolume {
function readDicomHuVolume(project: ProjectRecord, files: string[]): DicomHuVolume {
if (!files.length) {
throw new Error('当前项目没有可导出的 DICOM 序列');
}
const parsed = files.map((fileName) => {
const buffer = fs.readFileSync(path.join(dicomDir, fileName));
const buffer = fs.readFileSync(getProjectDicomFilePath(project, fileName));
const attributes = parseDicomAttributes(buffer, 'default');
const pixelTag = findExplicitTag(buffer, 0x7fe0, 0x0010);
if (!attributes.rows || !attributes.columns || !pixelTag) {
@@ -1038,10 +1106,16 @@ function isModuleIncludedForExport(style: ModuleStyleRecord, scope: Segmentation
return scope === 'all' || style.visible !== false;
}
function createSegmentationData(project: ProjectRecord, volume: DicomHuVolume, pose: ModelPoseValue, scope: SegmentationExportScope = 'visible') {
function createSegmentationData(
project: ProjectRecord,
volume: DicomHuVolume,
pose: ModelPoseValue,
scope: SegmentationExportScope = 'visible',
onlyFileName?: string,
) {
const data = Buffer.alloc(volume.width * volume.height * volume.depth);
const previews = (project.stlFiles ?? []).reduce<Record<string, ModelPreviewRecord>>((accumulator, fileName) => {
const filePath = path.join(modelDir, fileName);
const filePath = getProjectModelFilePath(project, fileName);
if (fs.existsSync(filePath)) {
accumulator[fileName] = createStlPreview(filePath, fileName, 200000) as ModelPreviewRecord;
}
@@ -1071,7 +1145,7 @@ function createSegmentationData(project: ProjectRecord, volume: DicomHuVolume, p
const payload = previews[fileName];
const style = getModuleStyle(project, fileName, index);
if (!payload || !isModuleIncludedForExport(style, scope)) {
if (!payload || (onlyFileName && fileName !== onlyFileName) || !isModuleIncludedForExport(style, scope)) {
return;
}
@@ -1087,7 +1161,7 @@ function createSegmentationData(project: ProjectRecord, volume: DicomHuVolume, p
return rows;
};
const filePath = path.join(modelDir, fileName);
const filePath = getProjectModelFilePath(project, fileName);
forEachBinaryStlTriangle(filePath, fileName, (
ax,
ay,
@@ -1209,6 +1283,10 @@ function parseSegmentationScope(raw: unknown): SegmentationExportScope {
return raw === 'all' ? 'all' : 'visible';
}
function parseSegmentationExportMode(raw: unknown): SegmentationExportMode {
return raw === 'separate' ? 'separate' : 'combined';
}
function latestSegmentationResult(project: ProjectRecord) {
return project.segmentationResults?.[project.segmentationResults.length - 1];
}
@@ -1233,6 +1311,34 @@ function parseExportTargets(raw: unknown): ProjectExportTarget[] {
return [...new Set(targets)];
}
function sanitizeUploadFileName(name: string, fallback: string, extension: string) {
const baseName = path.basename(name || fallback).replace(/[\\/:*?"<>|]+/g, '_').trim();
const withFallback = baseName || fallback;
return withFallback.toLowerCase().endsWith(extension) ? withFallback : `${withFallback}${extension}`;
}
function decodeUploadedAssetData(raw: string) {
const base64 = raw.includes(',') ? raw.slice(raw.indexOf(',') + 1) : raw;
if (!base64.trim()) {
throw new Error('上传文件内容为空');
}
return Buffer.from(base64, 'base64');
}
function parseUploadedAssets(raw: unknown): UploadedAssetPayload[] {
if (!Array.isArray(raw)) {
throw new Error('上传文件列表无效');
}
return raw
.map((item) => item && typeof item === 'object' ? item as Record<string, unknown> : null)
.filter((item): item is Record<string, unknown> => Boolean(item))
.map((item, index) => ({
name: typeof item.name === 'string' && item.name.trim() ? item.name.trim() : `asset-${index + 1}`,
data: typeof item.data === 'string' ? item.data : '',
}))
.filter((item) => item.data);
}
function createNiftiExport(
project: ProjectRecord,
files: string[],
@@ -1241,7 +1347,7 @@ function createNiftiExport(
pose?: ModelPoseValue,
segmentationScope: SegmentationExportScope = 'visible',
) {
const volume = readDicomHuVolume(files);
const volume = readDicomHuVolume(project, files);
if (target === 'dicom') {
return createNiftiBuffer(volume, volume.data, 'dicom', compressed);
}
@@ -1277,6 +1383,7 @@ function createProjectExportBundle({
compressed,
activePose,
segmentationScope,
segmentationExportMode,
exportRoot,
}: {
project: ProjectRecord;
@@ -1285,11 +1392,12 @@ function createProjectExportBundle({
compressed: boolean;
activePose?: ModelPoseValue;
segmentationScope: SegmentationExportScope;
segmentationExportMode: SegmentationExportMode;
exportRoot: string;
}) {
const entries: Array<{ name: string; data: Buffer; mtime?: number }> = [];
const needsVolume = targets.includes('dicom') || targets.includes('segmentation');
const volume = needsVolume ? readDicomHuVolume(files) : null;
const volume = needsVolume ? readDicomHuVolume(project, files) : null;
const format = compressed ? 'nii.gz' : 'nii';
if (targets.includes('dicom') && volume) {
@@ -1300,15 +1408,34 @@ function createProjectExportBundle({
}
if (targets.includes('segmentation') && volume) {
entries.push({
name: `${exportRoot}/${project.id}-segmentation-label.${format}`,
data: createNiftiBuffer(
volume,
createSegmentationData(project, volume, activePose ?? defaultModelPose, segmentationScope),
'segmentation',
compressed,
),
});
if (segmentationExportMode === 'separate') {
(project.stlFiles ?? []).forEach((fileName, index) => {
const style = getModuleStyle(project, fileName, index);
if (!isModuleIncludedForExport(style, segmentationScope)) {
return;
}
const moduleName = sanitizeFilenamePart(fileName.replace(/\.stl$/i, ''), `module-${index + 1}`);
entries.push({
name: `${exportRoot}/segmentation/${String(style.partId).padStart(3, '0')}-${moduleName}-label.${format}`,
data: createNiftiBuffer(
volume,
createSegmentationData(project, volume, activePose ?? defaultModelPose, segmentationScope, fileName),
'segmentation',
compressed,
),
});
});
} else {
entries.push({
name: `${exportRoot}/${project.id}-segmentation-label.${format}`,
data: createNiftiBuffer(
volume,
createSegmentationData(project, volume, activePose ?? defaultModelPose, segmentationScope),
'segmentation',
compressed,
),
});
}
entries.push({
name: `${exportRoot}/${project.id}-segmentation-labels.json`,
data: createSegmentationLabelMetadata(project, segmentationScope, activePose),
@@ -1324,7 +1451,7 @@ function createProjectExportBundle({
if (targets.includes('stl')) {
(project.stlFiles ?? []).forEach((fileName) => {
const filePath = path.join(modelDir, fileName);
const filePath = getProjectModelFilePath(project, fileName);
if (!fs.existsSync(filePath)) {
return;
}
@@ -1350,10 +1477,7 @@ function findProject(state: AppState, projectId: string) {
}
function getProjectDicomFiles(project: ProjectRecord) {
if (project.id !== 'head-ct-demo') {
return [];
}
return listFiles(dicomDir, '.dcm');
return listFiles(getProjectDicomDir(project), '.dcm');
}
function readAsciiValue(buffer: Buffer, start: number, length: number) {
@@ -1542,13 +1666,14 @@ function estimateSliceSpacing(parsed: ReturnType<typeof parseDicomPixels>[]) {
?? 1;
}
function getDicomVolume(files: string[], mode: DicomDisplayMode) {
const cached = dicomVolumeCache.get(mode);
function getDicomVolume(project: ProjectRecord, files: string[], mode: DicomDisplayMode) {
const cacheKey = `${project.id}:${project.dicomPath}:${mode}:${files.join('|')}`;
const cached = dicomVolumeCache.get(cacheKey);
if (cached) {
return cached;
}
const parsed = files.map((fileName) => parseDicomPixels(path.join(dicomDir, fileName), mode));
const parsed = files.map((fileName) => parseDicomPixels(getProjectDicomFilePath(project, fileName), mode));
const sliceSpacing = estimateSliceSpacing(parsed);
const volume = {
frames: parsed.map((frame) => frame.pixelBuffer),
@@ -1562,7 +1687,7 @@ function getDicomVolume(files: string[], mode: DicomDisplayMode) {
sliceThickness: parsed[0]?.attributes.sliceThickness ?? null,
spacingBetweenSlices: parsed[0]?.attributes.spacingBetweenSlices ?? null,
};
dicomVolumeCache.set(mode, volume);
dicomVolumeCache.set(cacheKey, volume);
return volume;
}
@@ -1602,19 +1727,19 @@ function resampleToPhysicalAspect(pixels: Buffer, width: number, height: number,
};
}
function warmDicomVolumeCache(files: string[]) {
function warmDicomVolumeCache(project: ProjectRecord, files: string[]) {
setTimeout(() => {
try {
getDicomVolume(files, 'default');
getDicomVolume(files, 'soft');
getDicomVolume(project, files, 'default');
getDicomVolume(project, files, 'soft');
} catch (error) {
console.warn('DICOM volume warmup failed:', error);
}
}, 300);
}
function createReformattedPreview(files: string[], plane: Exclude<DicomPlane, 'axial'>, slice: number, mode: DicomDisplayMode) {
const volume = getDicomVolume(files, mode);
function createReformattedPreview(project: ProjectRecord, files: string[], plane: Exclude<DicomPlane, 'axial'>, slice: number, mode: DicomDisplayMode) {
const volume = getDicomVolume(project, files, mode);
const maxSlice = plane === 'sagittal' ? volume.width - 1 : volume.height - 1;
const clampedSlice = Math.max(0, Math.min(maxSlice, slice));
const outputWidth = files.length;
@@ -1660,8 +1785,8 @@ function createReformattedPreview(files: string[], plane: Exclude<DicomPlane, 'a
};
}
function createDicomFusionVolume(files: string[], start: number, end: number, mode: DicomDisplayMode) {
const volume = getDicomVolume(files, mode);
function createDicomFusionVolume(project: ProjectRecord, files: string[], start: number, end: number, mode: DicomDisplayMode) {
const volume = getDicomVolume(project, files, mode);
const total = volume.frames.length;
const safeStart = Math.max(0, Math.min(total - 1, Number.isFinite(start) ? start : 0));
const safeEnd = Math.max(safeStart, Math.min(total - 1, Number.isFinite(end) ? end : safeStart + 49));
@@ -1773,7 +1898,7 @@ function cropDicomContent(pixels: Buffer, width: number, height: number) {
}
function createStlPreview(filePath: string, fileName: string, limit: number): ModelPreviewRecord {
const cacheKey = `${fileName}:${limit}`;
const cacheKey = `${filePath}:${fileName}:${limit}`;
const cached = modelPreviewCache.get(cacheKey);
if (cached) {
return cached as ModelPreviewRecord;
@@ -1896,12 +2021,13 @@ function createTarGz(entries: Array<{ name: string; data: Buffer; mtime?: number
return zlib.gzipSync(Buffer.concat(chunks));
}
function createDicomTarGz(files: string[]) {
function createDicomTarGz(project: ProjectRecord, files: string[]) {
const rootName = sanitizeFilenamePart(project.dicomPath || 'DICOM', 'DICOM');
return createTarGz(files.map((fileName) => {
const filePath = path.join(dicomDir, fileName);
const filePath = getProjectDicomFilePath(project, fileName);
const stat = fs.statSync(filePath);
return {
name: `Head_CT_DICOM/${fileName}`,
name: `${rootName}/${fileName}`,
data: fs.readFileSync(filePath),
mtime: stat.mtimeMs / 1000,
};
@@ -1939,7 +2065,7 @@ function formatNumber(value: number | null | undefined, digits = 3) {
function createDicomInfo(project: ProjectRecord, files: string[]) {
const attributes = files.map((fileName) => {
const buffer = fs.readFileSync(path.join(dicomDir, fileName));
const buffer = fs.readFileSync(getProjectDicomFilePath(project, fileName));
return parseDicomAttributes(buffer, 'default');
});
const first = attributes[0];
@@ -2009,7 +2135,7 @@ async function startServer() {
const port = Number(portArg ?? 4000);
ensureDir(exportDir);
app.use(express.json());
app.use(express.json({ limit: '512mb' }));
app.get('/api/health', (_req, res) => {
res.json({ ok: true, service: 'revoxelseg-dicom', time: now() });
@@ -2225,6 +2351,65 @@ async function startServer() {
res.json(project);
});
app.post('/api/projects/:projectId/import-assets', (req, res) => {
const kind = req.body?.kind === 'stl' ? 'stl' : 'dicom';
let uploadedFiles: UploadedAssetPayload[];
try {
uploadedFiles = parseUploadedAssets(req.body?.files);
} catch (error) {
res.status(400).json({ message: error instanceof Error ? error.message : '上传文件列表无效' });
return;
}
if (!uploadedFiles.length) {
res.status(400).json({ message: '请选择需要导入的文件' });
return;
}
const state = readState();
const project = findProject(state, req.params.projectId);
if (!project) {
res.status(404).json({ message: '项目不存在' });
return;
}
const targetDir = path.join(uploadDir, project.id, kind === 'dicom' ? 'DICOM' : 'STL');
try {
fs.rmSync(targetDir, { recursive: true, force: true });
ensureDir(targetDir);
uploadedFiles.forEach((file, index) => {
const fileName = sanitizeUploadFileName(
file.name,
kind === 'dicom' ? `slice-${String(index + 1).padStart(4, '0')}.dcm` : `model-${index + 1}.stl`,
kind === 'dicom' ? '.dcm' : '.stl',
);
fs.writeFileSync(path.join(targetDir, fileName), decodeUploadedAssetData(file.data));
});
if (kind === 'dicom') {
const dicomFiles = listFiles(targetDir, '.dcm');
project.dicomPath = toRepoRelativePath(targetDir);
project.dicomCount = dicomFiles.length;
project.segmentationResults = [];
} else {
const stlFiles = listFiles(targetDir, '.stl');
project.modelPath = toRepoRelativePath(targetDir);
project.stlFiles = stlFiles;
project.modelCount = stlFiles.length;
project.hasModel = stlFiles.length > 0;
project.moduleStyles = buildModuleStyles(stlFiles, project.moduleStyles);
project.segmentationResults = [];
}
project.status = project.dicomCount > 0 && project.hasModel ? 'completed' : 'pending';
clearProjectRuntimeCaches(project.id);
writeState(state);
res.json(project);
} catch (error) {
res.status(422).json({ message: error instanceof Error ? error.message : '项目资产导入失败' });
}
});
app.post('/api/projects/:projectId/segmentation-results', (req, res) => {
const state = readState();
const project = findProject(state, req.params.projectId);
@@ -2296,7 +2481,7 @@ async function startServer() {
let payload: unknown;
if (plane === 'axial') {
const slice = Math.max(0, Math.min(files.length - 1, Number.isFinite(requestedSlice) ? requestedSlice : 0));
const preview = parseDicomPreview(path.join(dicomDir, files[slice]), mode);
const preview = parseDicomPreview(getProjectDicomFilePath(project, files[slice]), mode);
payload = {
...preview,
plane,
@@ -2306,7 +2491,7 @@ async function startServer() {
};
} else {
payload = {
...createReformattedPreview(files, plane, Number.isFinite(requestedSlice) ? requestedSlice : 0, mode),
...createReformattedPreview(project, files, plane, Number.isFinite(requestedSlice) ? requestedSlice : 0, mode),
plane,
};
}
@@ -2337,7 +2522,7 @@ async function startServer() {
const end = Number.parseInt(String(req.query.end ?? '49'), 10);
try {
res.json(createDicomFusionVolume(files, start, end, mode));
res.json(createDicomFusionVolume(project, files, start, end, mode));
} catch (error) {
res.status(422).json({ message: error instanceof Error ? error.message : 'DICOM 三维融合体生成失败' });
}
@@ -2357,7 +2542,7 @@ async function startServer() {
}
try {
const archive = createDicomTarGz(files);
const archive = createDicomTarGz(project, files);
const filename = `${project.id}-${project.dicomPath || 'DICOM'}-${files.length}-files.tar.gz`;
res.setHeader('Content-Type', 'application/gzip');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
@@ -2391,12 +2576,12 @@ async function startServer() {
const project = findProject(readState(), req.params.projectId);
const fileName = path.basename(req.params.fileName);
if (!project || project.id !== 'head-ct-demo' || !project.stlFiles.includes(fileName)) {
if (!project || !project.stlFiles.includes(fileName)) {
res.status(404).json({ message: '模型文件不存在' });
return;
}
res.sendFile(path.join(modelDir, fileName));
res.sendFile(getProjectModelFilePath(project, fileName));
});
app.get('/api/projects/:projectId/models/:fileName/preview', (req, res) => {
@@ -2404,13 +2589,13 @@ async function startServer() {
const fileName = path.basename(req.params.fileName);
const limit = Number.parseInt(String(req.query.limit ?? '5000'), 10);
if (!project || project.id !== 'head-ct-demo' || !project.stlFiles.includes(fileName)) {
if (!project || !project.stlFiles.includes(fileName)) {
res.status(404).json({ message: '模型文件不存在' });
return;
}
try {
res.json(createStlPreview(path.join(modelDir, fileName), fileName, Number.isFinite(limit) ? limit : 5000));
res.json(createStlPreview(getProjectModelFilePath(project, fileName), fileName, Number.isFinite(limit) ? limit : 5000));
} catch (error) {
res.status(422).json({ message: error instanceof Error ? error.message : 'STL 预览失败' });
}
@@ -2514,6 +2699,7 @@ async function startServer() {
const segmentationScope = req.query.segmentationScope === undefined
? latestResult?.segmentationScope ?? 'visible'
: parseSegmentationScope(req.query.segmentationScope);
const segmentationExportMode = parseSegmentationExportMode(req.query.segmentationExportMode);
const format = req.query.format === 'nii' ? 'nii' : 'nii.gz';
const compressed = format === 'nii.gz';
@@ -2527,6 +2713,7 @@ async function startServer() {
compressed,
activePose,
segmentationScope,
segmentationExportMode,
exportRoot: exportBase,
});
const filename = `${exportBase}.tar.gz`;
@@ -2561,7 +2748,8 @@ async function startServer() {
app.listen(port, host, () => {
console.log(`ReVoxelSeg DICOM server ready at http://${host}:${port}/`);
warmDicomVolumeCache(getProjectDicomFiles(buildDefaultProject()));
const defaultProject = buildDefaultProject();
warmDicomVolumeCache(defaultProject, getProjectDicomFiles(defaultProject));
});
}