Files
massive-cloud/server/pointcloudSample.ts

324 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as fs from 'fs';
import * as path from 'path';
export type SampledPoint = { x: number; y: number; z: number; error: number };
/** 与 fileRoutes 批量采样一致:随机 seek 文本点云行 */
export async function randomSeekSample(
filePath: string,
fileSize: number,
k: number,
): Promise<SampledPoint[]> {
if (fileSize === 0 || k === 0) return [];
const PROBE_BYTES = 64 * 1024;
const probeBuf = Buffer.alloc(Math.min(PROBE_BYTES, fileSize));
const probeFd = fs.openSync(filePath, 'r');
fs.readSync(probeFd, probeBuf, 0, probeBuf.length, 0);
fs.closeSync(probeFd);
const probeLines = probeBuf.toString('utf8').split('\n').filter((l) => {
const t = l.trim();
return t && !t.startsWith('#') && !t.startsWith('//');
});
const sampleCount = Math.min(probeLines.length, 200);
const avgLineBytes =
sampleCount > 0
? probeLines.slice(0, sampleCount).reduce((s, l) => s + Buffer.byteLength(l) + 1, 0) / sampleCount
: 40;
console.log(
`[SeekSampler] ${path.basename(filePath)}: size=${(fileSize / 1e9).toFixed(2)}GB ` +
`avgLine=${avgLineBytes.toFixed(1)}B quota=${k}`,
);
const startOffset = Math.min(512, fileSize);
const usableSize = fileSize - startOffset;
if (usableSize <= 0) return [];
const interval = usableSize / k;
const seekPositions: number[] = [];
for (let i = 0; i < k; i++) {
const base = startOffset + Math.floor(i * interval);
const jitter = Math.floor(Math.random() * Math.min(interval * 0.5, 1024));
seekPositions.push(Math.min(base + jitter, fileSize - 2));
}
seekPositions.sort((a, b) => a - b);
const points: SampledPoint[] = [];
const fd = fs.openSync(filePath, 'r');
const lineBuf = Buffer.alloc(1024);
try {
for (const pos of seekPositions) {
if (points.length >= k) break;
const bytesRead = fs.readSync(fd, lineBuf, 0, lineBuf.length, pos);
if (bytesRead < 4) continue;
const chunk = lineBuf.toString('utf8', 0, bytesRead);
const nl1 = chunk.indexOf('\n');
if (nl1 === -1) continue;
const afterFirst = chunk.slice(nl1 + 1);
const nl2 = afterFirst.indexOf('\n');
const line = (nl2 === -1 ? afterFirst : afterFirst.slice(0, nl2)).trim();
if (!line || line.startsWith('#') || line.startsWith('//')) continue;
const parts = line.split(/[\s,\t]+/);
const x = parseFloat(parts[0]);
const y = parseFloat(parts[1]);
const z = parseFloat(parts[2]);
if (isNaN(x) || isNaN(y) || isNaN(z)) continue;
let error = parts.length >= 4 ? parseFloat(parts[3]) : NaN;
if (isNaN(error)) error = Math.abs(Math.sin(x * 0.5) * Math.cos(z * 0.5)) * 1.5;
points.push({ x, y, z, error });
}
} finally {
fs.closeSync(fd);
}
return points;
}
// ─── PLY 格式解析与采样 ───────────────────────────────────────────────────────
/** PLY 属性类型 → 字节数 */
const PLY_PROP_SIZES: Record<string, number> = {
char: 1, int8: 1,
uchar: 1, uint8: 1,
short: 2, int16: 2,
ushort: 2, uint16: 2,
int: 4, int32: 4,
uint: 4, uint32: 4,
float: 4, float32: 4,
double: 8, float64: 8,
};
interface PlyHeader {
/** 'ascii' | 'binary_little_endian' | 'binary_big_endian' */
format: string;
/** 顶点数 */
vertexCount: number;
/** 顶点元素各属性,顺序与文件一致 */
properties: Array<{ name: string; type: string; byteSize: number }>;
/** 'end_header\n' 之后的第一个字节偏移量(即数据区起始位置) */
dataOffset: number;
}
/**
* 读取 PLY 文件头(总是 ASCII,以 "end_header\n" 结尾)。
* 最多读取 16 KB,足以容纳任何合理的 PLY 头。
*/
function parsePlyHeader(filePath: string): PlyHeader | null {
const HEADER_MAX = 16 * 1024;
const buf = Buffer.alloc(HEADER_MAX);
const fd = fs.openSync(filePath, 'r');
const bytesRead = fs.readSync(fd, buf, 0, HEADER_MAX, 0);
fs.closeSync(fd);
const raw = buf.toString('ascii', 0, bytesRead);
// 必须以 "ply\n" 开头
if (!raw.startsWith('ply\n') && !raw.startsWith('ply\r\n')) return null;
const endHeaderMarker = 'end_header';
const endIdx = raw.indexOf(endHeaderMarker);
if (endIdx === -1) return null;
// 找到 end_header 之后换行符,跳过它
let dataStart = endIdx + endHeaderMarker.length;
if (raw[dataStart] === '\r') dataStart++;
if (raw[dataStart] === '\n') dataStart++;
const header = raw.slice(0, endIdx);
const lines = header.split(/\r?\n/);
let format = 'ascii';
let vertexCount = 0;
const properties: PlyHeader['properties'] = [];
let inVertex = false;
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts[0] === 'format') {
format = parts[1] ?? 'ascii';
} else if (parts[0] === 'element') {
inVertex = parts[1] === 'vertex';
if (inVertex) vertexCount = parseInt(parts[2] ?? '0', 10);
} else if (parts[0] === 'property' && inVertex) {
// skip "property list ..." (face element)
if (parts[1] === 'list') { inVertex = false; continue; }
const type = parts[1] ?? 'float';
const name = parts[2] ?? '';
const byteSize = PLY_PROP_SIZES[type] ?? 4;
properties.push({ name, type, byteSize });
}
}
if (vertexCount <= 0 || properties.length === 0) return null;
return { format, vertexCount, properties, dataOffset: dataStart };
}
/**
* 采样二进制 PLY 文件(little_endian 或 big_endian)。
* 策略:均匀间隔 + 小量随机抖动(与 randomSeekSample 一致)。
*/
function samplePlyBinary(
filePath: string,
header: PlyHeader,
k: number,
): SampledPoint[] {
const { vertexCount, properties, dataOffset, format } = header;
const littleEndian = format !== 'binary_big_endian';
const bytesPerVertex = properties.reduce((s, p) => s + p.byteSize, 0);
// 找 x, y, z(可能叫 x/y/z 或 X/Y/Z;允许宽松匹配)
let xOff = -1, yOff = -1, zOff = -1;
let xType = 'float', yType = 'float', zType = 'float';
let byteOff = 0;
for (const p of properties) {
const n = p.name.toLowerCase();
if (n === 'x') { xOff = byteOff; xType = p.type; }
else if (n === 'y') { yOff = byteOff; yType = p.type; }
else if (n === 'z') { zOff = byteOff; zType = p.type; }
byteOff += p.byteSize;
}
if (xOff === -1 || yOff === -1 || zOff === -1) {
console.warn(`[PLY] ${path.basename(filePath)}: could not find x/y/z properties`);
return [];
}
const actualK = Math.min(k, vertexCount);
const interval = vertexCount / actualK;
const indices: number[] = [];
for (let i = 0; i < actualK; i++) {
const base = Math.floor(i * interval);
const jitter = Math.floor(Math.random() * Math.max(1, interval * 0.5));
indices.push(Math.min(base + jitter, vertexCount - 1));
}
const recBuf = Buffer.alloc(bytesPerVertex);
const fd = fs.openSync(filePath, 'r');
const points: SampledPoint[] = [];
function readNum(buf: Buffer, off: number, type: string): number {
switch (type) {
case 'float': case 'float32': return littleEndian ? buf.readFloatLE(off) : buf.readFloatBE(off);
case 'double': case 'float64': return littleEndian ? buf.readDoubleLE(off) : buf.readDoubleBE(off);
case 'int': case 'int32': return littleEndian ? buf.readInt32LE(off) : buf.readInt32BE(off);
case 'uint': case 'uint32': return littleEndian ? buf.readUInt32LE(off) : buf.readUInt32BE(off);
case 'short': case 'int16': return littleEndian ? buf.readInt16LE(off) : buf.readInt16BE(off);
case 'ushort': case 'uint16': return littleEndian ? buf.readUInt16LE(off) : buf.readUInt16BE(off);
case 'char': case 'int8': return buf.readInt8(off);
case 'uchar': case 'uint8': return buf.readUInt8(off);
default: return littleEndian ? buf.readFloatLE(off) : buf.readFloatBE(off);
}
}
try {
for (const idx of indices) {
const filePos = dataOffset + idx * bytesPerVertex;
const n = fs.readSync(fd, recBuf, 0, bytesPerVertex, filePos);
if (n < bytesPerVertex) continue;
const x = readNum(recBuf, xOff, xType);
const y = readNum(recBuf, yOff, yType);
const z = readNum(recBuf, zOff, zType);
if (!isFinite(x) || !isFinite(y) || !isFinite(z)) continue;
const error = Math.abs(Math.sin(x * 0.001) * Math.cos(z * 0.001)) * 1.5;
points.push({ x, y, z, error });
}
} finally {
fs.closeSync(fd);
}
console.log(
`[PLY-Binary] ${path.basename(filePath)}: ${vertexCount} verts, ` +
`${bytesPerVertex}B/vert, sampled ${points.length}`,
);
return points;
}
/**
* 采样 ASCII PLY 文件。
* 复用 randomSeekSample 的均匀 seek 策略,跳过 PLY 头,从数据区随机取行。
*/
function samplePlyAscii(
filePath: string,
fileSize: number,
header: PlyHeader,
k: number,
): SampledPoint[] {
const { dataOffset } = header;
const usableSize = fileSize - dataOffset;
if (usableSize <= 0) return [];
const actualK = Math.min(k, header.vertexCount);
const interval = usableSize / actualK;
const seekPositions: number[] = [];
for (let i = 0; i < actualK; i++) {
const base = dataOffset + Math.floor(i * interval);
const jitter = Math.floor(Math.random() * Math.min(interval * 0.5, 1024));
seekPositions.push(Math.min(base + jitter, fileSize - 2));
}
const points: SampledPoint[] = [];
const fd = fs.openSync(filePath, 'r');
const lineBuf = Buffer.alloc(512);
try {
for (const pos of seekPositions) {
if (points.length >= actualK) break;
const bytesRead = fs.readSync(fd, lineBuf, 0, lineBuf.length, pos);
if (bytesRead < 4) continue;
const chunk = lineBuf.toString('utf8', 0, bytesRead);
const nl1 = chunk.indexOf('\n');
if (nl1 === -1) continue;
const afterFirst = chunk.slice(nl1 + 1);
const nl2 = afterFirst.indexOf('\n');
const line = (nl2 === -1 ? afterFirst : afterFirst.slice(0, nl2)).trim();
if (!line) continue;
const parts = line.split(/\s+/);
const x = parseFloat(parts[0]);
const y = parseFloat(parts[1]);
const z = parseFloat(parts[2]);
if (!isFinite(x) || !isFinite(y) || !isFinite(z)) continue;
const error = Math.abs(Math.sin(x * 0.001) * Math.cos(z * 0.001)) * 1.5;
points.push({ x, y, z, error });
}
} finally {
fs.closeSync(fd);
}
console.log(
`[PLY-ASCII] ${path.basename(filePath)}: sampled ${points.length} / ${header.vertexCount} verts`,
);
return points;
}
/**
* 通用采样入口:根据扩展名分发到对应采样器。
* 支持 .ply(ASCII + binary),.txt / .xyz / .pts(纯文本行)。
*/
export async function samplePointcloudFile(
filePath: string,
fileSize: number,
k: number,
): Promise<SampledPoint[]> {
if (fileSize === 0 || k === 0) return [];
const ext = path.extname(filePath).toLowerCase();
if (ext === '.ply') {
const header = parsePlyHeader(filePath);
if (!header) {
console.warn(`[PLY] ${path.basename(filePath)}: failed to parse header, falling back to text sampler`);
return randomSeekSample(filePath, fileSize, k);
}
if (header.format === 'ascii') {
return samplePlyAscii(filePath, fileSize, header, k);
}
return samplePlyBinary(filePath, header, k);
}
// .txt / .xyz / .pts / etc. — existing text line sampler
return randomSeekSample(filePath, fileSize, k);
}