2026-05-20-14-19-23 逆向分割结果流程调整
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user