90 lines
3.1 KiB
TypeScript
90 lines
3.1 KiB
TypeScript
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { join } from 'node:path';
|
||
import { afterEach, describe, expect, it } from 'vitest';
|
||
import { samplePointcloudFile, samplePointcloudFiles } from './pointcloudSample';
|
||
|
||
const temporaryDirectories: string[] = [];
|
||
|
||
afterEach(() => {
|
||
while (temporaryDirectories.length > 0) {
|
||
rmSync(temporaryDirectories.pop()!, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
function writeBinaryPly(properties: string[], records: number[][]): string {
|
||
const directory = mkdtempSync(join(tmpdir(), 'pointcloud-sample-'));
|
||
temporaryDirectories.push(directory);
|
||
const filePath = join(directory, 'cloud.ply');
|
||
const header = Buffer.from([
|
||
'ply',
|
||
'format binary_little_endian 1.0',
|
||
`element vertex ${records.length}`,
|
||
...properties.map((name) => `property float ${name}`),
|
||
'end_header',
|
||
'',
|
||
].join('\n'), 'ascii');
|
||
const body = Buffer.alloc(records.length * properties.length * 4);
|
||
records.forEach((record, recordIndex) => {
|
||
record.forEach((value, propertyIndex) => {
|
||
body.writeFloatLE(value, (recordIndex * properties.length + propertyIndex) * 4);
|
||
});
|
||
});
|
||
writeFileSync(filePath, Buffer.concat([header, body]));
|
||
return filePath;
|
||
}
|
||
|
||
describe('samplePointcloudFile', () => {
|
||
it('不会为只有 xyz 的输入点云伪造误差', async () => {
|
||
const filePath = writeBinaryPly(
|
||
['x', 'y', 'z'],
|
||
[[1, 2, 3], [4, 5, 6]],
|
||
);
|
||
|
||
const points = await samplePointcloudFile(filePath, 1_000, 2);
|
||
|
||
expect(points).toHaveLength(2);
|
||
expect(points.every((point) => point.error === undefined)).toBe(true);
|
||
});
|
||
|
||
it.each(['error', 'deviation'])('保留 PLY 中真实的 %s 属性', async (errorProperty) => {
|
||
const filePath = writeBinaryPly(
|
||
['x', 'y', 'z', errorProperty],
|
||
[[1, 2, 3, 0.25], [4, 5, 6, -0.5]],
|
||
);
|
||
|
||
const points = await samplePointcloudFile(filePath, 1_000, 2);
|
||
|
||
expect(points.map((point) => point.error)).toEqual([0.25, -0.5]);
|
||
});
|
||
});
|
||
|
||
describe('samplePointcloudFiles', () => {
|
||
it('合并彩色与无色 PLY,并使用 28 字节彩色步长', async () => {
|
||
const plain = writeBinaryPly(['x', 'y', 'z'], [[1, 2, 3], [4, 5, 6]]);
|
||
const colored = writeBinaryPly(
|
||
['x', 'y', 'z', 'red', 'green', 'blue'],
|
||
[[7, 8, 9, 255, 128, 0], [10, 11, 12, 0, 64, 255]],
|
||
);
|
||
|
||
const result = await samplePointcloudFiles([plain, colored], 4, 10);
|
||
|
||
expect(result.pointCount).toBe(4);
|
||
expect(result.hasColor).toBe(true);
|
||
expect(result.hasError).toBe(false);
|
||
expect(result.buffer.byteLength).toBe(4 * 28);
|
||
expect(result.fileStats.map((file) => file.sampled)).toEqual([2, 2]);
|
||
});
|
||
|
||
it('限制总采样数量,无色 PLY 使用 16 字节步长', async () => {
|
||
const first = writeBinaryPly(['x', 'y', 'z'], Array.from({ length: 10 }, (_, i) => [i, i, i]));
|
||
const second = writeBinaryPly(['x', 'y', 'z'], Array.from({ length: 10 }, (_, i) => [i + 10, i, i]));
|
||
|
||
const result = await samplePointcloudFiles([first, second], 100, 6);
|
||
|
||
expect(result.pointCount).toBe(6);
|
||
expect(result.hasColor).toBe(false);
|
||
expect(result.buffer.byteLength).toBe(6 * 16);
|
||
});
|
||
});
|