E57 Writer Best Practices
This is a list of the best practices on interpreting the E2807 standard and using the libE57 library.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Click each item to expand its details.
-
IndexBounds and Structured Point Clouds
The standard states that the bounds are defined to be tight. In table 28, the standard defines rowMaximum to be "The maximum rowIndex value of any point represented by this IndexBounds object." The problem with this is that it doesn't meet the real world needs for structured point clouds.
One of the best ways to judge a standard is its ability to round-trip the data completely. Given a PTX (size 100,100) file of a scan where rows 92 to 100 were all invalid, converting this to E57 and dropping out all of the invalid points and converting it back to PTX (size 91,100) would NOT reproduce the identical file. It would be short rows 92 to 100. The current standard fails the test.
The best way to solve this problem is to set the IndexBounds to the grid size of the structured point cloud like this:
// SimpleAPI
e57::Data3D header;
...
header.indexBounds.rowMaximum = nSizeRows - 1;
header.indexBounds.rowMinimum = 0;
header.indexBounds.columnMaximum = nSizeColumns - 1;
header.indexBounds.columnMinimum = 0;
header.indexBounds.returnMaximum = nSizeReturns - 1;
header.indexBounds.returnMinimum = 0;
...
// FoundationAPI
StructureNode indexBounds = StructureNode(imf_);
indexBounds.set("rowMinimum", IntegerNode(imf_, 0 ));
indexBounds.set("rowMaximum", IntegerNode(imf_, nSizeRows - 1));
indexBounds.set("columnMinimum", IntegerNode(imf_, 0 ));
indexBounds.set("columnMaximum", IntegerNode(imf_, nSizeColumns - 1));
indexBounds.set("returnMinimum", IntegerNode(imf_, 0 ));
indexBounds.set("returnMaximum", IntegerNode(imf_, nSizeReturns - 1));
scan.set("indexBounds", indexBounds);
-
Converting Matrix to Quaternion
Creating a quaternion from a rotation matrix:
double mat[3][3]; //matrix
double w,x,y,z; //quaternion
...
double dTrace = mat[0][0] + mat[1][1] + mat[2][2] + 1.0;
if(dTrace > 0.000001)
{
double S = 2.0 * sqrt(dTrace);
x = (mat[2][1] - mat[1][2]) / S;
y = (mat[0][2] - mat[2][0]) / S;
z = (mat[1][0] - mat[0][1]) / S;
w = 0.25 * S;
}
else if((mat[0][0] > mat[1][1]) && (mat[0][0] > mat[2][2]))
{
double S = sqrt(1.0 + mat[0][0] - mat[1][1] - mat[2][2]) * 2.0;
x = 0.25 * S;
y = (mat[1][0] + mat[0][1]) / S;
z = (mat[0][2] + mat[2][0]) / S;
w = (mat[2][1] - mat[1][2]) / S;
}
else if(mat[1][1] > mat[2][2])
{
double S = sqrt(1.0 + mat[1][1] - mat[0][0] - mat[2][2]) * 2.0;
x = (mat[1][0] + mat[0][1]) / S;
y = 0.25 * S;
z = (mat[2][1] + mat[1][2]) / S;
w = (mat[0][2] - mat[2][0]) / S;
}
else
{
double S = sqrt(1.0 + mat[2][2] - mat[0][0] - mat[1][1]) * 2.0;
x = (mat[0][2] + mat[2][0]) / S;
y = (mat[2][1] + mat[1][2]) / S;
z = 0.25 * S;
w = (mat[1][0] - mat[0][1]) / S;
}
// normalize the quaternion if the matrix is not a clean rigid body matrix or if it has scaler information.
double len = sqrt( w*w + x*x + y*y + z*z);
if(len != 0.)
{
w /= len;
x /= len;
y /= len;
z /= len;
}
// SimpleAPI
header.pose.rotation.w = w;
header.pose.rotation.x = x;
header.pose.rotation.y = y;
header.pose.rotation.z = z;
// FoundationAPI
if( (w != 1.) || (x != 0.) || (y != 0.) || (z != 0.) )
{
StructureNode pose = StructureNode(imf_);
StructureNode rotation = StructureNode(imf_);
rotation.set("w", FloatNode(imf_, w));
rotation.set("x", FloatNode(imf_, x));
rotation.set("y", FloatNode(imf_, y));
rotation.set("z", FloatNode(imf_, z));
pose.set("rotation", rotation);
scan.set("pose", pose);
}
-
Writing Guids
If you choose to use Window's GUID as the E57 guid, use the following:
//create a string like "{6A91E935-5559-477b-BE0C-7CE4E0BDFB7C}"
#if defined(_MSC_VER) // Only for windows
//create guid
GUID guid;
CoCreateGuid((GUID*)&guid);
//convert to string
OLECHAR wbuffer[64];
StringFromGUID2(guid,&wbuffer[0],64);
//converts wchar to char
size_t converted = 0;
char strGuid[64];
wcstombs_s(&converted, strGuid, wbuffer, 64);
// SimpleAPI
header.guid = (char*) strGuid;
// FoundationAPI
root_.set("guid", StringNode(imf_, (char*) strGuid));
#else // Non-windows
# include "boost/uuid/uuid.hpp"
# include "boost/uuid/uuid_generators.hpp"
# include "boost/uuid/uuid_io.hpp"
//create uuid
boost::uuids::random_generator gen;
boost::uuids::uuid Uuid = gen();
//convert to string
std::stringstream ss;
ss << Uuid;
//add {}
std::string strUuid = '{';
strUuid.append(ss.str());
strUuid.append(1, '}');
// SimpleAPI
header.guid = strUuid.c_str();
// FoundationAPI
root_.set("guid", StringNode(imf_, strUuid.c_str()));
#endif
-
Writing GPS Date Time
Use this code to write the GPS Date Time fields.
// SimpleAPI
#if defined(_MSC_VER) // Only for windows
// Init SYSTEMTIME with the current time
SYSTEMTIME fileDateTime;
header.acquisitionStart.SetSystemTime(fileDateTime);
#else
// Init these with a UTC time
int year, month, day, hour, minute;
float seconds;
header.acquisitionStart.SetUTCDateTime(year, month, day, hour, minute, seconds);
#endif
// FoundationAPI
#ifdef _C_TIMECONV_H_
// Init these with a UTC time
unsigned short utc_year; //Universal Time Coordinated [year]
unsigned char utc_month; //1-12 months
unsigned char utc_day; //1-31 days
unsigned char utc_hour; //hours
unsigned char utc_minute; //minutes
float utc_seconds;//seconds
unsigned short gps_week; //GPS week (0-1024+)
double gps_tow; //GPS time of week(0-604800.0) seconds
bool result = TIMECONV_GetGPSTimeFromUTCTime(
&utc_year, &utc_month, &utc_day, &utc_hour, &utc_minute, &utc_seconds, &gps_week, &gps_tow);
double dateTimeValue = (gps_week * 604800.) + gps_tow;
int isAtomicClockReferenced = 0;
#endif
StructureNode acquisitionStart = StructureNode(imf_);
acquisitionStart.set("dateTimeValue", FloatNode(imf_, dateTimeValue));
acquisitionStart.set("isAtomicClockReferenced", IntegerNode(imf_, isAtomicClockReferenced));
scan.set("acquisitionStart", acquisitionStart);
-
Writing Scaled Integers
Use this code to setup scaled integer point data.
//choose a scale factor for the data
#define DATA_SCALE_FACTOR (.000001)
//minimum negative limit of the data
double minData;
//maximum positive limit of the data
double maxData;
// SimpleAPI
// check for the case where abs(minData) > maxData
double maxDist = abs(maxData) > abs(minData) ? abs(maxData) : abs(minData);
long maxRange = NextHigherPowerOfTwo((maxData - minData)/DATA_SCALE_FACTOR) - 1;
// Calculate the needed range
header.pointFields.pointRangeMinimum = ((double) -(maxRange + 1)) * DATA_SCALE_FACTOR;
header.pointFields.pointRangeMaximum = ((double) maxRange) * DATA_SCALE_FACTOR;
header.pointFields.pointRangeScaledInteger = DATA_SCALE_FACTOR;
// FoundationAPI
double pointRangeScale = DATA_SCALE_FACTOR;
double pointRangeOffset = 0.;
// check for the case where abs(minData) > maxData
double maxDist = abs(maxData) > abs(minData) ? abs(maxData) : abs(minData);
long maxRange = NextHigherPowerOfTwo((maxData - minData)/pointRangeScale) - 1;
// Calculate the needed range
double pointRangeMinimum = ((double) -(maxRange + 1)) * pointRangeScale;
double pointRangeMaximum = ((double) maxRange) * pointRangeScale;
// make integer
int64_t intRangeMinimum = (int64_t) floor((pointRangeMinimum - pointRangeOffset)/pointRangeScale +.5);
int64_t intRangeMaximum = (int64_t) floor((pointRangeMaximum - pointRangeOffset)/pointRangeScale +.5);
StructureNode proto = StructureNode(imf_);
proto.set("cartesianX", ScaledIntegerNode(imf_, 0,
intRangeMinimum, intRangeMaximum, pointRangeScale, pointRangeOffset));
proto.set("cartesianY", ScaledIntegerNode(imf_, 0,
intRangeMinimum, intRangeMaximum, pointRangeScale, pointRangeOffset));
proto.set("cartesianZ", ScaledIntegerNode(imf_, 0,
intRangeMinimum, intRangeMaximum, pointRangeScale, pointRangeOffset));
... // other point data fields
VectorNode codecs = VectorNode(imf_, true);
CompressedVectorNode points = CompressedVectorNode(imf_, proto, codecs);
scan.set("points", points);
-
Writing Cartesian Bounds
The standard only allows doubles to be used for cartesian bounds coordinates; however point data should use scaled integers to be efficient. This mismatch will cause round off errors that will be found by the validate57.exe tool. To fix this problem the bounds need to be passed through the same calculations as a scaled integer.
//use the scale factor for the point data
#define DATA_SCALE_FACTOR (.000001)
//calculate the minimum/maximum from the point data.
double MaxX,MinX,MaxY,MinY,MaxZ,MinZ;
// SimpleAPI
//do this only if the point coordinate data is a scaled integer.
header.cartesianBounds.xMaximum = ceil(MaxX / DATA_SCALE_FACTOR + 0.9999999) * DATA_SCALE_FACTOR;
header.cartesianBounds.xMinimum = floor(MinX / DATA_SCALE_FACTOR - 0.9999999) * DATA_SCALE_FACTOR;
header.cartesianBounds.yMaximum = ceil(MaxY / DATA_SCALE_FACTOR + 0.9999999) * DATA_SCALE_FACTOR;
header.cartesianBounds.yMinimum = floor(MinY / DATA_SCALE_FACTOR - 0.9999999) * DATA_SCALE_FACTOR;
header.cartesianBounds.zMaximum = ceil(MaxZ / DATA_SCALE_FACTOR + 0.9999999) * DATA_SCALE_FACTOR;
header.cartesianBounds.zMinimum = floor(MinZ / DATA_SCALE_FACTOR - 0.9999999) * DATA_SCALE_FACTOR;
// FoundationAPI
//do this only if the point coordinate data is a scaled integer.
StructureNode bbox = StructureNode(imf_);
bbox.set("xMinimum", FloatNode(imf_, floor(MinX / DATA_SCALE_FACTOR - 0.9999999) * DATA_SCALE_FACTOR));
bbox.set("xMaximum", FloatNode(imf_, ceil(MaxX / DATA_SCALE_FACTOR + 0.9999999) * DATA_SCALE_FACTOR));
bbox.set("yMinimum", FloatNode(imf_, floor(MinY / DATA_SCALE_FACTOR - 0.9999999) * DATA_SCALE_FACTOR));
bbox.set("yMaximum", FloatNode(imf_, ceil(MaxY / DATA_SCALE_FACTOR + 0.9999999) * DATA_SCALE_FACTOR));
bbox.set("zMinimum", FloatNode(imf_, floor(MinZ / DATA_SCALE_FACTOR - 0.9999999) * DATA_SCALE_FACTOR));
bbox.set("zMaximum", FloatNode(imf_, ceil(MaxZ / DATA_SCALE_FACTOR + 0.9999999) * DATA_SCALE_FACTOR));
scan.set("cartesianBounds", bbox);
-
Writing Spherical Bounds
The standard only allows doubles to be used for spherical bounds coordinates; however range point data should use scaled integers to be efficient. This mismatch will cause round off errors that will be found by the validate57.exe tool. To fix this problem the bounds need to be passed through the same calculations as a scaled integer.
//use the scale factor for the point data
#define DATA_SCALE_FACTOR (.000001)
//calculate the minimum/maximum from the point data.
double MaxRange,MinRange,AzimuthStart,AzimuthEnd,MaxElevation,MinElevation;
// SimpleAPI
//do this only if the point range coordinate data is a scaled integer.
header.sphericalBounds.rangeMaximum = ceil( MaxRange / DATA_SCALE_FACTOR +0.9999999) * DATA_SCALE_FACTOR;
header.sphericalBounds.rangeMinimum = floor( MinRange / DATA_SCALE_FACTOR -0.9999999) * DATA_SCALE_FACTOR;
header.sphericalBounds.azimuthEnd = AzimuthEnd;
header.sphericalBounds.azimuthStart = AzimuthStart;
header.sphericalBounds.elevationMaximum = MaxElevation;
header.sphericalBounds.elevationMinimum = MinElevation;
// FoundationAPI
//do this only if the point coordinate data is a scaled integer.
StructureNode sbox = StructureNode(imf_);
sbox.set("rangeMinimum", FloatNode(imf_, floor( MinRange / DATA_SCALE_FACTOR -0.9999999) * DATA_SCALE_FACTOR));
sbox.set("rangeMaximum", FloatNode(imf_, ceil( MaxRange / DATA_SCALE_FACTOR +0.9999999) * DATA_SCALE_FACTOR));
sbox.set("elevationMinimum", FloatNode(imf_, MinElevation));
sbox.set("elevationMaximum", FloatNode(imf_, MaxElevation));
sbox.set("azimuthStart", FloatNode(imf_, AzimuthStart));
sbox.set("azimuthEnd", FloatNode(imf_, AzimuthEnd));
scan.set("sphericalBounds", sbox);
This site is © Copyright 2010 E57.04 3D Imaging System File Format Committee, All Rights Reserved
|