diff --git a/src/IO/AsciiPCLoader/asciipcloader.cpp b/src/IO/AsciiPCLoader/asciipcloader.cpp new file mode 100644 index 00000000000..c199e267d00 --- /dev/null +++ b/src/IO/AsciiPCLoader/asciipcloader.cpp @@ -0,0 +1,162 @@ +#include + +#include +#include //logs +#include +#include + +using std::ifstream; +using std::make_unique; +using std::runtime_error; +using std::to_string; + +const string asciiExt( "txt" ); + +namespace Ra { +namespace IO { + +vector AsciiPointCloudLoader::getFileExtensions() const { + return vector( { "*." + asciiExt } ); +} + +bool AsciiPointCloudLoader::handleFileExtension( const string& extension ) const { + return extension.compare( asciiExt ) == 0; +} + +Ra::Core::Asset::FileData* AsciiPointCloudLoader::loadFile( const string& filename ) { + using Ra::Core::Asset::FileData; + using Ra::Core::Asset::GeometryData; + using Ra::Core::Utils::logERROR; + using Ra::Core::Utils::logINFO; + + FileData* fileData = new Ra::Core::Asset::FileData( filename ); + + if ( !fileData->isInitialized() ) { + delete fileData; + LOG( logINFO ) << "Filedata could not be initialized"; + return nullptr; + } + + auto startTime = clock(); + // a unique name is required by the component messaging system + static int id = 0; + auto geometry = + make_unique( "TRAJ_PC_" + to_string( ++id ), GeometryData::POINT_CLOUD ); + if ( geometry == nullptr ) { + LOG( logERROR ) << "could not create geometry"; + return nullptr; + } + + geometry->setFrame( Ra::Core::Transform::Identity() ); + + ifstream stream( filename ); + if ( !stream.is_open() ) { + throw runtime_error( "Could not open ifstream to path " + filename ); + return nullptr; + } + else { + LOG( logINFO ) << "---Beginning file loading---"; + } + + /*reading how data is arranged*/ + string line; + + if ( !std::getline( stream, line ) ) { + delete fileData; + LOG( logINFO ) << "file does not contain ascii data format. aborting"; + return nullptr; + } + + // retrieving all properties from header line + std::stringstream words( line ); + vector tokens; + string temp; + while ( words >> temp ) { + tokens.emplace_back( temp ); + }; + + // making sure X, Y, Z and Time properties are found + int xpos = -1, ypos = -1, zpos = -1; + for ( size_t i = 0; i < tokens.size(); ++i ) { + string token = tokens[i]; + if ( xpos == -1 ) { + if ( token.compare( "X" ) == 0 ) { xpos = i; } + } + if ( ypos == -1 ) { + if ( token.compare( "Y" ) == 0 ) { ypos = i; } + } + if ( zpos == -1 ) { + if ( token.compare( "Z" ) == 0 ) { zpos = i; } + } + } + + if ( ( xpos == -1 ) || ( ypos == -1 ) || ( zpos == -1 ) ) { + delete fileData; + LOG( logERROR ) << "file does not contain mandatory properties (X, Y, Z, Time). aborting"; + return nullptr; + } + + Ra::Core::VectorArray& vertices = geometry->getGeometry().verticesWithLock(); + + // creating custom attribs for all other properties + vector> attribs; + attribs.reserve( tokens.size() - 3 ); + + for ( size_t i = 0; i < tokens.size(); ++i ) { + // skipping mandatory attribs + if ( ( (int)i == xpos ) || ( (int)i == ypos ) || ( (int)i == zpos ) ) { continue; } + attribs.emplace_back( + geometry->getGeometry().vertexAttribs().addAttrib( tokens[i] ) ); + } + + /*recording data*/ + vector pointRecord; + int i = 0; + double num; + while ( stream >> num ) { + pointRecord.emplace_back( num ); + + if ( i == (int)( tokens.size() - 1 ) ) { + vertices.emplace_back( Scalar( pointRecord[xpos] ), + Scalar( pointRecord[ypos] ), + Scalar( pointRecord[zpos] ) ); + size_t k = 0; + for ( size_t j = 0; j < tokens.size(); ++j ) { + if ( ( (int)j == xpos ) || ( (int)j == ypos ) || ( (int)j == zpos ) ) { continue; } + geometry->getGeometry() + .vertexAttribs() + .getDataWithLock( attribs[k] ) + .emplace_back( Scalar( pointRecord[j] ) ); + geometry->getGeometry().vertexAttribs().unlock( attribs[k] ); + ++k; + } + pointRecord.clear(); + } + i = ( i + 1 ) % tokens.size(); + } + stream.close(); + + geometry->getGeometry().verticesUnlock(); + + // finalizing + fileData->m_geometryData.clear(); + fileData->m_geometryData.reserve( 1 ); + fileData->m_geometryData.push_back( move( geometry ) ); + + fileData->m_loadingTime = ( clock() - startTime ) / Scalar( CLOCKS_PER_SEC ); + + LOG( logINFO ) << "---File loading ended---"; + + fileData->displayInfo(); + + fileData->m_processed = true; + + return fileData; +} + +string AsciiPointCloudLoader::name() const { + return "Plain Text (ASCII)"; +} + +} // namespace IO +} // namespace Ra diff --git a/src/IO/AsciiPCLoader/asciipcloader.hpp b/src/IO/AsciiPCLoader/asciipcloader.hpp new file mode 100644 index 00000000000..69c0022dfd6 --- /dev/null +++ b/src/IO/AsciiPCLoader/asciipcloader.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +using std::string; +using std::vector; + +namespace Ra { +namespace IO { + +class RA_IO_API AsciiPointCloudLoader : public Ra::Core::Asset::FileLoaderInterface +{ + public: + AsciiPointCloudLoader() = default; + virtual ~AsciiPointCloudLoader() = default; + + vector getFileExtensions() const override; + bool handleFileExtension( const std::string& extension ) const override; + + /** load vertices properties from ASCII .txt file + * + * expected content in file: header line + data records + * + * header line content : attribute names separated by blanks ( ) + * + * example: Time X Y Z Roll Pitch Heading sdX sdY sdZ + * + * X, Y, Z and Time properties (case sensitive) are mandatory + * for file loading. Failure to find any of these properties + * results in file reading failure + * + * X, Y, Z properties are aggregated in geometry vertex array, + * while all other properties are loaded as custom attribs in + * Radium + * + * @todo add aggregation for normals/colors + * @param [in] filename file path + * @return nullptr if file opening fails, geometry creation + * fails or one of the mandatory properties is not found + */ + Ra::Core::Asset::FileData* loadFile( const std::string& filename ) override; + string name() const override; +}; + +} // namespace IO +} // namespace Ra diff --git a/src/IO/LasLoader/lasloader.cpp b/src/IO/LasLoader/lasloader.cpp new file mode 100644 index 00000000000..f17bb02c380 --- /dev/null +++ b/src/IO/LasLoader/lasloader.cpp @@ -0,0 +1,273 @@ +#include + +#include +#include +#include + +using std::ifstream; +using std::ios; +using std::make_unique; +using std::runtime_error; +using std::to_string; + +const string lasExt( "las" ); + +namespace Ra { +namespace IO { + +vector LasLoader::getFileExtensions() const { + return vector( { "*." + lasExt } ); +} + +bool LasLoader::handleFileExtension( const string& extension ) const { + return extension.compare( lasExt ) == 0; +} + +double readDouble( const char* buffer ) { + double res = 0; + char* converter = reinterpret_cast( &res ); + std::copy( buffer, buffer + sizeof( double ), converter ); + return res; +} + +Ra::Core::Asset::FileData* LasLoader::loadFile( const string& filename ) { + using Ra::Core::VectorArray; + using Ra::Core::Asset::FileData; + using Ra::Core::Asset::GeometryData; + using Ra::Core::Utils::logERROR; + using Ra::Core::Utils::logINFO; + using Ra::Core::Utils::logWARNING; + + FileData* fileData = new Ra::Core::Asset::FileData( filename ); + + if ( !fileData->isInitialized() ) { + delete fileData; + LOG( logWARNING ) << "Filedata could not be initialized"; + return nullptr; + } + + auto startTime = clock(); + // a unique name is required by the component messaging system + static int nameId = 0; + auto geometry = + make_unique( "LAS_PC_" + to_string( ++nameId ), GeometryData::POINT_CLOUD ); + if ( geometry == nullptr ) { + LOG( logERROR ) << "could not create geometry"; + return nullptr; + } + geometry->setFrame( Ra::Core::Transform::Identity() ); + + ifstream stream( filename, ios::out | ios::binary ); + + if ( !stream.is_open() ) { + throw runtime_error( "Could not open binary ifstream to path " + filename ); + return nullptr; + } + else { + LOG( logINFO ) << "---Beginning file loading---"; + } + + /*header part*/ + char buffer[48]; + + // reading minor version (needed to check data format) + stream.seekg( 0 ); + stream.seekg( 25 ); + if ( !stream.read( buffer, 1 ) ) { + delete fileData; + throw runtime_error( "Could not read minor version" ); + return nullptr; + } + unsigned char minor = *(unsigned char*)buffer; + + if ( ( minor < 1 ) || ( minor > 4 ) ) { + delete fileData; + LOG( logERROR ) << "LAS version 1." << (int)minor << " unsupported"; + return nullptr; + } + else { + LOG( logINFO ) << "LAS version 1." << (int)minor; + } + + // reading distance from file start to first point record + stream.seekg( 0 ); + stream.seekg( 96 ); + if ( !stream.read( buffer, 4 ) ) { + delete fileData; + throw runtime_error( "Could not read offset to point records" ); + return nullptr; + } + unsigned int offset = *(unsigned int*)buffer; + + // reading point data format + stream.seekg( 0 ); + stream.seekg( 104 ); + if ( !stream.read( buffer, 1 ) ) { + delete fileData; + throw runtime_error( "Could not read data format" ); + return nullptr; + } + unsigned char data_format = *(unsigned char*)buffer; + + if ( ( data_format > 3 && minor < 3 ) || ( data_format > 5 && minor == 3 ) || + ( data_format > 6 && minor == 4 ) ) { + delete fileData; + throw runtime_error( "Corrupted file. Unvalid data format" ); + return nullptr; + } + else { + LOG( logINFO ) << "Data format " << (int)data_format; + LOG( logINFO ) << "Loading properties \"x\", \"y\", \"z\","; + } + + // reading data record length (bytes) + if ( !stream.read( buffer, 2 ) ) { + delete fileData; + throw runtime_error( "Could not read point record length" ); + return nullptr; + } + unsigned short data_len = *(unsigned short*)buffer; + + // reading total number of data records + if ( !stream.read( buffer, 4 ) ) { + delete fileData; + throw runtime_error( "Could not read number of point records" ); + return nullptr; + } + unsigned int nb_data = *(unsigned int*)buffer; + + // reading scales and offsets (used for computing coordinates) + stream.seekg( 0 ); + stream.seekg( 131 ); + if ( !stream.read( buffer, 48 ) ) { + delete fileData; + throw runtime_error( "Could not read scale and offset fields" ); + return nullptr; + } + + double scale_x = readDouble( buffer ); + double scale_y = readDouble( buffer + 8 ); + double scale_z = readDouble( buffer + 16 ); + double offset_x = readDouble( buffer + 24 ); + double offset_y = readDouble( buffer + 32 ); + double offset_z = readDouble( buffer + 40 ); + + /*loading properties*/ + vector point( data_len ); + + VectorArray& vertices = geometry->getGeometry().verticesWithLock(); + vertices.reserve( nb_data ); + // checking for colors + Ra::Core::Utils::AttribHandle handle_color; + Ra::Core::VectorArray colors; + if ( data_format == 2 || data_format == 3 || ( minor >= 3 && data_format == 5 ) ) { + handle_color = geometry->getGeometry().vertexAttribs().addAttrib( + Ra::Core::Geometry::getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_COLOR ) ); + LOG( logINFO ) << "\"red\", \"green\", \"blue\"."; + } + + // checking for GPS Time + Ra::Core::Utils::AttribHandle handle_time; + Ra::Core::VectorArray gps_time; + if ( ( data_format == 1 ) || ( data_format == 3 ) || + ( ( minor > 2 ) && ( data_format == 4 ) ) || ( ( minor > 2 ) && ( data_format == 5 ) ) || + ( ( minor == 4 ) && ( data_format == 6 ) ) ) { + handle_time = geometry->getGeometry().vertexAttribs().addAttrib( "gps_time" ); + LOG( logINFO ) << "\"GPS time\"."; + } + + for ( unsigned int i = 0; i < nb_data; ++i ) { + // positioning at point data location in file + int pos = offset + i * data_len; + stream.seekg( 0 ); + stream.seekg( pos ); + + // reading point data + if ( !stream.read( point.data(), data_len ) ) { + delete fileData; + throw runtime_error( "Could not read point record " + to_string( i ) ); + return nullptr; + } + + // extracting initial x y z coordinates + int ix; + int iy; + int iz; + + ix = *(int*)( point.data() ); + iy = *(int*)( point.data() + 4 ); + iz = *(int*)( point.data() + 8 ); + + // scaling to obtain actual coordinates + double x = (double)ix * scale_x + offset_x; + double y = (double)iy * scale_y + offset_y; + double z = (double)iz * scale_z + offset_z; + + // storing coordinates in vertices object + vertices.emplace_back( Scalar( x ), Scalar( y ), Scalar( z ) ); + + // loading colors if found + if ( handle_color.idx().isValid() ) { + unsigned short red, green, blue; + + if ( data_format == 5 || data_format == 3 ) { + red = *(unsigned short*)( point.data() + 28 ); + green = *(unsigned short*)( point.data() + 30 ); + blue = *(unsigned short*)( point.data() + 32 ); + } + else { + red = *(unsigned short*)( point.data() + 20 ); + green = *(unsigned short*)( point.data() + 22 ); + blue = *(unsigned short*)( point.data() + 24 ); + } + + colors.emplace_back( Scalar( red ) / 65536_ra, + Scalar( green ) / 65536_ra, + Scalar( blue ) / 65536_ra, + 1_ra ); + } + + if ( handle_time.idx().isValid() ) { + // loading GPS time if found + if ( data_format == 6 ) { + gps_time.emplace_back( Scalar( readDouble( point.data() + 22 ) ) ); + } + else { + gps_time.emplace_back( Scalar( readDouble( point.data() + 20 ) ) ); + } + } + } + stream.close(); + point.clear(); + + geometry->getGeometry().verticesUnlock(); + + if ( handle_color.idx().isValid() ) { + geometry->getGeometry().vertexAttribs().getAttrib( handle_color ).setData( colors ); + } + if ( handle_time.idx().isValid() ) { + geometry->getGeometry().vertexAttribs().getAttrib( handle_time ).setData( gps_time ); + } + + // finalizing + fileData->m_geometryData.clear(); + fileData->m_geometryData.reserve( 1 ); + fileData->m_geometryData.push_back( move( geometry ) ); + + fileData->m_loadingTime = ( clock() - startTime ) / Scalar( CLOCKS_PER_SEC ); + + LOG( logINFO ) << "---File loading ended---"; + + fileData->displayInfo(); + + fileData->m_processed = true; + + return fileData; +} + +string LasLoader::name() const { + return "LASer (.las)"; +} + +} // namespace IO +} // namespace Ra diff --git a/src/IO/LasLoader/lasloader.hpp b/src/IO/LasLoader/lasloader.hpp new file mode 100644 index 00000000000..fef0dedd849 --- /dev/null +++ b/src/IO/LasLoader/lasloader.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include //AttribManager, logs +#include + +using std::string; +using std::vector; + +namespace Ra { +namespace IO { + +class RA_IO_API LasLoader : public Ra::Core::Asset::FileLoaderInterface +{ + public: + LasLoader() = default; + virtual ~LasLoader() = default; + + vector getFileExtensions() const override; + bool handleFileExtension( const std::string& extension ) const override; + + /** load vertices from .las file + * + * load x, y, z point coordinates properties and; depending on + * data format parsed from header; red, green, blue and gps_time properties + * + * x, y, z coordinates are scaled according to scale and offset properties parsed + * from the file header + * + * support LAS specification version from 1.1 to 1.4 + * + * @note LAS specification lists other attributes that can be loaded + * for each point record (intensity, edge of flight line, scan direction, ...) however + * only the attributes mentionned above are loaded. See the official LAS specification + * for more details + * here + * @param [in] filename file path + * @return nullptr if file opening or geometry creation fails + */ + Ra::Core::Asset::FileData* loadFile( const std::string& filename ) override; + string name() const override; +}; + +} // namespace IO +} // namespace Ra diff --git a/src/IO/filelist.cmake b/src/IO/filelist.cmake index ee29da027be..4872ac65a01 100644 --- a/src/IO/filelist.cmake +++ b/src/IO/filelist.cmake @@ -4,9 +4,13 @@ # ./generateFilelistForModule.sh IO # ---------------------------------------------------- -set(io_sources CameraLoader/CameraLoader.cpp) +set(io_sources CameraLoader/CameraLoader.cpp AsciiPCLoader/asciipcloader.cpp + LasLoader/lasloader.cpp +) -set(io_headers CameraLoader/CameraLoader.hpp RaIO.hpp) +set(io_headers CameraLoader/CameraLoader.hpp LasLoader/lasloader.hpp + AsciiPCLoader/asciipcloader.hpp RaIO.hpp +) set(io_inlines)