point-cloud-convert
Version:
A simple JavaScript tool converting point cloud file from one format to another.
431 lines (380 loc) • 14.6 kB
JavaScript
(function () {
// supported will update once new point cloud format is supported
var supported = [
'ply2pcd',
'pcd2ply',
'asc2pcd',
'pcd2asc',
'asc2ply',
'ply2asc'
];
var doConvert = {};
// input lines, return result with destLines if success
doConvert.pcd2ply = function (lines) {
// get info from pcd header (including the first 11 lines)
var dataType = lines[10].split(' ')[1]; // get type of file (ascii or binary)
// TODO support pcd binary file
if (dataType === 'binary') {
return {
success: false,
msg: 'binary pcd file is not supported so far.'
}
}
var fields = lines[2].split(' ').slice(1);
var sizes = lines[3].split(' ').slice(1);
var types = lines[4].split(' ').slice(1);
var counts = lines[5].split(' ').slice(1);
var width = +lines[6].split(' ')[1];
var height = +lines[7].split(' ')[1];
var viewpoint = lines[8].split(' ').slice(1);
var points = +lines[9].split(' ')[1];
// varruct pcdObj
var dataLines = lines.slice(11);
var pcdObj = {};
pcdObj.width = width;
pcdObj.height = height;
pcdObj.points = points;
pcdObj.viewpoint = viewpoint;
for (var i = 0; i < fields.length; ++i) {
pcdObj[fields[i]] = dataLines.map(function(line) {
return +line.split(' ')[i];
});
pcdObj[fields[i] + '-size'] = +sizes[i];
pcdObj[fields[i] + '-type'] = types[i];
pcdObj[fields[i] + '-count'] = +counts[i];
}
// unpack rgb or rgba
if (pcdObj.rgb) {
pcdObj.r = [];
pcdObj.g = [];
pcdObj.b = [];
pcdObj.rgb.forEach(rgb => {
var rgbValue = unpackPCDColor(rgb);
// if rgb-type is F,rgbValue need to multiply by 2 (learn from practice)
if (pcdObj['rgb-type'] === 'F') {
rgbValue = rgbValue.map(function(value) { return 2 * value; });
}
pcdObj.r.push(rgbValue[0]);
pcdObj.g.push(rgbValue[1]);
pcdObj.b.push(rgbValue[2]);
});
}
if (pcdObj.rgba) {
pcdObj.r = [];
pcdObj.g = [];
pcdObj.b = [];
pcdObj.a = [];
pcdObj.rgba.forEach(rgb => {
var rgbValue = unpackPCDColor(rgb);
// if rgb-type is F,rgbValue need to multiply by 2 (learn from practice)
if (pcdObj['rgba-type'] === 'F') {
rgbValue = rgbValue.map(function(value) { return 2 * value; });
}
pcdObj.r.push(rgbValue[0]);
pcdObj.g.push(rgbValue[1]);
pcdObj.b.push(rgbValue[2]);
// default a = 255
pcdObj.a.push(255);
});
}
// convert pcdObj to ply format
var plyLines = [
'ply',
'format ascii 1.0',
'element vertex ' + pcdObj.points,
'property float x',
'property float y',
'property float z'
];
if (pcdObj.r) {
plyLines.push('property uchar red');
plyLines.push('property uchar green');
plyLines.push('property uchar blue');
plyLines.push('property uchar alpha');
}
plyLines = plyLines.concat([
'element camera 1',
'property float view_px',
'property float view_py',
'property float view_pz',
'property float x_axisx',
'property float x_axisy',
'property float x_axisz',
'property float y_axisx',
'property float y_axisy',
'property float y_axisz',
'property float z_axisx',
'property float z_axisy',
'property float z_axisz',
'property float focal',
'property float scalex',
'property float scaley',
'property float centerx',
'property float centery',
'property int viewportx',
'property int viewporty',
'property float k1',
'property float k2'
]);
plyLines.push('end_header');
// vertexes
for (var i = 0; i < pcdObj.points; ++i) {
var xyz = [pcdObj.x[i], pcdObj.y[i], pcdObj.z[i]];
var rgba = [];
if (pcdObj.r) {
rgba = [pcdObj.r[i], pcdObj.g[i], pcdObj.b[i], pcdObj.a ? pcdObj.a[i] : 255];
}
var vertexLine = xyz.concat(rgba).join(' ');
plyLines.push(vertexLine);
}
// TODO camera params
cameraParams = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 213, 1, 0, 0];
cameraParams[17] = pcdObj.width;
cameraParams[18] = pcdObj.height;
plyLines.push(cameraParams.join(' '));
plyLines.push(''); // this empty line is necessary
return {
success: true,
lines: plyLines
};
}
doConvert.ply2pcd = function (lines) {
// ply format: ascii, binary_little_endian, binary_big_endian
// TODO support ply binary file
if (lines.slice(0, 5).join('').indexOf('binary') !== -1) {
console.error('binary ply file is not supported.');
return;
}
// get info from header
var endHeaderInex = lines.indexOf('end_header');
var headerLines = lines.slice(0, endHeaderInex);
var dataLines = lines.slice(endHeaderInex + 1);
var elementSplitorIndexes = [];
headerLines.forEach((line, index) => {
if (line.indexOf('element') !== -1) {
elementSplitorIndexes.push(index);
}
});
// carmeraFieldLines are optional
var vertexFieldLines;
var cameraFieldLines;
var vertexFields;
var cameraFields;
if (elementSplitorIndexes.length === 2) {
vertexFieldLines = headerLines.slice(elementSplitorIndexes[0] + 1, elementSplitorIndexes[1]);
cameraFieldLines = headerLines.slice(elementSplitorIndexes[1] + 1);
cameraFields = cameraFieldLines.map(function(line) { return line.split(' ')[2]; });
} else {
vertexFieldLines = headerLines.slice(elementSplitorIndexes[0] + 1);
}
vertexFields = vertexFieldLines.map(function(line) { return line.split(' ')[2]; });
var points = +headerLines[elementSplitorIndexes[0]].split(' ')[2];
// split dataLines
var vertexDataLines = dataLines.slice(0, points + 1);
var cameraDataLines = dataLines.slice(points);
var plyObj = {};
plyObj.points = points;
plyObj.width = points;
plyObj.height = 1;
for (var i = 0; i < vertexFields.length; ++i) {
plyObj[vertexFields[i][0]] = vertexDataLines.map(function(line) { return +line.split(' ')[i]; });
}
if (cameraDataLines.length !== 0) {
for (var i = 0; i < cameraFields.length; ++i) {
plyObj[cameraFields[i]] = cameraDataLines.map(function(line) { return +line.split(' ')[i]; });
}
plyObj.width = +cameraDataLines[0].split(' ')[17];
plyObj.height = +cameraDataLines[0].split(' ')[18];
}
// convert plyObj to pcd format file
var pcdLines = [
'# .PCD v0.7 - Point Cloud Data file format',
'VERSION 0.7'
];
var midHeader = [];
// TODO only rgb is considered, but rgba?
if (plyObj.r) {
midHeader = [
'FIELDS x y z rgb',
'SIZE 4 4 4 4',
'TYPE F F F U',
'COUNT 1 1 1 1'
];
} else {
midHeader = [
'FIELDS x y z',
'SIZE 4 4 4',
'TYPE F F F',
'COUNT 1 1 1'
];
}
pcdLines = pcdLines.concat(midHeader);
pcdLines.push('WIDTH ' + plyObj.width);
pcdLines.push('HEIGHT ' + plyObj.height);
// TODO viewport is a constant
pcdLines.push('VIEWPOINT 0 0 0 1 0 0 0');
pcdLines.push('POINTS ' + plyObj.points);
pcdLines.push('DATA ascii');
for (var i = 0; i < plyObj.points; ++i) {
var lineItems = [plyObj.x[i], plyObj.y[i], plyObj.z[i]];
if (plyObj.r) {
lineItems.push(packPCDColor(plyObj.r[i], plyObj.g[i], plyObj.b[i]));
}
pcdLines.push(lineItems.join(' '));
}
pcdLines.push('');
return {
success: true,
lines: pcdLines
};
}
doConvert.asc2pcd = function (lines) {
lines = lines.filter(function(line) {
if (line.indexOf('#') !== -1) {
return false;
}
return true;
});
var pcdLines = [
'# .PCD v0.7 - Point Cloud Data file format',
'VERSION 0.7'
];
// number of element for each line
var fieldsCnt = lines[0].split(' ').length;
var midHeader = [];
if (fieldsCnt > 4) { // xyz, rgb...
midHeader = [
'FIELDS x y z rgb',
'SIZE 4 4 4 4',
'TYPE F F F U',
'COUNT 1 1 1 1'
];
} else { // only xyz
midHeader = [
'FIELDS x y z',
'SIZE 4 4 4',
'TYPE F F F',
'COUNT 1 1 1'
];
}
pcdLines = pcdLines.concat(midHeader);
// unorganized point cloud
var width = lines.length;
var height = 1;
var points = width * height;
pcdLines.push('WIDTH ' + width);
pcdLines.push('HEIGHT ' + height);
// TODO viewport is a constant
pcdLines.push('VIEWPOINT 0 0 0 1 0 0 0');
pcdLines.push('POINTS ' + points);
pcdLines.push('DATA ascii');
for (var i = 0; i < points; ++i) {
var items = lines[i].split(' ');
var lineItems = [+items[0], +items[1], +items[2]];
if (fieldsCnt > 4) {
var r = +items[3];
var g = +items[4];
var b = +items[5];
if (r <= 1 && r >= 0) r = Math.floor(r * 255);
if (g <= 1 && g >= 0) g = Math.floor(g * 255);
if (b <= 1 && b >= 0) b = Math.floor(b * 255);
lineItems.push(packPCDColor(r, g, b));
}
pcdLines.push(lineItems.join(' '));
}
pcdLines.push('');
return {
success: true,
lines: pcdLines
};
}
doConvert.pcd2asc = function (lines) {
// get info from header
var dataType = lines[10].split(' ')[1];
if (dataType === 'binary') {
return {
success: false,
msg: 'binary pcd file is not supported.'
}
}
var points = +lines[9].split(' ')[1];
var ascLines = [];
// TODO to be more precise
var fields = lines[2].split(' ').slice(1);
for (var i = 0; i < points; ++i) {
var lineItems = lines[i + 11].split(' ');
var ascLineItems = [+lineItems[0], +lineItems[1], +lineItems[2]];
if (fields.length > 3) {
ascLineItems = ascLineItems.concat(
unpackPCDColor(+lineItems[3]).map(function(value) { return (value / 255).toFixed(3); })
);
}
ascLines.push(ascLineItems.join(' '));
}
ascLines.push('');
return {
success: true,
lines: ascLines
};
}
doConvert.asc2ply = function (lines) {
// indirect conversion
var result = doConvert.asc2pcd(lines);
if (!result.success) {
return {
success: false,
msg: 'A error occurred in asc2ply period'
}
}
return doConvert.pcd2ply(result.lines);
}
doConvert.ply2asc = function (lines) {
// TODO 转换有问题,没有颜色,最后有 NaN
var result = doConvert.ply2pcd(lines);
if (!result.success) {
return {
success: false,
msg: 'A error occurred in pcd2asc period'
}
}
return doConvert.pcd2asc(result.lines);
}
// tool function
function packPCDColor(r, g, b) {
var rgb = (r << 16) | (g << 8) | b;
return rgb;
}
function unpackPCDColor(rgb) {
var r = (rgb >> 16) & 0x0000ff;
var g = (rgb >> 8) & 0x0000ff;
var b = (rgb) & 0x0000ff;
return [r, g, b];
}
/**
*
* @param {string} input input string
* @param {string} conversionType conversion type, eg. pcd2ply, ply2pcd
*/
var pointCloudConvert = function(input, conversionType) {
if (supported.indexOf(conversionType) === -1) {
throw new Error('can not support conversion type: ' + conversionType);
}
if (typeof input !== 'string') {
throw new Error('input must be a string');
}
var lines = input.split('\n').map(function(line) { return line.trim(); });
lines = lines.filter(function(line) { return line.length !== 0; });
var result = doConvert[conversionType](lines);
if (result.success) {
return result.lines.join('\n');
} else {
throw new Error('conversion ' + conversionType + ' failed: ' + result.msg);
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = pointCloudConvert;
}
if (typeof window !== 'undefined') {
window.pointCloudConvert = pointCloudConvert;
}
})();