2026-05-20-14-19-23 逆向分割结果流程调整

This commit is contained in:
2026-05-20 14:38:01 +08:00
parent 6a50287a2a
commit 2a599695e9
10 changed files with 616 additions and 124 deletions

View File

@@ -11,6 +11,8 @@ type DicomPlane = 'axial' | 'sagittal' | 'coronal';
type DicomDisplayMode = 'default' | 'bone' | 'soft' | 'contrast';
type ProjectExportTarget = 'dicom' | 'segmentation' | 'pose' | 'stl';
type SegmentationExportScope = 'all' | 'visible';
type SegmentationDisplayLevel = 'standard' | 'fine' | 'ultra' | 'solid';
type SegmentationDicomOpacityLevel = 'low' | 'medium' | 'high';
interface ModuleStyleRecord {
visible: boolean;
@@ -42,6 +44,13 @@ interface SegmentationResultRecord {
segmentationScope: SegmentationExportScope;
pose: ModelPoseValue;
moduleStyles: Record<string, ModuleStyleRecord>;
sliceStart: number;
sliceEnd: number;
mappingSlice: number;
displayLevel: SegmentationDisplayLevel;
dicomOpacityLevel: SegmentationDicomOpacityLevel;
showBounds: boolean;
cutEnabled: boolean;
}
interface UserRecord {
@@ -285,30 +294,51 @@ function normalizeSegmentationResults(
existing: Partial<SegmentationResultRecord>[] | undefined,
stlFiles: string[],
currentModuleStyles: Record<string, ModuleStyleRecord>,
dicomCount = 0,
) {
if (!Array.isArray(existing)) {
return [];
}
const maxSlice = Math.max(dicomCount - 1, 0);
const normalizeSlice = (value: unknown, fallback: number) => (
typeof value === 'number' && Number.isFinite(value)
? clampNumber(Math.round(value), 0, maxSlice)
: clampNumber(fallback, 0, maxSlice)
);
const normalizeDisplayLevel = (value: unknown): SegmentationDisplayLevel => (
value === 'fine' || value === 'ultra' || value === 'solid' ? value : 'standard'
);
const normalizeDicomOpacityLevel = (value: unknown): SegmentationDicomOpacityLevel => (
value === 'medium' || value === 'high' ? value : 'low'
);
return existing
.map((record, index): SegmentationResultRecord => {
const rawStyles = record?.moduleStyles && typeof record.moduleStyles === 'object' && !Array.isArray(record.moduleStyles)
? record.moduleStyles
: currentModuleStyles;
const sliceStart = normalizeSlice(record?.sliceStart, 0);
const sliceEnd = normalizeSlice(record?.sliceEnd, maxSlice);
return {
id: typeof record?.id === 'string' && record.id.trim()
? record.id.trim().slice(0, 80)
: `segmentation-${index}`,
name: typeof record?.name === 'string' && record.name.trim()
? record.name.trim().slice(0, 80)
: `分割结果 ${index + 1}`,
name: '逆向分割结果',
createdAt: typeof record?.createdAt === 'string' && record.createdAt.trim() ? record.createdAt : now(),
segmentationScope: record?.segmentationScope === 'all' ? 'all' : 'visible',
pose: normalizeModelPoseValue(record?.pose),
moduleStyles: buildModuleStyles(stlFiles, rawStyles),
sliceStart,
sliceEnd,
mappingSlice: normalizeSlice(record?.mappingSlice, sliceEnd),
displayLevel: normalizeDisplayLevel(record?.displayLevel),
dicomOpacityLevel: normalizeDicomOpacityLevel(record?.dicomOpacityLevel),
showBounds: typeof record?.showBounds === 'boolean' ? record.showBounds : true,
cutEnabled: typeof record?.cutEnabled === 'boolean' ? record.cutEnabled : false,
};
})
.slice(-20);
.slice(-1);
}
function buildDefaultProject(): ProjectRecord {
@@ -382,7 +412,7 @@ function normalizeState(state: AppState): AppState {
maskFormats: project.maskFormats ?? ['nii', 'nii.gz'],
moduleStyles,
modelPoses: normalizeModelPoses(project.modelPoses),
segmentationResults: normalizeSegmentationResults(project.segmentationResults, stlFiles, moduleStyles),
segmentationResults: normalizeSegmentationResults(project.segmentationResults, stlFiles, moduleStyles, project.dicomCount ?? 0),
};
})
: [];
@@ -401,6 +431,7 @@ function normalizeState(state: AppState): AppState {
savedDefaultProject?.segmentationResults,
defaultProject.stlFiles,
defaultModuleStyles,
defaultProject.dicomCount,
),
},
...customProjects,
@@ -1135,6 +1166,22 @@ function parseSegmentationScope(raw: unknown): SegmentationExportScope {
return raw === 'all' ? 'all' : 'visible';
}
function latestSegmentationResult(project: ProjectRecord) {
return project.segmentationResults?.[project.segmentationResults.length - 1];
}
function projectWithSegmentationResultStyles(project: ProjectRecord): ProjectRecord {
const latestResult = latestSegmentationResult(project);
if (!latestResult) {
return project;
}
return {
...project,
moduleStyles: latestResult.moduleStyles,
};
}
function parseExportTargets(raw: unknown): ProjectExportTarget[] {
const values = typeof raw === 'string' ? raw.split(',') : [];
const targets = values.filter((value): value is ProjectExportTarget => (
@@ -2071,17 +2118,26 @@ async function startServer() {
: project.moduleStyles;
const record: SegmentationResultRecord = {
id: `segmentation-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
name: rawName || `分割结果 ${project.segmentationResults.length + 1}`,
name: rawName || '逆向分割结果',
createdAt: now(),
segmentationScope: parseSegmentationScope(req.body?.segmentationScope),
pose: normalizeModelPoseValue(req.body?.pose as Partial<ModelPoseValue> | undefined),
moduleStyles: buildModuleStyles(project.stlFiles, rawStyles),
sliceStart: Number(req.body?.sliceStart),
sliceEnd: Number(req.body?.sliceEnd),
mappingSlice: Number(req.body?.mappingSlice),
displayLevel: req.body?.displayLevel as SegmentationDisplayLevel,
dicomOpacityLevel: req.body?.dicomOpacityLevel as SegmentationDicomOpacityLevel,
showBounds: typeof req.body?.showBounds === 'boolean' ? req.body.showBounds : true,
cutEnabled: typeof req.body?.cutEnabled === 'boolean' ? req.body.cutEnabled : false,
};
project.moduleStyles = record.moduleStyles;
project.segmentationResults = normalizeSegmentationResults(
[...(project.segmentationResults ?? []), record],
[record],
project.stlFiles,
project.moduleStyles,
record.moduleStyles,
project.dicomCount,
);
writeState(state);
res.status(201).json(project);
@@ -2276,12 +2332,16 @@ async function startServer() {
const requestedTarget = targetOverride ?? String(req.query.target ?? 'segmentation');
const target = requestedTarget === 'dicom' || requestedTarget === 'pose' ? requestedTarget : 'segmentation';
const activePose = parseModelPoseQuery(req.query.pose);
const segmentationScope = parseSegmentationScope(req.query.segmentationScope);
const exportProject = projectWithSegmentationResultStyles(project);
const latestResult = latestSegmentationResult(project);
const activePose = parseModelPoseQuery(req.query.pose) ?? latestResult?.pose;
const segmentationScope = req.query.segmentationScope === undefined
? latestResult?.segmentationScope ?? 'visible'
: parseSegmentationScope(req.query.segmentationScope);
try {
if (target === 'pose') {
const posePayload = createPoseExport(project, activePose);
const posePayload = createPoseExport(exportProject, activePose);
const filename = `${project.id}-pose-data.json`;
fs.writeFileSync(path.join(exportDir, filename), posePayload);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
@@ -2293,7 +2353,7 @@ async function startServer() {
const files = getProjectDicomFiles(project);
const format = req.query.format === 'nii' ? 'nii' : 'nii.gz';
const compressed = format === 'nii.gz';
const payload = createNiftiExport(project, files, target, compressed, activePose, segmentationScope);
const payload = createNiftiExport(exportProject, files, target, compressed, activePose, segmentationScope);
const suffix = target === 'dicom' ? 'dicom-image' : 'segmentation-label';
const filename = `${project.id}-${suffix}.${format}`;
fs.writeFileSync(path.join(exportDir, filename), payload);
@@ -2323,15 +2383,19 @@ async function startServer() {
return;
}
const activePose = parseModelPoseQuery(req.query.pose);
const segmentationScope = parseSegmentationScope(req.query.segmentationScope);
const exportProject = projectWithSegmentationResultStyles(project);
const latestResult = latestSegmentationResult(project);
const activePose = parseModelPoseQuery(req.query.pose) ?? latestResult?.pose;
const segmentationScope = req.query.segmentationScope === undefined
? latestResult?.segmentationScope ?? 'visible'
: parseSegmentationScope(req.query.segmentationScope);
const format = req.query.format === 'nii' ? 'nii' : 'nii.gz';
const compressed = format === 'nii.gz';
try {
const files = getProjectDicomFiles(project);
const payload = createProjectExportBundle({
project,
project: exportProject,
files,
targets,
compressed,