2026-05-20-02-55-11 修复头部STL实体导出
This commit is contained in:
@@ -718,6 +718,58 @@ function intersectExportTriangleWithPlane(a: Point3DRecord, b: Point3DRecord, c:
|
||||
return maxDistance > 1e-8 ? segment : null;
|
||||
}
|
||||
|
||||
function readBinaryStlTriangleCount(buffer: Buffer, fileName: string) {
|
||||
if (buffer.length < 84) {
|
||||
throw new Error(`STL 文件内容为空或不完整:${fileName}`);
|
||||
}
|
||||
|
||||
const triangleCount = buffer.readUInt32LE(80);
|
||||
const expectedLength = 84 + triangleCount * 50;
|
||||
if (triangleCount <= 0 || expectedLength > buffer.length + 1024) {
|
||||
throw new Error(`当前仅支持二进制 STL:${fileName}`);
|
||||
}
|
||||
|
||||
return triangleCount;
|
||||
}
|
||||
|
||||
function forEachBinaryStlTriangle(
|
||||
filePath: string,
|
||||
fileName: string,
|
||||
callback: (
|
||||
ax: number,
|
||||
ay: number,
|
||||
az: number,
|
||||
bx: number,
|
||||
by: number,
|
||||
bz: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
cz: number,
|
||||
) => void,
|
||||
) {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const triangleCount = readBinaryStlTriangleCount(buffer, fileName);
|
||||
|
||||
for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
|
||||
const offset = 84 + triangleIndex * 50;
|
||||
if (offset + 50 > buffer.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
callback(
|
||||
buffer.readFloatLE(offset + 12),
|
||||
buffer.readFloatLE(offset + 16),
|
||||
buffer.readFloatLE(offset + 20),
|
||||
buffer.readFloatLE(offset + 24),
|
||||
buffer.readFloatLE(offset + 28),
|
||||
buffer.readFloatLE(offset + 32),
|
||||
buffer.readFloatLE(offset + 36),
|
||||
buffer.readFloatLE(offset + 40),
|
||||
buffer.readFloatLE(offset + 44),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function addExportSegmentToRows(rows: number[][], width: number, height: number, segment: PlaneSegmentRecord) {
|
||||
const deltaY = segment.b.y - segment.a.y;
|
||||
if (Math.abs(deltaY) < 0.01) {
|
||||
@@ -742,8 +794,61 @@ function addExportSegmentToRows(rows: number[][], width: number, height: number,
|
||||
}
|
||||
}
|
||||
|
||||
function fillExportInternalHoles(mask: Uint8Array, width: number, height: number) {
|
||||
const outside = new Uint8Array(width * height);
|
||||
const stack: number[] = [];
|
||||
const pushIfEmpty = (x: number, y: number) => {
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = y * width + x;
|
||||
if (outside[index] || mask[index]) {
|
||||
return;
|
||||
}
|
||||
|
||||
outside[index] = 1;
|
||||
stack.push(index);
|
||||
};
|
||||
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
pushIfEmpty(x, 0);
|
||||
pushIfEmpty(x, height - 1);
|
||||
}
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
pushIfEmpty(0, y);
|
||||
pushIfEmpty(width - 1, y);
|
||||
}
|
||||
|
||||
while (stack.length) {
|
||||
const index = stack.pop();
|
||||
if (index === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const x = index % width;
|
||||
const y = Math.floor(index / width);
|
||||
pushIfEmpty(x + 1, y);
|
||||
pushIfEmpty(x - 1, y);
|
||||
pushIfEmpty(x, y + 1);
|
||||
pushIfEmpty(x, y - 1);
|
||||
}
|
||||
|
||||
let patchedPixels = 0;
|
||||
for (let index = 0; index < mask.length; index += 1) {
|
||||
if (!outside[index] && !mask[index]) {
|
||||
mask[index] = 1;
|
||||
patchedPixels += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return patchedPixels;
|
||||
}
|
||||
|
||||
function fillExportRows(data: Buffer, width: number, height: number, slice: number, rows: number[][], label: number) {
|
||||
const sliceOffset = slice * width * height;
|
||||
const mask = new Uint8Array(width * height);
|
||||
let filledPixels = 0;
|
||||
|
||||
rows.forEach((intersections, row) => {
|
||||
if (intersections.length < 2) {
|
||||
return;
|
||||
@@ -768,10 +873,28 @@ function fillExportRows(data: Buffer, width: number, height: number, slice: numb
|
||||
const startX = clampNumber(Math.ceil(rawStartX), 0, width - 1);
|
||||
const endX = clampNumber(Math.floor(rawEndX), 0, width - 1);
|
||||
for (let x = startX; x <= endX; x += 1) {
|
||||
data[sliceOffset + row * width + x] = label;
|
||||
const index = row * width + x;
|
||||
if (!mask[index]) {
|
||||
mask[index] = 1;
|
||||
filledPixels += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (filledPixels === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
filledPixels += fillExportInternalHoles(mask, width, height);
|
||||
const sliceOffset = slice * width * height;
|
||||
for (let index = 0; index < mask.length; index += 1) {
|
||||
if (mask[index]) {
|
||||
data[sliceOffset + index] = label;
|
||||
}
|
||||
}
|
||||
|
||||
return filledPixels;
|
||||
}
|
||||
|
||||
function getModuleStyle(project: ProjectRecord, fileName: string, index: number): ModuleStyleRecord {
|
||||
@@ -836,25 +959,36 @@ function createSegmentationData(project: ProjectRecord, volume: DicomHuVolume, p
|
||||
return rows;
|
||||
};
|
||||
|
||||
for (let vertexIndex = 0; vertexIndex + 8 < payload.vertices.length; vertexIndex += 9) {
|
||||
const filePath = path.join(modelDir, fileName);
|
||||
forEachBinaryStlTriangle(filePath, fileName, (
|
||||
ax,
|
||||
ay,
|
||||
az,
|
||||
bx,
|
||||
by,
|
||||
bz,
|
||||
cx,
|
||||
cy,
|
||||
cz,
|
||||
) => {
|
||||
const a = transformPointForExportPose(
|
||||
payload.vertices[vertexIndex],
|
||||
payload.vertices[vertexIndex + 1],
|
||||
payload.vertices[vertexIndex + 2],
|
||||
ax,
|
||||
ay,
|
||||
az,
|
||||
metrics,
|
||||
pose,
|
||||
);
|
||||
const b = transformPointForExportPose(
|
||||
payload.vertices[vertexIndex + 3],
|
||||
payload.vertices[vertexIndex + 4],
|
||||
payload.vertices[vertexIndex + 5],
|
||||
bx,
|
||||
by,
|
||||
bz,
|
||||
metrics,
|
||||
pose,
|
||||
);
|
||||
const c = transformPointForExportPose(
|
||||
payload.vertices[vertexIndex + 6],
|
||||
payload.vertices[vertexIndex + 7],
|
||||
payload.vertices[vertexIndex + 8],
|
||||
cx,
|
||||
cy,
|
||||
cz,
|
||||
metrics,
|
||||
pose,
|
||||
);
|
||||
@@ -872,7 +1006,7 @@ function createSegmentationData(project: ProjectRecord, volume: DicomHuVolume, p
|
||||
b: mapPoint(segment.b),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rowsBySlice.forEach((rows, slice) => {
|
||||
fillExportRows(data, volume.width, volume.height, slice, rows, label);
|
||||
|
||||
Reference in New Issue
Block a user