feat: secure analysis PLY sampling
This commit is contained in:
@@ -3,6 +3,14 @@ import * as path from 'path';
|
||||
|
||||
export type SampledPoint = { x: number; y: number; z: number; error?: number; r?: number; g?: number; b?: number };
|
||||
|
||||
export type SampledPointcloudBatch = {
|
||||
buffer: Buffer;
|
||||
pointCount: number;
|
||||
hasColor: boolean;
|
||||
hasError: boolean;
|
||||
fileStats: Array<{ name: string; sizeGB: number; sampled: number }>;
|
||||
};
|
||||
|
||||
/** 与 fileRoutes 批量采样一致:随机 seek 文本点云行 */
|
||||
export async function randomSeekSample(
|
||||
filePath: string,
|
||||
@@ -356,3 +364,70 @@ export async function samplePointcloudFile(
|
||||
// .txt / .xyz / .pts / etc. — existing text line sampler
|
||||
return randomSeekSample(filePath, fileSize, k);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对一组已校验的点云文件按文件大小分配总采样配额,并编码为浏览器消费的二进制协议。
|
||||
* 每点固定包含 x/y/z/error;任一文件带颜色时,所有点再包含归一化后的 r/g/b。
|
||||
*/
|
||||
export async function samplePointcloudFiles(
|
||||
filePaths: string[],
|
||||
requestedTotalPoints: number,
|
||||
maxTotalPoints = 2_000_000,
|
||||
): Promise<SampledPointcloudBatch> {
|
||||
const totalPoints = Number.isFinite(requestedTotalPoints)
|
||||
? Math.max(0, Math.min(Math.floor(requestedTotalPoints), maxTotalPoints))
|
||||
: 0;
|
||||
const fileInfos = filePaths.map((filePath) => ({
|
||||
filePath,
|
||||
name: path.basename(filePath),
|
||||
size: fs.statSync(filePath).size,
|
||||
}));
|
||||
const totalBytes = fileInfos.reduce((sum, file) => sum + file.size, 0);
|
||||
|
||||
// floor 后把剩余名额依次补给文件,保证所有配额之和严格不超过总预算。
|
||||
const quotas = fileInfos.map((file) => totalBytes > 0
|
||||
? Math.floor((file.size / totalBytes) * totalPoints)
|
||||
: 0);
|
||||
let remainder = totalPoints - quotas.reduce((sum, quota) => sum + quota, 0);
|
||||
for (let index = 0; remainder > 0 && fileInfos.length > 0; index = (index + 1) % fileInfos.length) {
|
||||
quotas[index] += 1;
|
||||
remainder -= 1;
|
||||
}
|
||||
|
||||
const sampledPerFile = await Promise.all(
|
||||
fileInfos.map((file, index) => samplePointcloudFile(file.filePath, file.size, quotas[index])),
|
||||
);
|
||||
const pointCount = sampledPerFile.reduce((sum, points) => sum + points.length, 0);
|
||||
const hasError = sampledPerFile.some((points) => points.some((point) => Number.isFinite(point.error)));
|
||||
const hasColor = sampledPerFile.some((points) => points.some((point) => point.r !== undefined));
|
||||
const bytesPerPoint = hasColor ? 28 : 16;
|
||||
const buffer = Buffer.allocUnsafe(pointCount * bytesPerPoint);
|
||||
let offset = 0;
|
||||
|
||||
for (const points of sampledPerFile) {
|
||||
for (const point of points) {
|
||||
buffer.writeFloatLE(point.x, offset);
|
||||
buffer.writeFloatLE(point.y, offset + 4);
|
||||
buffer.writeFloatLE(point.z, offset + 8);
|
||||
buffer.writeFloatLE(point.error ?? Number.NaN, offset + 12);
|
||||
if (hasColor) {
|
||||
buffer.writeFloatLE((point.r ?? 128) / 255, offset + 16);
|
||||
buffer.writeFloatLE((point.g ?? 128) / 255, offset + 20);
|
||||
buffer.writeFloatLE((point.b ?? 128) / 255, offset + 24);
|
||||
}
|
||||
offset += bytesPerPoint;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
buffer,
|
||||
pointCount,
|
||||
hasColor,
|
||||
hasError,
|
||||
fileStats: fileInfos.map((file, index) => ({
|
||||
name: file.name,
|
||||
sizeGB: +(file.size / 1e9).toFixed(3),
|
||||
sampled: sampledPerFile[index].length,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user