From 07dabbd1867486c8dad6adde469262a7df76719f Mon Sep 17 00:00:00 2001 From: hesuicong Date: Mon, 21 Jul 2025 10:09:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4scripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/CMakeLists.txt | 14 + apps/DensifyPointCloud/CMakeLists.txt | 13 + apps/DensifyPointCloud/DensifyPointCloud.cpp | 433 + apps/InterfaceCOLMAP/CMakeLists.txt | 13 + apps/InterfaceCOLMAP/InterfaceCOLMAP.cpp | 1493 ++ apps/InterfaceCOLMAP/endian.h | 167 + apps/InterfaceCOLMAPTest/CMakeLists.txt | 13 + .../InterfaceCOLMAPTest.cpp | 1493 ++ apps/InterfaceCOLMAPTest/endian.h | 167 + apps/InterfaceMVSNet/CMakeLists.txt | 13 + apps/InterfaceMVSNet/InterfaceMVSNet.cpp | 706 + apps/InterfaceMetashape/CMakeLists.txt | 13 + .../InterfaceMetashape/InterfaceMetashape.cpp | 887 + apps/InterfaceOpenMVG/CMakeLists.txt | 23 + apps/InterfaceOpenMVG/InterfaceOpenMVG.cpp | 755 + apps/InterfacePolycam/CMakeLists.txt | 13 + apps/InterfacePolycam/InterfacePolycam.cpp | 362 + apps/InterfaceVisualSFM/CMakeLists.txt | 13 + apps/InterfaceVisualSFM/DataInterface.h | 383 + .../InterfaceVisualSFM/InterfaceVisualSFM.cpp | 607 + apps/InterfaceVisualSFM/Util.h | 756 + apps/ReconstructMesh/CMakeLists.txt | 13 + apps/ReconstructMesh/ReconstructMesh.cpp | 498 + apps/RefineMesh/CMakeLists.txt | 13 + apps/RefineMesh/RefineMesh.cpp | 266 + apps/Tests/CMakeLists.txt | 15 + apps/Tests/Tests.cpp | 136 + apps/Tests/data/images/00000.jpg | Bin 0 -> 286347 bytes apps/Tests/data/images/00001.jpg | Bin 0 -> 303625 bytes apps/Tests/data/images/00002.jpg | Bin 0 -> 295933 bytes apps/Tests/data/images/00003.jpg | Bin 0 -> 296986 bytes apps/Tests/data/scene.mvs | Bin 0 -> 119447 bytes apps/TextureMesh/CMakeLists.txt | 13 + apps/TextureMesh/TextureMesh.cpp | 1168 + apps/TransformScene/CMakeLists.txt | 13 + apps/TransformScene/TransformScene.cpp | 329 + apps/Viewer/CMakeLists.txt | 47 + apps/Viewer/Camera.cpp | 180 + apps/Viewer/Camera.h | 87 + apps/Viewer/Common.cpp | 36 + apps/Viewer/Common.h | 104 + apps/Viewer/Image.cpp | 124 + apps/Viewer/Image.h | 97 + apps/Viewer/Scene.cpp | 823 + apps/Viewer/Scene.h | 111 + apps/Viewer/Viewer.cpp | 226 + apps/Viewer/Window.cpp | 493 + apps/Viewer/Window.h | 167 + libs/CMakeLists.txt | 8 + libs/Common/AABB.h | 121 + libs/Common/AABB.inl | 419 + libs/Common/AutoEstimator.h | 668 + libs/Common/AutoPtr.h | 275 + libs/Common/CMakeLists.txt | 25 + libs/Common/Common.cpp | 47 + libs/Common/Common.h | 308 + libs/Common/Config.h | 281 + libs/Common/ConfigTable.cpp | 153 + libs/Common/ConfigTable.h | 82 + libs/Common/CriticalSection.h | 264 + libs/Common/EventQueue.cpp | 101 + libs/Common/EventQueue.h | 90 + libs/Common/FastDelegate.h | 2105 ++ libs/Common/FastDelegateBind.h | 240 + libs/Common/FastDelegateCPP11.h | 379 + libs/Common/File.h | 686 + libs/Common/Filters.h | 612 + libs/Common/HTMLDoc.h | 457 + libs/Common/HalfFloat.h | 159 + libs/Common/Hash.h | 303 + libs/Common/Line.h | 95 + libs/Common/Line.inl | 215 + libs/Common/LinkLib.h | 111 + libs/Common/List.h | 1704 ++ libs/Common/Log.cpp | 453 + libs/Common/Log.h | 211 + libs/Common/MemFile.h | 182 + libs/Common/OBB.h | 127 + libs/Common/OBB.inl | 401 + libs/Common/Octree.h | 237 + libs/Common/Octree.inl | 851 + libs/Common/Plane.h | 161 + libs/Common/Plane.inl | 623 + libs/Common/Queue.h | 296 + libs/Common/Random.h | 164 + libs/Common/Ray.h | 226 + libs/Common/Ray.inl | 951 + libs/Common/Rotation.h | 595 + libs/Common/Rotation.inl | 1563 ++ libs/Common/SML.cpp | 419 + libs/Common/SML.h | 116 + libs/Common/Sampler.inl | 274 + libs/Common/Semaphore.h | 170 + libs/Common/SharedPtr.h | 209 + libs/Common/Sphere.h | 56 + libs/Common/Sphere.inl | 80 + libs/Common/Streams.h | 244 + libs/Common/Strings.h | 239 + libs/Common/Thread.h | 435 + libs/Common/Timer.cpp | 165 + libs/Common/Timer.h | 141 + libs/Common/Types.cpp | 78 + libs/Common/Types.h | 2825 +++ libs/Common/Types.inl | 4039 +++ libs/Common/Util.cpp | 805 + libs/Common/Util.h | 835 + libs/Common/Util.inl | 1289 + libs/Common/UtilCUDA.cpp | 436 + libs/Common/UtilCUDA.h | 719 + libs/IO/CMakeLists.txt | 51 + libs/IO/Common.cpp | 12 + libs/IO/Common.h | 63 + libs/IO/Image.cpp | 935 + libs/IO/Image.h | 128 + libs/IO/ImageBMP.cpp | 261 + libs/IO/ImageBMP.h | 36 + libs/IO/ImageDDS.cpp | 341 + libs/IO/ImageDDS.h | 36 + libs/IO/ImageJPG.cpp | 281 + libs/IO/ImageJPG.h | 44 + libs/IO/ImagePNG.cpp | 307 + libs/IO/ImagePNG.h | 46 + libs/IO/ImageSCI.cpp | 78 + libs/IO/ImageSCI.h | 34 + libs/IO/ImageTGA.cpp | 146 + libs/IO/ImageTGA.h | 39 + libs/IO/ImageTIFF.cpp | 567 + libs/IO/ImageTIFF.h | 44 + libs/IO/OBJ.cpp | 290 + libs/IO/OBJ.h | 115 + libs/IO/PLY.cpp | 2386 ++ libs/IO/PLY.h | 315 + libs/IO/TinyXML2.cpp | 2987 +++ libs/IO/TinyXML2.h | 2368 ++ libs/IO/json.hpp | 20406 ++++++++++++++++ libs/IO/tiny_gltf.h | 7754 ++++++ libs/MVS.h | 53 + libs/MVS/CMakeLists.txt | 77 + libs/MVS/Camera.cpp | 437 + libs/MVS/Camera.h | 499 + libs/MVS/Common.cpp | 69 + libs/MVS/Common.h | 74 + libs/MVS/DepthMap.cpp | 2163 ++ libs/MVS/DepthMap.h | 526 + libs/MVS/Image.cpp | 434 + libs/MVS/Image.h | 146 + libs/MVS/Interface.h | 797 + libs/MVS/Mesh.cpp | 4617 ++++ libs/MVS/Mesh.h | 447 + libs/MVS/PatchMatchCUDA.cpp | 419 + libs/MVS/PatchMatchCUDA.cu | 662 + libs/MVS/PatchMatchCUDA.h | 58 + libs/MVS/PatchMatchCUDA.inl | 143 + libs/MVS/Platform.cpp | 55 + libs/MVS/Platform.h | 95 + libs/MVS/PointCloud.cpp | 696 + libs/MVS/PointCloud.h | 190 + libs/MVS/PythonWrapper.cpp | 142 + libs/MVS/RectsBinPack.cpp | 1334 + libs/MVS/RectsBinPack.h | 420 + libs/MVS/Scene.cpp | 2078 ++ libs/MVS/Scene.h | 180 + libs/MVS/SceneDensify.cpp | 2360 ++ libs/MVS/SceneDensify.h | 118 + libs/MVS/SceneReconstruct.cpp | 1162 + libs/MVS/SceneRefine.cpp | 1428 ++ libs/MVS/SceneRefineCUDA.cpp | 2866 +++ libs/MVS/SceneTexture.cpp | 7630 ++++++ libs/MVS/SemiGlobalMatcher.cpp | 2365 ++ libs/MVS/SemiGlobalMatcher.h | 217 + .../__pycache__/colmap_loader.cpython-310.pyc | Bin 0 -> 19981 bytes .../mask_face_occlusion.cpython-310.pyc | Bin 0 -> 11465 bytes libs/MVS/colmap_loader.py | 871 + libs/MVS/mask_face_occlusion.py | 565 + .../calculate_distance.cpython-310.pyc | Bin 0 -> 647 bytes .../__pycache__/colmap_loader.cpython-310.pyc | Bin 0 -> 19060 bytes .../__pycache__/get_caminfos.cpython-310.pyc | Bin 0 -> 945 bytes .../__pycache__/get_depth.cpython-310.pyc | Bin 0 -> 848 bytes .../get_difference_mask.cpython-310.pyc | Bin 0 -> 1734 bytes .../get_pose_matrix.cpython-310.pyc | Bin 0 -> 1520 bytes .../get_pose_matrix.cpython-311.pyc | Bin 0 -> 3089 bytes .../get_pose_matrix.cpython-312.pyc | Bin 0 -> 2171 bytes .../get_world_point.cpython-310.pyc | Bin 0 -> 2778 bytes .../project_to_image.cpython-310.pyc | Bin 0 -> 1577 bytes libs/MVS/utils/calculate_distance.py | 15 + libs/MVS/utils/colmap_loader.py | 840 + libs/MVS/utils/get_caminfos.py | 36 + libs/MVS/utils/get_depth.py | 26 + libs/MVS/utils/get_difference_mask.py | 53 + libs/MVS/utils/get_pose_matrix.py | 105 + libs/MVS/utils/get_world_point.py | 117 + libs/MVS/utils/project_to_image.py | 66 + libs/Math/CMakeLists.txt | 44 + libs/Math/Common.cpp | 12 + libs/Math/Common.h | 38 + libs/Math/IBFS/IBFS.cpp | 977 + libs/Math/IBFS/IBFS.h | 449 + libs/Math/IBFS/license.txt | 33 + libs/Math/LBP.h | 241 + libs/Math/LMFit/CHANGELOG | 115 + libs/Math/LMFit/COPYING | 30 + libs/Math/LMFit/lmmin.cpp | 1392 ++ libs/Math/LMFit/lmmin.h | 85 + libs/Math/RobustNorms.h | 176 + libs/Math/RobustNorms.png | Bin 0 -> 73478 bytes libs/Math/SimilarityTransform.cpp | 66 + libs/Math/SimilarityTransform.h | 55 + libs/Math/TRWS/CHANGES.TXT | 13 + libs/Math/TRWS/LICENSE.TXT | 31 + libs/Math/TRWS/MRFEnergy.h | 246 + libs/Math/TRWS/MRFEnergy.inl | 255 + libs/Math/TRWS/README.TXT | 21 + libs/Math/TRWS/instances.h | 22 + libs/Math/TRWS/minimize.inl | 299 + libs/Math/TRWS/ordering.inl | 151 + libs/Math/TRWS/treeProbabilities.inl | 36 + libs/Math/TRWS/typeBinary.h | 392 + libs/Math/TRWS/typeBinaryFast.h | 309 + libs/Math/TRWS/typeGeneral.h | 614 + libs/Math/TRWS/typePotts.h | 443 + libs/Math/TRWS/typeTruncatedLinear.h | 463 + libs/Math/TRWS/typeTruncatedLinear2D.h | 491 + libs/Math/TRWS/typeTruncatedQuadratic.h | 533 + libs/Math/TRWS/typeTruncatedQuadratic2D.h | 574 + 224 files changed, 130146 insertions(+) create mode 100644 apps/CMakeLists.txt create mode 100644 apps/DensifyPointCloud/CMakeLists.txt create mode 100644 apps/DensifyPointCloud/DensifyPointCloud.cpp create mode 100644 apps/InterfaceCOLMAP/CMakeLists.txt create mode 100644 apps/InterfaceCOLMAP/InterfaceCOLMAP.cpp create mode 100644 apps/InterfaceCOLMAP/endian.h create mode 100644 apps/InterfaceCOLMAPTest/CMakeLists.txt create mode 100644 apps/InterfaceCOLMAPTest/InterfaceCOLMAPTest.cpp create mode 100644 apps/InterfaceCOLMAPTest/endian.h create mode 100644 apps/InterfaceMVSNet/CMakeLists.txt create mode 100644 apps/InterfaceMVSNet/InterfaceMVSNet.cpp create mode 100644 apps/InterfaceMetashape/CMakeLists.txt create mode 100644 apps/InterfaceMetashape/InterfaceMetashape.cpp create mode 100644 apps/InterfaceOpenMVG/CMakeLists.txt create mode 100644 apps/InterfaceOpenMVG/InterfaceOpenMVG.cpp create mode 100644 apps/InterfacePolycam/CMakeLists.txt create mode 100644 apps/InterfacePolycam/InterfacePolycam.cpp create mode 100644 apps/InterfaceVisualSFM/CMakeLists.txt create mode 100644 apps/InterfaceVisualSFM/DataInterface.h create mode 100644 apps/InterfaceVisualSFM/InterfaceVisualSFM.cpp create mode 100644 apps/InterfaceVisualSFM/Util.h create mode 100644 apps/ReconstructMesh/CMakeLists.txt create mode 100644 apps/ReconstructMesh/ReconstructMesh.cpp create mode 100644 apps/RefineMesh/CMakeLists.txt create mode 100644 apps/RefineMesh/RefineMesh.cpp create mode 100644 apps/Tests/CMakeLists.txt create mode 100644 apps/Tests/Tests.cpp create mode 100644 apps/Tests/data/images/00000.jpg create mode 100644 apps/Tests/data/images/00001.jpg create mode 100644 apps/Tests/data/images/00002.jpg create mode 100644 apps/Tests/data/images/00003.jpg create mode 100644 apps/Tests/data/scene.mvs create mode 100644 apps/TextureMesh/CMakeLists.txt create mode 100644 apps/TextureMesh/TextureMesh.cpp create mode 100644 apps/TransformScene/CMakeLists.txt create mode 100644 apps/TransformScene/TransformScene.cpp create mode 100644 apps/Viewer/CMakeLists.txt create mode 100644 apps/Viewer/Camera.cpp create mode 100644 apps/Viewer/Camera.h create mode 100644 apps/Viewer/Common.cpp create mode 100644 apps/Viewer/Common.h create mode 100644 apps/Viewer/Image.cpp create mode 100644 apps/Viewer/Image.h create mode 100644 apps/Viewer/Scene.cpp create mode 100644 apps/Viewer/Scene.h create mode 100644 apps/Viewer/Viewer.cpp create mode 100644 apps/Viewer/Window.cpp create mode 100644 apps/Viewer/Window.h create mode 100644 libs/CMakeLists.txt create mode 100644 libs/Common/AABB.h create mode 100644 libs/Common/AABB.inl create mode 100644 libs/Common/AutoEstimator.h create mode 100644 libs/Common/AutoPtr.h create mode 100644 libs/Common/CMakeLists.txt create mode 100644 libs/Common/Common.cpp create mode 100644 libs/Common/Common.h create mode 100644 libs/Common/Config.h create mode 100644 libs/Common/ConfigTable.cpp create mode 100644 libs/Common/ConfigTable.h create mode 100644 libs/Common/CriticalSection.h create mode 100644 libs/Common/EventQueue.cpp create mode 100644 libs/Common/EventQueue.h create mode 100644 libs/Common/FastDelegate.h create mode 100644 libs/Common/FastDelegateBind.h create mode 100644 libs/Common/FastDelegateCPP11.h create mode 100644 libs/Common/File.h create mode 100644 libs/Common/Filters.h create mode 100644 libs/Common/HTMLDoc.h create mode 100644 libs/Common/HalfFloat.h create mode 100644 libs/Common/Hash.h create mode 100644 libs/Common/Line.h create mode 100644 libs/Common/Line.inl create mode 100644 libs/Common/LinkLib.h create mode 100644 libs/Common/List.h create mode 100644 libs/Common/Log.cpp create mode 100644 libs/Common/Log.h create mode 100644 libs/Common/MemFile.h create mode 100644 libs/Common/OBB.h create mode 100644 libs/Common/OBB.inl create mode 100644 libs/Common/Octree.h create mode 100644 libs/Common/Octree.inl create mode 100644 libs/Common/Plane.h create mode 100644 libs/Common/Plane.inl create mode 100644 libs/Common/Queue.h create mode 100644 libs/Common/Random.h create mode 100644 libs/Common/Ray.h create mode 100644 libs/Common/Ray.inl create mode 100644 libs/Common/Rotation.h create mode 100644 libs/Common/Rotation.inl create mode 100644 libs/Common/SML.cpp create mode 100644 libs/Common/SML.h create mode 100644 libs/Common/Sampler.inl create mode 100644 libs/Common/Semaphore.h create mode 100644 libs/Common/SharedPtr.h create mode 100644 libs/Common/Sphere.h create mode 100644 libs/Common/Sphere.inl create mode 100644 libs/Common/Streams.h create mode 100644 libs/Common/Strings.h create mode 100644 libs/Common/Thread.h create mode 100644 libs/Common/Timer.cpp create mode 100644 libs/Common/Timer.h create mode 100644 libs/Common/Types.cpp create mode 100644 libs/Common/Types.h create mode 100644 libs/Common/Types.inl create mode 100644 libs/Common/Util.cpp create mode 100644 libs/Common/Util.h create mode 100644 libs/Common/Util.inl create mode 100644 libs/Common/UtilCUDA.cpp create mode 100644 libs/Common/UtilCUDA.h create mode 100644 libs/IO/CMakeLists.txt create mode 100644 libs/IO/Common.cpp create mode 100644 libs/IO/Common.h create mode 100644 libs/IO/Image.cpp create mode 100644 libs/IO/Image.h create mode 100644 libs/IO/ImageBMP.cpp create mode 100644 libs/IO/ImageBMP.h create mode 100644 libs/IO/ImageDDS.cpp create mode 100644 libs/IO/ImageDDS.h create mode 100644 libs/IO/ImageJPG.cpp create mode 100644 libs/IO/ImageJPG.h create mode 100644 libs/IO/ImagePNG.cpp create mode 100644 libs/IO/ImagePNG.h create mode 100644 libs/IO/ImageSCI.cpp create mode 100644 libs/IO/ImageSCI.h create mode 100644 libs/IO/ImageTGA.cpp create mode 100644 libs/IO/ImageTGA.h create mode 100644 libs/IO/ImageTIFF.cpp create mode 100644 libs/IO/ImageTIFF.h create mode 100644 libs/IO/OBJ.cpp create mode 100644 libs/IO/OBJ.h create mode 100644 libs/IO/PLY.cpp create mode 100644 libs/IO/PLY.h create mode 100644 libs/IO/TinyXML2.cpp create mode 100644 libs/IO/TinyXML2.h create mode 100644 libs/IO/json.hpp create mode 100644 libs/IO/tiny_gltf.h create mode 100644 libs/MVS.h create mode 100644 libs/MVS/CMakeLists.txt create mode 100644 libs/MVS/Camera.cpp create mode 100644 libs/MVS/Camera.h create mode 100644 libs/MVS/Common.cpp create mode 100644 libs/MVS/Common.h create mode 100644 libs/MVS/DepthMap.cpp create mode 100644 libs/MVS/DepthMap.h create mode 100644 libs/MVS/Image.cpp create mode 100644 libs/MVS/Image.h create mode 100644 libs/MVS/Interface.h create mode 100644 libs/MVS/Mesh.cpp create mode 100644 libs/MVS/Mesh.h create mode 100644 libs/MVS/PatchMatchCUDA.cpp create mode 100644 libs/MVS/PatchMatchCUDA.cu create mode 100644 libs/MVS/PatchMatchCUDA.h create mode 100644 libs/MVS/PatchMatchCUDA.inl create mode 100644 libs/MVS/Platform.cpp create mode 100644 libs/MVS/Platform.h create mode 100644 libs/MVS/PointCloud.cpp create mode 100644 libs/MVS/PointCloud.h create mode 100644 libs/MVS/PythonWrapper.cpp create mode 100644 libs/MVS/RectsBinPack.cpp create mode 100644 libs/MVS/RectsBinPack.h create mode 100644 libs/MVS/Scene.cpp create mode 100644 libs/MVS/Scene.h create mode 100644 libs/MVS/SceneDensify.cpp create mode 100644 libs/MVS/SceneDensify.h create mode 100644 libs/MVS/SceneReconstruct.cpp create mode 100644 libs/MVS/SceneRefine.cpp create mode 100644 libs/MVS/SceneRefineCUDA.cpp create mode 100644 libs/MVS/SceneTexture.cpp create mode 100644 libs/MVS/SemiGlobalMatcher.cpp create mode 100644 libs/MVS/SemiGlobalMatcher.h create mode 100644 libs/MVS/__pycache__/colmap_loader.cpython-310.pyc create mode 100644 libs/MVS/__pycache__/mask_face_occlusion.cpython-310.pyc create mode 100644 libs/MVS/colmap_loader.py create mode 100755 libs/MVS/mask_face_occlusion.py create mode 100644 libs/MVS/utils/__pycache__/calculate_distance.cpython-310.pyc create mode 100644 libs/MVS/utils/__pycache__/colmap_loader.cpython-310.pyc create mode 100644 libs/MVS/utils/__pycache__/get_caminfos.cpython-310.pyc create mode 100644 libs/MVS/utils/__pycache__/get_depth.cpython-310.pyc create mode 100644 libs/MVS/utils/__pycache__/get_difference_mask.cpython-310.pyc create mode 100644 libs/MVS/utils/__pycache__/get_pose_matrix.cpython-310.pyc create mode 100644 libs/MVS/utils/__pycache__/get_pose_matrix.cpython-311.pyc create mode 100644 libs/MVS/utils/__pycache__/get_pose_matrix.cpython-312.pyc create mode 100644 libs/MVS/utils/__pycache__/get_world_point.cpython-310.pyc create mode 100644 libs/MVS/utils/__pycache__/project_to_image.cpython-310.pyc create mode 100644 libs/MVS/utils/calculate_distance.py create mode 100644 libs/MVS/utils/colmap_loader.py create mode 100644 libs/MVS/utils/get_caminfos.py create mode 100644 libs/MVS/utils/get_depth.py create mode 100644 libs/MVS/utils/get_difference_mask.py create mode 100644 libs/MVS/utils/get_pose_matrix.py create mode 100644 libs/MVS/utils/get_world_point.py create mode 100644 libs/MVS/utils/project_to_image.py create mode 100644 libs/Math/CMakeLists.txt create mode 100644 libs/Math/Common.cpp create mode 100644 libs/Math/Common.h create mode 100644 libs/Math/IBFS/IBFS.cpp create mode 100644 libs/Math/IBFS/IBFS.h create mode 100644 libs/Math/IBFS/license.txt create mode 100644 libs/Math/LBP.h create mode 100644 libs/Math/LMFit/CHANGELOG create mode 100644 libs/Math/LMFit/COPYING create mode 100644 libs/Math/LMFit/lmmin.cpp create mode 100644 libs/Math/LMFit/lmmin.h create mode 100644 libs/Math/RobustNorms.h create mode 100644 libs/Math/RobustNorms.png create mode 100644 libs/Math/SimilarityTransform.cpp create mode 100644 libs/Math/SimilarityTransform.h create mode 100644 libs/Math/TRWS/CHANGES.TXT create mode 100644 libs/Math/TRWS/LICENSE.TXT create mode 100644 libs/Math/TRWS/MRFEnergy.h create mode 100644 libs/Math/TRWS/MRFEnergy.inl create mode 100644 libs/Math/TRWS/README.TXT create mode 100644 libs/Math/TRWS/instances.h create mode 100644 libs/Math/TRWS/minimize.inl create mode 100644 libs/Math/TRWS/ordering.inl create mode 100644 libs/Math/TRWS/treeProbabilities.inl create mode 100644 libs/Math/TRWS/typeBinary.h create mode 100644 libs/Math/TRWS/typeBinaryFast.h create mode 100644 libs/Math/TRWS/typeGeneral.h create mode 100644 libs/Math/TRWS/typePotts.h create mode 100644 libs/Math/TRWS/typeTruncatedLinear.h create mode 100644 libs/Math/TRWS/typeTruncatedLinear2D.h create mode 100644 libs/Math/TRWS/typeTruncatedQuadratic.h create mode 100644 libs/Math/TRWS/typeTruncatedQuadratic2D.h diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt new file mode 100644 index 0000000..3bb00c6 --- /dev/null +++ b/apps/CMakeLists.txt @@ -0,0 +1,14 @@ +# Add applications +ADD_SUBDIRECTORY(InterfaceCOLMAP) +ADD_SUBDIRECTORY(InterfaceMetashape) +ADD_SUBDIRECTORY(InterfaceMVSNet) +ADD_SUBDIRECTORY(InterfacePolycam) +ADD_SUBDIRECTORY(DensifyPointCloud) +ADD_SUBDIRECTORY(ReconstructMesh) +ADD_SUBDIRECTORY(RefineMesh) +ADD_SUBDIRECTORY(TextureMesh) +ADD_SUBDIRECTORY(TransformScene) +ADD_SUBDIRECTORY(Viewer) +if(OpenMVS_ENABLE_TESTS) + ADD_SUBDIRECTORY(Tests) +endif() diff --git a/apps/DensifyPointCloud/CMakeLists.txt b/apps/DensifyPointCloud/CMakeLists.txt new file mode 100644 index 0000000..38e2731 --- /dev/null +++ b/apps/DensifyPointCloud/CMakeLists.txt @@ -0,0 +1,13 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(DensifyPointCloud "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS DensifyPointCloud + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/DensifyPointCloud/DensifyPointCloud.cpp b/apps/DensifyPointCloud/DensifyPointCloud.cpp new file mode 100644 index 0000000..f314c77 --- /dev/null +++ b/apps/DensifyPointCloud/DensifyPointCloud.cpp @@ -0,0 +1,433 @@ +/* + * DensifyPointCloud.cpp + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#include + +using namespace MVS; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("DensifyPointCloud") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { +String strInputFileName; +String strPointCloudFileName; +String strOutputFileName; +String strViewNeighborsFileName; +String strOutputViewNeighborsFileName; +String strMeshFileName; +String strExportROIFileName; +String strImportROIFileName; +String strDenseConfigFileName; +String strExportDepthMapsName; +String strMaskPath; +float fMaxSubsceneArea; +float fSampleMesh; +float fBorderROI; +bool bCrop2ROI; +int nEstimateROI; +int nTowerMode; +int nFusionMode; +float fEstimateScale; +int thFilterPointCloud; +int nExportNumViews; +int nArchiveType; +int nProcessPriority; +unsigned nMaxThreads; +String strConfigFileName; +boost::program_options::variables_map vm; +} // namespace OPT + +class Application { +public: + Application() {} + ~Application() { Finalize(); } + + bool Initialize(size_t argc, LPCTSTR* argv); + void Finalize(); +}; // Application + +// initialize and parse the command line parameters +bool Application::Initialize(size_t argc, LPCTSTR* argv) +{ + // initialize log and console + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "produce this help message") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: -1-interface, 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + #ifdef _USE_CUDA + ("cuda-device", boost::program_options::value(&CUDA::desiredDeviceID)->default_value(-1), "CUDA device number to be used for depth-map estimation (-2 - CPU processing, -1 - best GPU, >=0 - device index)") + #endif + ; + + // group of options allowed both on command line and in config file + #ifdef _USE_CUDA + const unsigned nNumViewsDefault(8); + const unsigned numIters(4); + #else + const unsigned nNumViewsDefault(5); + const unsigned numIters(3); + #endif + unsigned nResolutionLevel; + unsigned nMaxResolution; + unsigned nMinResolution; + unsigned nNumViews; + unsigned nMinViewsFuse; + unsigned nSubResolutionLevels; + unsigned nEstimationIters; + unsigned nEstimationGeometricIters; + unsigned nEstimateColors; + unsigned nEstimateNormals; + unsigned nOptimize; + int nIgnoreMaskLabel; + bool bRemoveDmaps; + boost::program_options::options_description config("Densify options"); + config.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input filename containing camera poses and image list") + ("pointcloud-file,p", boost::program_options::value(&OPT::strPointCloudFileName), "sparse point-cloud with views file name to densify (overwrite existing point-cloud)") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the dense point-cloud (optional)") + ("view-neighbors-file", boost::program_options::value(&OPT::strViewNeighborsFileName), "input filename containing the list of views and their neighbors (optional)") + ("output-view-neighbors-file", boost::program_options::value(&OPT::strOutputViewNeighborsFileName), "output filename containing the generated list of views and their neighbors") + ("resolution-level", boost::program_options::value(&nResolutionLevel)->default_value(1), "how many times to scale down the images before point cloud computation") + ("max-resolution", boost::program_options::value(&nMaxResolution)->default_value(2560), "do not scale images higher than this resolution") + ("min-resolution", boost::program_options::value(&nMinResolution)->default_value(640), "do not scale images lower than this resolution") + ("sub-resolution-levels", boost::program_options::value(&nSubResolutionLevels)->default_value(2), "number of patch-match sub-resolution iterations (0 - disabled)") + ("number-views", boost::program_options::value(&nNumViews)->default_value(nNumViewsDefault), "number of views used for depth-map estimation (0 - all neighbor views available)") + ("number-views-fuse", boost::program_options::value(&nMinViewsFuse)->default_value(3), "minimum number of images that agrees with an estimate during fusion in order to consider it inlier (<2 - only merge depth-maps)") + ("ignore-mask-label", boost::program_options::value(&nIgnoreMaskLabel)->default_value(-1), "label value to ignore in the image mask, stored in the MVS scene or next to each image with '.mask.png' extension (<0 - disabled)") + ("mask-path", boost::program_options::value(&OPT::strMaskPath), "path to folder containing mask images with '.mask.png' extension") + ("iters", boost::program_options::value(&nEstimationIters)->default_value(numIters), "number of patch-match iterations") + ("geometric-iters", boost::program_options::value(&nEstimationGeometricIters)->default_value(2), "number of geometric consistent patch-match iterations (0 - disabled)") + ("estimate-colors", boost::program_options::value(&nEstimateColors)->default_value(2), "estimate the colors for the dense point-cloud (0 - disabled, 1 - final, 2 - estimate)") + ("estimate-normals", boost::program_options::value(&nEstimateNormals)->default_value(2), "estimate the normals for the dense point-cloud (0 - disabled, 1 - final, 2 - estimate)") + ("estimate-scale", boost::program_options::value(&OPT::fEstimateScale)->default_value(0.f), "estimate the point-scale for the dense point-cloud (scale multiplier, 0 - disabled)") + ("sub-scene-area", boost::program_options::value(&OPT::fMaxSubsceneArea)->default_value(0.f), "split the scene in sub-scenes such that each sub-scene surface does not exceed the given maximum sampling area (0 - disabled)") + ("sample-mesh", boost::program_options::value(&OPT::fSampleMesh)->default_value(0.f), "uniformly samples points on a mesh (0 - disabled, <0 - number of points, >0 - sample density per square unit)") + ("fusion-mode", boost::program_options::value(&OPT::nFusionMode)->default_value(0), "depth-maps fusion mode (-2 - fuse disparity-maps, -1 - export disparity-maps only, 0 - depth-maps & fusion, 1 - export depth-maps only)") + ("postprocess-dmaps", boost::program_options::value(&nOptimize)->default_value(7), "flags used to filter the depth-maps after estimation (0 - disabled, 1 - remove-speckles, 2 - fill-gaps, 4 - adjust-filter)") + ("filter-point-cloud", boost::program_options::value(&OPT::thFilterPointCloud)->default_value(0), "filter dense point-cloud based on visibility (0 - disabled)") + ("export-number-views", boost::program_options::value(&OPT::nExportNumViews)->default_value(0), "export points with >= number of views (0 - disabled, <0 - save MVS project too)") + ("roi-border", boost::program_options::value(&OPT::fBorderROI)->default_value(0), "add a border to the region-of-interest when cropping the scene (0 - disabled, >0 - percentage, <0 - absolute)") + ("estimate-roi", boost::program_options::value(&OPT::nEstimateROI)->default_value(2), "estimate and set region-of-interest (0 - disabled, 1 - enabled, 2 - adaptive)") + ("crop-to-roi", boost::program_options::value(&OPT::bCrop2ROI)->default_value(true), "crop scene using the region-of-interest") + ("remove-dmaps", boost::program_options::value(&bRemoveDmaps)->default_value(false), "remove depth-maps after fusion") + ("tower-mode", boost::program_options::value(&OPT::nTowerMode)->default_value(4), "add a cylinder of points in the center of ROI; scene assume to be Z-up oriented (0 - disabled, 1 - replace, 2 - append, 3 - select neighbors, 4 - select neighbors & append, <0 - force tower mode)") + ; + + // hidden options, allowed both on command line and + // in config file, but will not be shown to the user + boost::program_options::options_description hidden("Hidden options"); + hidden.add_options() + ("mesh-file", boost::program_options::value(&OPT::strMeshFileName), "mesh file name used for image pair overlap estimation") + ("export-roi-file", boost::program_options::value(&OPT::strExportROIFileName), "ROI file name to be exported form the scene") + ("import-roi-file", boost::program_options::value(&OPT::strImportROIFileName), "ROI file name to be imported into the scene") + ("dense-config-file", boost::program_options::value(&OPT::strDenseConfigFileName), "optional configuration file for the densifier (overwritten by the command line options)") + ("export-depth-maps-name", boost::program_options::value(&OPT::strExportDepthMapsName), "render given mesh and save the depth-map for every image to this file name base (empty - disabled)") + ; + + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(config).add(hidden); + + boost::program_options::options_description config_file_options; + config_file_options.add(config).add(hidden); + + boost::program_options::positional_options_description p; + p.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(p).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + INIT_WORKING_FOLDER; + // parse configuration file + std::ifstream ifs(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (ifs) { + boost::program_options::store(parse_config_file(ifs, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // initialize the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-")+Util::getUniqueName(0)+_T(".log")).c_str()); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // validate input + Util::ensureValidPath(OPT::strInputFileName); + if (OPT::vm.count("help") || OPT::strInputFileName.empty()) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(config); + GET_LOG() << visible; + } + if (OPT::strInputFileName.empty()) + return false; + + // initialize optional options + Util::ensureValidPath(OPT::strPointCloudFileName); + Util::ensureValidPath(OPT::strOutputFileName); + Util::ensureValidPath(OPT::strViewNeighborsFileName); + Util::ensureValidPath(OPT::strOutputViewNeighborsFileName); + Util::ensureValidPath(OPT::strMeshFileName); + Util::ensureValidPath(OPT::strExportROIFileName); + Util::ensureValidPath(OPT::strImportROIFileName); + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = Util::getFileFullName(OPT::strInputFileName) + _T("_dense.mvs"); + + // init dense options + if (!OPT::strDenseConfigFileName.empty()) + OPT::strDenseConfigFileName = MAKE_PATH_SAFE(OPT::strDenseConfigFileName); + OPTDENSE::init(); + const bool bValidConfig(OPTDENSE::oConfig.Load(OPT::strDenseConfigFileName)); + OPTDENSE::update(); + OPTDENSE::nResolutionLevel = nResolutionLevel; + OPTDENSE::nMaxResolution = nMaxResolution; + OPTDENSE::nMinResolution = nMinResolution; + OPTDENSE::nSubResolutionLevels = nSubResolutionLevels; + OPTDENSE::nNumViews = nNumViews; + OPTDENSE::nMinViewsFuse = nMinViewsFuse; + OPTDENSE::nEstimationIters = nEstimationIters; + OPTDENSE::nEstimationGeometricIters = nEstimationGeometricIters; + OPTDENSE::nEstimateColors = nEstimateColors; + OPTDENSE::nEstimateNormals = nEstimateNormals; + OPTDENSE::nOptimize = nOptimize; + OPTDENSE::nIgnoreMaskLabel = nIgnoreMaskLabel; + OPTDENSE::bRemoveDmaps = bRemoveDmaps; + if (!bValidConfig && !OPT::strDenseConfigFileName.empty()) + OPTDENSE::oConfig.Save(OPT::strDenseConfigFileName); + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::Finalize() +{ + MVS::Finalize(); + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +} // unnamed namespace + +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application application; + if (!application.Initialize(argc, argv)) + return EXIT_FAILURE; + + Scene scene(OPT::nMaxThreads); + if (OPT::fSampleMesh != 0) { + // sample input mesh and export the obtained point-cloud + if (!scene.Load(MAKE_PATH_SAFE(OPT::strInputFileName), true) || scene.mesh.IsEmpty()) + return EXIT_FAILURE; + TD_TIMER_START(); + PointCloud pointcloud; + if (OPT::fSampleMesh > 0) + scene.mesh.SamplePoints(OPT::fSampleMesh, 0, pointcloud); + else + scene.mesh.SamplePoints(ROUND2INT(-OPT::fSampleMesh), pointcloud); + VERBOSE("Sample mesh completed: %u points (%s)", pointcloud.GetSize(), TD_TIMER_GET_FMT().c_str()); + pointcloud.Save(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName))+_T(".ply")); + return EXIT_SUCCESS; + } + // load and estimate a dense point-cloud + const Scene::SCENE_TYPE sceneType(scene.Load(MAKE_PATH_SAFE(OPT::strInputFileName))); + if (sceneType == Scene::SCENE_NA) + return EXIT_FAILURE; + if (!OPT::strPointCloudFileName.empty() && !scene.pointcloud.Load(MAKE_PATH_SAFE(OPT::strPointCloudFileName))) { + VERBOSE("error: cannot load point-cloud file"); + return EXIT_FAILURE; + } + if (!OPT::strMaskPath.empty()) { + Util::ensureValidFolderPath(OPT::strMaskPath); + for (Image& image : scene.images) { + if (!image.maskName.empty()) { + VERBOSE("error: Image %s has non-empty maskName %s", image.name.c_str(), image.maskName.c_str()); + return EXIT_FAILURE; + } + image.maskName = OPT::strMaskPath + Util::getFileName(image.name) + ".mask.png"; + if (!File::access(image.maskName)) { + VERBOSE("error: Mask image %s not found", image.maskName.c_str()); + return EXIT_FAILURE; + } + } + } + if (!OPT::strImportROIFileName.empty()) { + std::ifstream fs(MAKE_PATH_SAFE(OPT::strImportROIFileName)); + if (!fs) + return EXIT_FAILURE; + fs >> scene.obb; + scene.Save(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName))+_T(".mvs"), (ARCHIVE_TYPE)OPT::nArchiveType); + return EXIT_SUCCESS; + } + if (!scene.IsBounded()) + scene.EstimateROI(OPT::nEstimateROI, 1.1f); + if (!OPT::strExportROIFileName.empty() && scene.IsBounded()) { + std::ofstream fs(MAKE_PATH_SAFE(OPT::strExportROIFileName)); + if (!fs) + return EXIT_FAILURE; + fs << scene.obb; + return EXIT_SUCCESS; + } + if (OPT::nTowerMode!=0) + scene.InitTowerScene(OPT::nTowerMode); + if (!OPT::strMeshFileName.empty()) + scene.mesh.Load(MAKE_PATH_SAFE(OPT::strMeshFileName)); + if (!OPT::strViewNeighborsFileName.empty()) + scene.LoadViewNeighbors(MAKE_PATH_SAFE(OPT::strViewNeighborsFileName)); + if (!OPT::strOutputViewNeighborsFileName.empty()) { + if (!scene.ImagesHaveNeighbors()) { + VERBOSE("error: neighbor views not computed yet"); + return EXIT_FAILURE; + } + scene.SaveViewNeighbors(MAKE_PATH_SAFE(OPT::strOutputViewNeighborsFileName)); + return EXIT_SUCCESS; + } + if (!OPT::strExportDepthMapsName.empty() && !scene.mesh.IsEmpty()) { + // project mesh onto each image and save the resulted depth-maps + TD_TIMER_START(); + if (!scene.ExportMeshToDepthMaps(MAKE_PATH_SAFE(OPT::strExportDepthMapsName))) + return EXIT_FAILURE; + VERBOSE("Mesh projection completed: %u depth-maps (%s)", scene.images.size(), TD_TIMER_GET_FMT().c_str()); + return EXIT_SUCCESS; + } + if (OPT::fMaxSubsceneArea > 0) { + // split the scene in sub-scenes by maximum sampling area + Scene::ImagesChunkArr chunks; + scene.Split(chunks, OPT::fMaxSubsceneArea); + scene.ExportChunks(chunks, GET_PATH_FULL(OPT::strOutputFileName), (ARCHIVE_TYPE)OPT::nArchiveType); + return EXIT_SUCCESS; + } + if (OPT::thFilterPointCloud < 0) { + // filter point-cloud based on camera-point visibility intersections + scene.PointCloudFilter(OPT::thFilterPointCloud); + const String baseFileName(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName))+_T("_filtered")); + scene.Save(baseFileName+_T(".mvs"), (ARCHIVE_TYPE)OPT::nArchiveType); + scene.pointcloud.Save(baseFileName+_T(".ply")); + return EXIT_SUCCESS; + } + if (OPT::nExportNumViews && scene.pointcloud.IsValid()) { + // export point-cloud containing only points with N+ views + const String baseFileName(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName))+ + String::FormatString(_T("_%dviews"), ABS(OPT::nExportNumViews))); + if (OPT::nExportNumViews > 0) { + // export point-cloud containing only points with N+ views + scene.pointcloud.SaveNViews(baseFileName+_T(".ply"), (IIndex)OPT::nExportNumViews); + } else { + // save scene and export point-cloud containing only points with N+ views + scene.pointcloud.RemoveMinViews((IIndex)-OPT::nExportNumViews); + scene.Save(baseFileName+_T(".mvs"), (ARCHIVE_TYPE)OPT::nArchiveType); + scene.pointcloud.Save(baseFileName+_T(".ply")); + } + return EXIT_SUCCESS; + } + if (OPT::fEstimateScale > 0 && !scene.pointcloud.IsEmpty() && !scene.images.empty()) { + // simply export existing point-cloud with scale + if (scene.pointcloud.normals.empty()) { + if (!scene.pointcloud.IsValid()) { + VERBOSE("error: can not estimate normals as the point-cloud is not valid"); + return EXIT_FAILURE; + } + EstimatePointNormals(scene.images, scene.pointcloud); + } + const String baseFileName(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName))); + scene.pointcloud.SaveWithScale(baseFileName+_T("_scale.ply"), scene.images, OPT::fEstimateScale); + return EXIT_SUCCESS; + } + PointCloud sparsePointCloud; + if ((ARCHIVE_TYPE)OPT::nArchiveType != ARCHIVE_MVS || sceneType == Scene::SCENE_INTERFACE) { + #if TD_VERBOSE != TD_VERBOSE_OFF + if (VERBOSITY_LEVEL > 1 && !scene.pointcloud.IsEmpty()) + scene.pointcloud.PrintStatistics(scene.images.data(), &scene.obb); + #endif + if ((ARCHIVE_TYPE)OPT::nArchiveType == ARCHIVE_MVS) + sparsePointCloud = scene.pointcloud; + TD_TIMER_START(); + if (!scene.DenseReconstruction(OPT::nFusionMode, OPT::bCrop2ROI, OPT::fBorderROI)) { + if (ABS(OPT::nFusionMode) != 1) + return EXIT_FAILURE; + VERBOSE("Depth-maps estimated (%s)", TD_TIMER_GET_FMT().c_str()); + return EXIT_SUCCESS; + } + VERBOSE("Densifying point-cloud completed: %u points (%s)", scene.pointcloud.GetSize(), TD_TIMER_GET_FMT().c_str()); + } + + // save the final point-cloud + const String baseFileName(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName))); + scene.pointcloud.Save(baseFileName+_T(".ply"), (ARCHIVE_TYPE)OPT::nArchiveType == ARCHIVE_MVS); + #if TD_VERBOSE != TD_VERBOSE_OFF + if (VERBOSITY_LEVEL > 2) + scene.ExportCamerasMLP(baseFileName+_T(".mlp"), baseFileName+_T(".ply")); + #endif + if ((ARCHIVE_TYPE)OPT::nArchiveType == ARCHIVE_MVS) + scene.pointcloud.Swap(sparsePointCloud); + scene.Save(baseFileName+_T(".mvs"), (ARCHIVE_TYPE)OPT::nArchiveType); + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/InterfaceCOLMAP/CMakeLists.txt b/apps/InterfaceCOLMAP/CMakeLists.txt new file mode 100644 index 0000000..5d48c79 --- /dev/null +++ b/apps/InterfaceCOLMAP/CMakeLists.txt @@ -0,0 +1,13 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(InterfaceCOLMAP "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS InterfaceCOLMAP + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/InterfaceCOLMAP/InterfaceCOLMAP.cpp b/apps/InterfaceCOLMAP/InterfaceCOLMAP.cpp new file mode 100644 index 0000000..2c31092 --- /dev/null +++ b/apps/InterfaceCOLMAP/InterfaceCOLMAP.cpp @@ -0,0 +1,1493 @@ +/* + * InterfaceCOLMAP.cpp + * + * Copyright (c) 2014-2018 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#include +#include "endian.h" + +using namespace MVS; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("InterfaceCOLMAP") +#define MVS_FILE_EXTENSION _T(".mvs") +#define COLMAP_IMAGES_FOLDER _T("images/") +#define COLMAP_SPARSE_FOLDER _T("sparse/") +#define COLMAP_CAMERAS_TXT COLMAP_SPARSE_FOLDER _T("cameras.txt") +#define COLMAP_IMAGES_TXT COLMAP_SPARSE_FOLDER _T("images.txt") +#define COLMAP_POINTS_TXT COLMAP_SPARSE_FOLDER _T("points3D.txt") +#define COLMAP_CAMERAS_BIN COLMAP_SPARSE_FOLDER _T("cameras.bin") +#define COLMAP_IMAGES_BIN COLMAP_SPARSE_FOLDER _T("images.bin") +#define COLMAP_POINTS_BIN COLMAP_SPARSE_FOLDER _T("points3D.bin") +#define COLMAP_DENSE_POINTS _T("fused.ply") +#define COLMAP_DENSE_POINTS_VISIBILITY _T("fused.ply.vis") +#define COLMAP_STEREO_FOLDER _T("stereo/") +#define COLMAP_FUSION COLMAP_STEREO_FOLDER _T("fusion.cfg") +#define COLMAP_PATCHMATCH COLMAP_STEREO_FOLDER _T("patch-match.cfg") +#define COLMAP_STEREO_CONSISTENCYGRAPHS_FOLDER COLMAP_STEREO_FOLDER _T("consistency_graphs/") +#define COLMAP_STEREO_DEPTHMAPS_FOLDER COLMAP_STEREO_FOLDER _T("depth_maps/") +#define COLMAP_STEREO_NORMALMAPS_FOLDER COLMAP_STEREO_FOLDER _T("normal_maps/") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { +bool IsFromOpenMVS; // conversion direction +bool bNormalizeIntrinsics; +bool bForceSparsePointCloud; +String strInputFileName; +String strPointCloudFileName; +String strOutputFileName; +String strImageFolder; +unsigned nArchiveType; +int nProcessPriority; +unsigned nMaxThreads; +String strConfigFileName; +boost::program_options::variables_map vm; +} // namespace OPT + +class Application { +public: + Application() {} + ~Application() { CleanUp(); } + + bool Initialize(size_t argc, LPCTSTR* argv); + void CleanUp(); +}; // Application + +// Initializes the application and parses command-line parameters +bool Application::Initialize(size_t argc, LPCTSTR* argv) +{ + // Open the log file and console for output + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "imports SfM or MVS scene stored in COLMAP undistoreted format OR exports MVS scene to COLMAP format") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: -1-interface, 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description configOptions("Main options"); + configOptions.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input COLMAP folder containing cameras, images and points files OR input MVS project file") + ("pointcloud-file,p", boost::program_options::value(&OPT::strPointCloudFileName), "point-cloud with views file name (overwrite existing point-cloud)") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the MVS project") + ("image-folder", boost::program_options::value(&OPT::strImageFolder)->default_value(COLMAP_IMAGES_FOLDER), "folder to the undistorted images") + ("normalize,f", boost::program_options::value(&OPT::bNormalizeIntrinsics)->default_value(false), "normalize intrinsics while exporting to MVS format") + ("force-points,e", boost::program_options::value(&OPT::bForceSparsePointCloud)->default_value(false), "force exporting point-cloud as sparse points also even if dense point-cloud detected") + ; + + // Combine all command-line options + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(configOptions); + + // Only config file options + boost::program_options::options_description config_file_options; + config_file_options.add(configOptions); + + boost::program_options::positional_options_description positionalOptions; + positionalOptions.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(positionalOptions).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + + // Initialize working folder + INIT_WORKING_FOLDER; + + // Parse configuration file if exists + std::ifstream configFile(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (configFile) { + boost::program_options::store(parse_config_file(configFile, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // Open and configure the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-")+Util::getUniqueName(0)+_T(".log"))); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // Check and validate paths + Util::ensureValidPath(OPT::strInputFileName); + Util::ensureValidPath(OPT::strPointCloudFileName); + + // Handle help option or invalid command + const bool bInvalidCommand(OPT::strInputFileName.empty()); + if (OPT::vm.count("help") || bInvalidCommand) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(configOptions); + GET_LOG() << _T("\n" + "Import/export 3D reconstruction from COLMAP (TXT/BIN format) and to COLMAP (TXT format). \n" + "In order to import a scene, run COLMAP SfM and next undistort the images (only PINHOLE\n" + "camera model supported for the moment)." + "\n") + << visible; + } + + if (bInvalidCommand) + return false; + + // initialize optional options + Util::ensureValidFolderPath(OPT::strImageFolder); + Util::ensureValidPath(OPT::strOutputFileName); + OPT::strImageFolder = MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strImageFolder); + + // Determine if the source file is an OpenMVS project by checking the file extension + const String inputFileExtension(Util::getFileExt(OPT::strInputFileName).ToLower()); + OPT::IsFromOpenMVS = (inputFileExtension == MVS_FILE_EXTENSION); + + // Set default output filename based on input filename if not explicitly specified + if (OPT::IsFromOpenMVS) { + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = Util::getFilePath(OPT::strInputFileName); + } else { + // Ensure the input filename ends with a directory slash if needed + Util::ensureFolderSlash(OPT::strInputFileName); + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = OPT::strInputFileName + _T("scene") MVS_FILE_EXTENSION; + } + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::CleanUp() +{ + MVS::Finalize(); + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +namespace COLMAP { + +using namespace colmap; + +// See colmap/src/util/types.h +typedef uint32_t camera_t; +typedef uint32_t image_t; +typedef uint64_t image_pair_t; +typedef uint32_t point2D_t; +typedef uint64_t point3D_t; + +const std::vector mapCameraModel = { + "SIMPLE_PINHOLE", + "PINHOLE", + "SIMPLE_RADIAL", + "RADIAL", + "OPENCV", + "OPENCV_FISHEYE", + "FULL_OPENCV", + "FOV", + "SIMPLE_RADIAL_FISHEYE", + "RADIAL_FISHEYE", + "THIN_PRISM_FISHEYE" +}; + +// tools +bool NextLine(std::istream& stream, std::istringstream& in, bool bIgnoreEmpty=true) { + String line; + do { + std::getline(stream, line); + Util::strTrim(line, _T(" ")); + if (stream.fail()) + return false; + } while (((bIgnoreEmpty && line.empty()) || line[0u] == '#') && stream.good()); + in.clear(); + in.str(line); + return true; +} +// structure describing a camera +struct Camera { + uint32_t ID; // ID of the camera + String model; // camera model name + uint32_t width, height; // camera resolution + std::vector params; // camera parameters + uint64_t numCameras{0}; // only for binary format + + Camera() {} + Camera(uint32_t _ID) : ID(_ID) {} + bool operator < (const Camera& rhs) const { return ID < rhs.ID; } + + struct CameraHash { + size_t operator()(const Camera& camera) const { + const size_t h1(std::hash()(camera.model)); + const size_t h2(std::hash()(camera.width)); + const size_t h3(std::hash()(camera.height)); + size_t h(h1 ^ ((h2 ^ (h3 << 1)) << 1)); + for (REAL p: camera.params) + h = std::hash()(p) ^ (h << 1); + return h; + } + }; + struct CameraEqualTo { + bool operator()(const Camera& _Left, const Camera& _Right) const { + return _Left.model == _Right.model && + _Left.width == _Right.width && _Left.height == _Right.height && + _Left.params == _Right.params; + } + }; + + bool Read(std::istream& stream, bool binary) { + if (binary) + return ReadBIN(stream); + return ReadTXT(stream); + } + + bool Write(std::ostream& stream, bool binary) { + if (binary) + return WriteBIN(stream); + return WriteTXT(stream); + } + + // Camera list with one line of data per camera: + // CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[] + bool ReadTXT(std::istream& stream) { + std::istringstream in; + if (!NextLine(stream, in)) + return false; + in >> ID >> model >> width >> height; + if (in.fail()) + return false; + --ID; + if (model != _T("PINHOLE")) + return false; + params.resize(4); + in >> params[0] >> params[1] >> params[2] >> params[3]; + return !in.fail(); + } + + // See: colmap/src/base/reconstruction.cc + // void Reconstruction::ReadCamerasBinary(const std::string& path) + bool ReadBIN(std::istream& stream) { + if (stream.peek() == EOF) + return false; + + if (numCameras == 0) { + // Read the first entry in the binary file + numCameras = ReadBinaryLittleEndian(&stream); + } + + ID = ReadBinaryLittleEndian(&stream)-1; + model = mapCameraModel[ReadBinaryLittleEndian(&stream)]; + width = (uint32_t)ReadBinaryLittleEndian(&stream); + height = (uint32_t)ReadBinaryLittleEndian(&stream); + if (model != _T("PINHOLE")) + return false; + params.resize(4); + ReadBinaryLittleEndian(&stream, ¶ms); + return true; + } + + bool WriteTXT(std::ostream& out) const { + out << ID+1 << _T(" ") << model << _T(" ") << width << _T(" ") << height; + if (out.fail()) + return false; + for (REAL param: params) { + out << _T(" ") << param; + if (out.fail()) + return false; + } + out << std::endl; + return true; + } + + bool WriteBIN(std::ostream& stream) { + if (numCameras != 0) { + // Write the first entry in the binary file + WriteBinaryLittleEndian(&stream, numCameras); + numCameras = 0; + } + + WriteBinaryLittleEndian(&stream, ID+1); + const int64 modelId(std::distance(mapCameraModel.begin(), std::find(mapCameraModel.begin(), mapCameraModel.end(), model))); + WriteBinaryLittleEndian(&stream, (int)modelId); + WriteBinaryLittleEndian(&stream, width); + WriteBinaryLittleEndian(&stream, height); + for (REAL param: params) + WriteBinaryLittleEndian(&stream, param); + return !stream.fail(); + } +}; +typedef std::vector Cameras; +// structure describing an image +struct Image { + struct Proj { + Eigen::Vector2f p; + uint32_t idPoint; + }; + uint32_t ID; // ID of the image + Eigen::Quaterniond q; // rotation + Eigen::Vector3d t; // translation + uint32_t idCamera; // ID of the associated camera + String name; // image file name + std::vector projs; // known image projections + uint64_t numRegImages{0}; // only for binary format + + Image() {} + Image(uint32_t _ID) : ID(_ID) {} + bool operator < (const Image& rhs) const { return ID < rhs.ID; } + + bool Read(std::istream& stream, bool binary) { + if (binary) + return ReadBIN(stream); + return ReadTXT(stream); + } + + bool Write(std::ostream& stream, bool binary) { + if (binary) + return WriteBIN(stream); + return WriteTXT(stream); + } + + // Image list with two lines of data per image: + // IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME + // POINTS2D[] as (X, Y, POINT3D_ID) + bool ReadTXT(std::istream& stream) { + std::istringstream in; + if (!NextLine(stream, in)) + return false; + in >> ID + >> q.w() >> q.x() >> q.y() >> q.z() + >> t(0) >> t(1) >> t(2) + >> idCamera >> name; + if (in.fail()) + return false; + --ID; --idCamera; + Util::ensureValidPath(name); + if (!NextLine(stream, in, false)) + return false; + projs.clear(); + while (true) { + Proj proj; + in >> proj.p(0) >> proj.p(1) >> (int&)proj.idPoint; + if (in.fail()) + break; + --proj.idPoint; + projs.emplace_back(proj); + } + return true; + } + + // See: colmap/src/base/reconstruction.cc + // void Reconstruction::ReadImagesBinary(const std::string& path) + bool ReadBIN(std::istream& stream) { + if (stream.peek() == EOF) + return false; + + if (!numRegImages) { + // Read the first entry in the binary file + numRegImages = ReadBinaryLittleEndian(&stream); + } + + ID = ReadBinaryLittleEndian(&stream)-1; + q.w() = ReadBinaryLittleEndian(&stream); + q.x() = ReadBinaryLittleEndian(&stream); + q.y() = ReadBinaryLittleEndian(&stream); + q.z() = ReadBinaryLittleEndian(&stream); + t(0) = ReadBinaryLittleEndian(&stream); + t(1) = ReadBinaryLittleEndian(&stream); + t(2) = ReadBinaryLittleEndian(&stream); + idCamera = ReadBinaryLittleEndian(&stream)-1; + + name = ""; + while (true) { + char nameChar; + stream.read(&nameChar, 1); + if (nameChar == '\0') + break; + name += nameChar; + } + Util::ensureValidPath(name); + + const size_t numPoints2D = ReadBinaryLittleEndian(&stream); + projs.clear(); + for (size_t j = 0; j < numPoints2D; ++j) { + Proj proj; + proj.p(0) = (float)ReadBinaryLittleEndian(&stream); + proj.p(1) = (float)ReadBinaryLittleEndian(&stream); + proj.idPoint = (uint32_t)ReadBinaryLittleEndian(&stream)-1; + projs.emplace_back(proj); + } + return true; + } + + bool WriteTXT(std::ostream& out) const { + out << ID+1 << _T(" ") + << q.w() << _T(" ") << q.x() << _T(" ") << q.y() << _T(" ") << q.z() << _T(" ") + << t(0) << _T(" ") << t(1) << _T(" ") << t(2) << _T(" ") + << idCamera+1 << _T(" ") << name + << std::endl; + for (const Proj& proj: projs) { + out << proj.p(0) << _T(" ") << proj.p(1) << _T(" ") << (int)proj.idPoint+1 << _T(" "); + if (out.fail()) + return false; + } + out << std::endl; + return !out.fail(); + } + + bool WriteBIN(std::ostream& stream) { + if (numRegImages != 0) { + // Write the first entry in the binary file + WriteBinaryLittleEndian(&stream, numRegImages); + numRegImages = 0; + } + + WriteBinaryLittleEndian(&stream, ID+1); + + WriteBinaryLittleEndian(&stream, q.w()); + WriteBinaryLittleEndian(&stream, q.x()); + WriteBinaryLittleEndian(&stream, q.y()); + WriteBinaryLittleEndian(&stream, q.z()); + + WriteBinaryLittleEndian(&stream, t(0)); + WriteBinaryLittleEndian(&stream, t(1)); + WriteBinaryLittleEndian(&stream, t(2)); + + WriteBinaryLittleEndian(&stream, idCamera+1); + + stream.write(name.c_str(), name.size()+1); + + WriteBinaryLittleEndian(&stream, projs.size()); + for (const Proj& proj: projs) { + WriteBinaryLittleEndian(&stream, proj.p(0)); + WriteBinaryLittleEndian(&stream, proj.p(1)); + WriteBinaryLittleEndian(&stream, proj.idPoint+1); + } + return !stream.fail(); + } +}; +typedef std::vector Images; +// structure describing a 3D point +struct Point { + struct Track { + uint32_t idImage; + uint32_t idProj; + }; + uint32_t ID; // ID of the point + Interface::Pos3f p; // position + Interface::Col3 c; // BGR color + float e; // error + std::vector tracks; // point track + uint64_t numPoints3D{0}; // only for binary format + + Point() {} + Point(uint32_t _ID) : ID(_ID) {} + bool operator < (const Image& rhs) const { return ID < rhs.ID; } + + bool Read(std::istream& stream, bool binary) { + if (binary) + return ReadBIN(stream); + return ReadTXT(stream); + } + + bool Write(std::ostream& stream, bool binary) { + if (binary) + return WriteBIN(stream); + return WriteTXT(stream); + } + + // 3D point list with one line of data per point: + // POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX) + bool ReadTXT(std::istream& stream) { + std::istringstream in; + if (!NextLine(stream, in)) + return false; + int r,g,b; + in >> ID + >> p.x >> p.y >> p.z + >> r >> g >> b + >> e; + c.x = CLAMP(b,0,255); + c.y = CLAMP(g,0,255); + c.z = CLAMP(r,0,255); + if (in.fail()) + return false; + --ID; + tracks.clear(); + while (true) { + Track track; + in >> track.idImage >> track.idProj; + if (in.fail()) + break; + --track.idImage; --track.idProj; + tracks.emplace_back(track); + } + return !tracks.empty(); + } + + // See: colmap/src/base/reconstruction.cc + // void Reconstruction::ReadPoints3DBinary(const std::string& path) + bool ReadBIN(std::istream& stream) { + if (stream.peek() == EOF) + return false; + + if (!numPoints3D) { + // Read the first entry in the binary file + numPoints3D = ReadBinaryLittleEndian(&stream); + } + + int r,g,b; + ID = (uint32_t)ReadBinaryLittleEndian(&stream)-1; + p.x = (float)ReadBinaryLittleEndian(&stream); + p.y = (float)ReadBinaryLittleEndian(&stream); + p.z = (float)ReadBinaryLittleEndian(&stream); + r = ReadBinaryLittleEndian(&stream); + g = ReadBinaryLittleEndian(&stream); + b = ReadBinaryLittleEndian(&stream); + e = (float)ReadBinaryLittleEndian(&stream); + c.x = CLAMP(b,0,255); + c.y = CLAMP(g,0,255); + c.z = CLAMP(r,0,255); + + const size_t trackLength = ReadBinaryLittleEndian(&stream); + tracks.clear(); + for (size_t j = 0; j < trackLength; ++j) { + Track track; + track.idImage = ReadBinaryLittleEndian(&stream)-1; + track.idProj = ReadBinaryLittleEndian(&stream)-1; + tracks.emplace_back(track); + } + return !tracks.empty(); + } + + bool WriteTXT(std::ostream& out) const { + ASSERT(!tracks.empty()); + const int r(c.z),g(c.y),b(c.x); + out << ID+1 << _T(" ") + << p.x << _T(" ") << p.y << _T(" ") << p.z << _T(" ") + << r << _T(" ") << g << _T(" ") << b << _T(" ") + << e << _T(" "); + for (const Track& track: tracks) { + out << track.idImage+1 << _T(" ") << track.idProj+1 << _T(" "); + if (out.fail()) + return false; + } + out << std::endl; + return !out.fail(); + } + + bool WriteBIN(std::ostream& stream) { + ASSERT(!tracks.empty()); + if (numPoints3D != 0) { + // Write the first entry in the binary file + WriteBinaryLittleEndian(&stream, numPoints3D); + numPoints3D = 0; + } + + WriteBinaryLittleEndian(&stream, ID+1); + WriteBinaryLittleEndian(&stream, p.x); + WriteBinaryLittleEndian(&stream, p.y); + WriteBinaryLittleEndian(&stream, p.z); + WriteBinaryLittleEndian(&stream, c.z); + WriteBinaryLittleEndian(&stream, c.y); + WriteBinaryLittleEndian(&stream, c.x); + WriteBinaryLittleEndian(&stream, e); + + WriteBinaryLittleEndian(&stream, tracks.size()); + for (const Track& track: tracks) { + WriteBinaryLittleEndian(&stream, track.idImage+1); + WriteBinaryLittleEndian(&stream, track.idProj+1); + } + return !stream.fail(); + } +}; +typedef std::vector Points; +// structure describing an 2D dynamic matrix +template +struct Mat { + size_t width_ = 0; + size_t height_ = 0; + size_t depth_ = 0; + std::vector data_; + + size_t GetNumBytes() const { + return data_.size() * sizeof(T); + } + const T* GetChannelPtr(size_t c) const { + return data_.data()+width_*height_*c; + } + + // See: colmap/src/mvs/mat.h + void Read(const std::string& path) { + std::streampos pos; { + std::fstream text_file(path, std::ios::in | std::ios::binary); + char unused_char; + text_file >> width_ >> unused_char >> height_ >> unused_char >> depth_ >> + unused_char; + pos = text_file.tellg(); + } + data_.resize(width_ * height_ * depth_); + std::fstream binary_file(path, std::ios::in | std::ios::binary); + binary_file.seekg(pos); + ReadBinaryLittleEndian(&binary_file, &data_); + } + void Write(const std::string& path) const { + { + std::fstream text_file(path, std::ios::out); + text_file << width_ << "&" << height_ << "&" << depth_ << "&"; + } + std::fstream binary_file(path, std::ios::out | std::ios::binary | std::ios::app); + WriteBinaryLittleEndian(&binary_file, data_); + } +}; +} // namespace COLMAP + +typedef Eigen::Matrix EMat33d; +typedef Eigen::Matrix EVec3d; + +/** + * Determines whether to use the TXT or BIN version of a file based on availability. + * It tries to open the TXT file first; if unsuccessful, it attempts to open the BIN file. + * + * @param filenameTXT The path to the TXT file. + * @param filenameBIN The path to the BIN file. + * @param fileStream Reference to the ifstream object that will be used for file operations. + * @param chosenFilename Reference to a string that will store the path of the chosen file. + * @param isBinaryFormat Reference to a boolean that indicates whether the chosen file is binary. + * @return true if either file was successfully opened, false otherwise. + */ +bool DetermineInputSource(const String& filenameTXT, const String& filenameBIN, std::ifstream& fileStream, String& filenameCamera, bool& isBinaryFormat) +{ + // Try to open the TXT file first. + fileStream.open(filenameTXT); + if (fileStream.good()) { + filenameCamera = filenameTXT; + isBinaryFormat = false; + return true; + } + fileStream.open(filenameBIN, std::ios::binary); + if (fileStream.good()) { + filenameCamera = filenameBIN; + isBinaryFormat = true; + return true; + } + VERBOSE("error: unable to open file '%s'", filenameTXT.c_str()); + VERBOSE("error: unable to open file '%s'", filenameBIN.c_str()); + return false; +} + + +bool ImportScene(const String& inputFolder, const String& outputFolder, Interface& scene, PointCloud& pointcloud) +{ + // Define a map to store camera IDs and associated platform index. + typedef std::unordered_map CamerasMap; + CamerasMap mapCameras; + { + // Determine the camera file source between TXT and BIN formats. + const String filenameCamerasTXT(inputFolder+COLMAP_CAMERAS_TXT); + const String filenameCamerasBIN(inputFolder+COLMAP_CAMERAS_BIN); + std::ifstream cameraFile; + bool isBinaryFormat; + String filenameCamera; + if (!DetermineInputSource(filenameCamerasTXT, filenameCamerasBIN, cameraFile, filenameCamera, isBinaryFormat)) { + return false; // Exit if file source determination fails. + } + LOG_OUT() << "Reading cameras: " << filenameCamera << std::endl; + + typedef std::unordered_map CamerasSet; + CamerasSet setCameras; + COLMAP::Camera colmapCamera; + while (cameraFile.good() && colmapCamera.Read(cameraFile, isBinaryFormat)) { + const auto setIt(setCameras.emplace(colmapCamera, (uint32_t)scene.platforms.size())); + mapCameras.emplace(colmapCamera.ID, setIt.first->second); + if (!setIt.second) { + // reuse existing platform + continue; + } + // create new platform + Interface::Platform platform; + platform.name = String::FormatString(_T("platform%03u"), colmapCamera.ID); // only one camera per platform supported + Interface::Platform::Camera camera; + camera.name = colmapCamera.model; + camera.K = Interface::Mat33d::eye(); + // account for different pixel center conventions as COLMAP uses pixel center at (0.5,0.5) + camera.K(0,0) = colmapCamera.params[0]; + camera.K(1,1) = colmapCamera.params[1]; + camera.K(0,2) = colmapCamera.params[2]-REAL(0.5); + camera.K(1,2) = colmapCamera.params[3]-REAL(0.5); + camera.R = Interface::Mat33d::eye(); + camera.C = Interface::Pos3d(0,0,0); + if (OPT::bNormalizeIntrinsics) { + // normalize camera intrinsics + camera.K = Camera::ScaleK(camera.K, 1.0/Camera::GetNormalizationScale(colmapCamera.width, colmapCamera.height)); + } else { + camera.width = colmapCamera.width; + camera.height = colmapCamera.height; + } + platform.cameras.emplace_back(camera); + scene.platforms.emplace_back(platform); + } + } + if (mapCameras.empty()) { + VERBOSE("error: no valid cameras (make sure they are in PINHOLE model)"); + return false; + } + + // Function to read and process image data from input files + typedef std::map ImagesMap; + ImagesMap mapImages; + { + // Define a map to associate COLMAP image structures with scene image indices + const String filenameImagesTXT(inputFolder+COLMAP_IMAGES_TXT); + const String filenameImagesBIN(inputFolder+COLMAP_IMAGES_BIN); + std::ifstream file; + bool binary; + String filenameImages; + if (!DetermineInputSource(filenameImagesTXT, filenameImagesBIN, file, filenameImages, binary)) { + return false; + } + LOG_OUT() << "Reading images: " << filenameImages << std::endl; + + // Read image data from the file and store in the scene structure + COLMAP::Image imageColmap; + while (file.good() && imageColmap.Read(file, binary)) { + mapImages.emplace(imageColmap, (uint32_t)scene.images.size()); + Interface::Platform::Pose pose; + Eigen::Map(pose.R.val) = imageColmap.q.toRotationMatrix(); + EnsureRotationMatrix((Matrix3x3d&)pose.R); + Eigen::Map(&pose.C.x) = -(imageColmap.q.inverse() * imageColmap.t); + + Interface::Image image; + image.name = MAKE_PATH_REL(outputFolder,OPT::strImageFolder+imageColmap.name); + image.platformID = mapCameras.at(imageColmap.idCamera); + image.cameraID = 0; + image.ID = imageColmap.ID; + + Interface::Platform& platform = scene.platforms[image.platformID]; + image.poseID = (uint32_t)platform.poses.size(); + platform.poses.emplace_back(pose); + scene.images.emplace_back(image); + } + } + + // read points list + const String filenameDensePoints(inputFolder+COLMAP_DENSE_POINTS); + const String filenameDenseVisPoints(inputFolder+COLMAP_DENSE_POINTS_VISIBILITY); + { + // parse sparse point-cloud + const String filenamePointsTXT(inputFolder+COLMAP_POINTS_TXT); + const String filenamePointsBIN(inputFolder+COLMAP_POINTS_BIN); + std::ifstream file; + bool binary; + String filenamePoints; + if (!DetermineInputSource(filenamePointsTXT, filenamePointsBIN, file, filenamePoints, binary)) { + return false; + } + LOG_OUT() << "Reading points: " << filenamePoints << std::endl; + COLMAP::Point point; + while (file.good() && point.Read(file, binary)) { + Interface::Vertex vertex; + vertex.X = point.p; + for (const COLMAP::Point::Track& track: point.tracks) { + Interface::Vertex::View view; + view.imageID = mapImages.at(COLMAP::Image(track.idImage)); + view.confidence = 0; + vertex.views.emplace_back(view); + } + std::sort(vertex.views.begin(), vertex.views.end(), + [](const Interface::Vertex::View& view0, const Interface::Vertex::View& view1) { return view0.imageID < view1.imageID; }); + scene.vertices.emplace_back(std::move(vertex)); + scene.verticesColor.emplace_back(Interface::Color{point.c}); + } + } + pointcloud.Release(); + if (File::access(filenameDensePoints) && File::access(filenameDenseVisPoints)) { + // parse dense point-cloud + LOG_OUT() << "Reading points: " << filenameDensePoints << " and " << filenameDenseVisPoints << std::endl; + if (!pointcloud.Load(filenameDensePoints)) { + VERBOSE("error: unable to open file '%s'", filenameDensePoints.c_str()); + return false; + } + File file(filenameDenseVisPoints, File::READ, File::OPEN); + if (!file.isOpen()) { + VERBOSE("error: unable to open file '%s'", filenameDenseVisPoints.c_str()); + return false; + } + uint64_t numPoints(0); + file.read(&numPoints, sizeof(uint64_t)); + if (pointcloud.GetSize() != numPoints) { + VERBOSE("error: point-cloud and visibility have different size"); + return false; + } + pointcloud.pointViews.resize(numPoints); + for (size_t i=0; isecond]; + CLISTDEF2(String) neighborNames; + Util::strSplit(neighbors, _T(','), neighborNames); + FOREACH(i, neighborNames) { + String& neighborName = neighborNames[i]; + Util::strTrim(neighborName, _T(" ")); + const ImagesMap::const_iterator it_neighbor = std::find_if(mapImages.begin(), mapImages.end(), + [&neighborName](const ImagesMap::value_type& image) { + return image.first.name == neighborName; + }); + if (it_neighbor == mapImages.end()) { + if (i == 0) + break; + continue; + } + imageNeighbors.emplace_back(scene.images[it_neighbor->second].ID); + } + } + } + LOG_OUT() << "Reading depth-maps/normal-maps: " << pathDepthMaps << " and " << pathNormalMaps << std::endl; + Util::ensureFolder(outputFolder); + const String strType[] = {".geometric.bin", ".photometric.bin"}; + FOREACH(idx, scene.images) { + const Interface::Image& image = scene.images[idx]; + COLMAP::Mat colDepthMap, colNormalMap; + const String filenameImage(Util::getFileNameExt(image.name)); + for (int i=0; i<2; ++i) { + const String filenameDepthMaps(pathDepthMaps+filenameImage+strType[i]); + if (File::isFile(filenameDepthMaps)) { + colDepthMap.Read(filenameDepthMaps); + const String filenameNormalMaps(pathNormalMaps+filenameImage+strType[i]); + if (File::isFile(filenameNormalMaps)) { + colNormalMap.Read(filenameNormalMaps); + } + break; + } + } + if (!colDepthMap.data_.empty()) { + IIndexArr IDs = {image.ID}; + IDs.Join(imagesNeighbors[(IIndex)idx]); + const Interface::Platform& platform = scene.platforms[image.platformID]; + const Interface::Platform::Pose pose(platform.GetPose(image.cameraID, image.poseID)); + const Interface::Mat33d K(platform.GetFullK(image.cameraID, (uint32_t)colDepthMap.width_, (uint32_t)colDepthMap.height_)); + MVS::DepthMap depthMap((int)colDepthMap.height_, (int)colDepthMap.width_); + memcpy(depthMap.getData(), colDepthMap.data_.data(), colDepthMap.GetNumBytes()); + MVS::NormalMap normalMap; + if (!colNormalMap.data_.empty()) { + normalMap.create((int)colNormalMap.height_, (int)colNormalMap.width_); + cv::merge(std::vector{ + cv::Mat((int)colNormalMap.height_, (int)colNormalMap.width_, CV_32F, (void*)colNormalMap.GetChannelPtr(0)), + cv::Mat((int)colNormalMap.height_, (int)colNormalMap.width_, CV_32F, (void*)colNormalMap.GetChannelPtr(1)), + cv::Mat((int)colNormalMap.height_, (int)colNormalMap.width_, CV_32F, (void*)colNormalMap.GetChannelPtr(2)) + }, normalMap); + } + MVS::ConfidenceMap confMap; + MVS::ViewsMap viewsMap; + const auto depthMM(std::minmax_element(colDepthMap.data_.cbegin(), colDepthMap.data_.cend())); + const MVS::Depth dMin(*depthMM.first), dMax(*depthMM.second); + if (!ExportDepthDataRaw(outputFolder+String::FormatString("depth%04u.dmap", image.ID), MAKE_PATH_FULL(outputFolder, image.name), IDs, depthMap.size(), K, pose.R, pose.C, dMin, dMax, depthMap, normalMap, confMap, viewsMap)) + return false; + } + } + } + return true; +} + + +bool ImportPointCloud(const String& strPointCloudFileName, Interface& scene) +{ + PointCloud pointcloud; + if (!pointcloud.Load(strPointCloudFileName)) { + VERBOSE("error: cannot load point-cloud file"); + return false; + } + if (!pointcloud.IsValid()) { + VERBOSE("error: loaded point-cloud does not have visibility information"); + return false; + } + // replace scene point-cloud with the loaded one + scene.vertices.clear(); + scene.verticesColor.clear(); + scene.verticesNormal.clear(); + scene.vertices.reserve(pointcloud.points.size()); + if (!pointcloud.colors.empty()) + scene.verticesColor.reserve(pointcloud.points.size()); + if (!pointcloud.normals.empty()) + scene.verticesNormal.reserve(pointcloud.points.size()); + FOREACH(i, pointcloud.points) { + Interface::Vertex vertex; + vertex.X = pointcloud.points[i]; + vertex.views.reserve(pointcloud.pointViews[i].size()); + FOREACH(j, pointcloud.pointViews[i]) { + Interface::Vertex::View& view = vertex.views.emplace_back(); + view.imageID = pointcloud.pointViews[i][j]; + view.confidence = (pointcloud.pointWeights.empty() ? 0.f : pointcloud.pointWeights[i][j]); + } + scene.vertices.emplace_back(std::move(vertex)); + if (!pointcloud.colors.empty()) { + const Pixel8U& c = pointcloud.colors[i]; + scene.verticesColor.emplace_back(Interface::Color{Interface::Col3{c.b, c.g, c.r}}); + } + if (!pointcloud.normals.empty()) + scene.verticesNormal.emplace_back(Interface::Normal{pointcloud.normals[i]}); + } + return true; +} + +bool ExportScene(const String& strFolder, const Interface& scene, bool bForceSparsePointCloud = false, bool binary = true) +{ + Util::ensureFolder(strFolder+COLMAP_SPARSE_FOLDER); + + // write camera list + CLISTDEF0IDX(KMatrix,uint32_t) Ks; + CLISTDEF0IDX(COLMAP::Camera,uint32_t) cams; + { + // Construct the filename for camera data based on the format preference + const String filenameCameras(strFolder+(binary?COLMAP_CAMERAS_BIN:COLMAP_CAMERAS_TXT)); + LOG_OUT() << "Writing cameras: " << filenameCameras << std::endl; + std::ofstream file(filenameCameras, binary ? std::ios::trunc|std::ios::binary : std::ios::trunc); + + if (!file.good()) { + VERBOSE("error: unable to open file '%s'", filenameCameras.c_str()); + return false; + } + + COLMAP::Camera cam; + if (binary) { + cam.numCameras = 0; + for (const Interface::Platform& platform: scene.platforms) + cam.numCameras += (uint32_t)platform.cameras.size(); + } else { + file << _T("# Camera list with one line of data per camera:") << std::endl; + file << _T("# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]") << std::endl; + } + cam.model = _T("PINHOLE"); + cam.params.resize(4); + for (uint32_t ID=0; ID<(uint32_t)scene.platforms.size(); ++ID) { + const Interface::Platform& platform = scene.platforms[ID]; + ASSERT(platform.cameras.size() == 1); // only one camera per platform supported + const Interface::Platform::Camera& camera = platform.cameras[0]; + cam.ID = ID; + KMatrix K; + if (camera.width == 0 || camera.height == 0) { + // find one image using this camera + const Interface::Image* pImage(NULL); + for (uint32_t i=0; i<(uint32_t)scene.images.size(); ++i) { + const Interface::Image& image = scene.images[i]; + if (image.platformID == ID && image.cameraID == 0 && image.poseID != NO_ID) { + pImage = ℑ + break; + } + } + if (pImage == NULL) { + LOG("error: no image using camera %u of platform %u", 0, ID); + continue; + } + IMAGEPTR ptrImage(Image::ReadImageHeader(MAKE_PATH_SAFE(pImage->name))); + if (ptrImage == NULL) + return false; + cam.width = ptrImage->GetWidth(); + cam.height = ptrImage->GetHeight(); + // unnormalize camera intrinsics + K = platform.GetFullK(0, cam.width, cam.height); + } else { + cam.width = camera.width; + cam.height = camera.height; + K = camera.K; + } + // account for different pixel center conventions as COLMAP uses pixel center at (0.5,0.5) + cam.params[0] = K(0,0); + cam.params[1] = K(1,1); + cam.params[2] = K(0,2)+REAL(0.5); + cam.params[3] = K(1,2)+REAL(0.5); + if (!cam.Write(file, binary)) + return false; + Ks.emplace_back(K); + cams.emplace_back(cam); + } + } + + // create images list + COLMAP::Images images; + CameraArr cameras; + float maxNumPointsSparse(0); + const float avgViewsPerPoint(3.f); + const uint32_t avgResolutionSmallView(640*480), avgResolutionLargeView(6000*4000); + const uint32_t avgPointsPerSmallView(3000), avgPointsPerLargeView(12000); + { + images.resize(scene.images.size()); + cameras.resize((unsigned)scene.images.size()); + for (uint32_t ID=0; ID<(uint32_t)scene.images.size(); ++ID) { + const Interface::Image& image = scene.images[ID]; + if (image.poseID == NO_ID) + continue; + const Interface::Platform& platform = scene.platforms[image.platformID]; + const Interface::Platform::Pose& pose = platform.poses[image.poseID]; + ASSERT(image.cameraID == 0); + COLMAP::Image& img = images[ID]; + img.ID = ID; + img.q = Eigen::Quaterniond(Eigen::Map(pose.R.val)); + img.t = -(img.q * Eigen::Map(&pose.C.x)); + img.idCamera = image.platformID; + img.name = MAKE_PATH_REL(OPT::strImageFolder, MAKE_PATH_FULL(WORKING_FOLDER_FULL, image.name)); + Camera& camera = cameras[ID]; + camera.K = Ks[image.platformID]; + camera.R = pose.R; + camera.C = pose.C; + camera.ComposeP(); + const COLMAP::Camera& cam = cams[image.platformID]; + const uint32_t resolutionView(cam.width*cam.height); + const float linearFactor(float(avgResolutionLargeView-resolutionView)/(avgResolutionLargeView-avgResolutionSmallView)); + maxNumPointsSparse += (avgPointsPerSmallView+(avgPointsPerLargeView-avgPointsPerSmallView)*linearFactor)/avgViewsPerPoint; + } + } + + // auto-select dense or sparse mode based on number of points + const bool bSparsePointCloud(scene.vertices.size() < (size_t)maxNumPointsSparse); + if (bSparsePointCloud || bForceSparsePointCloud) { + // write points list + { + const String filenamePoints(strFolder+(binary?COLMAP_POINTS_BIN:COLMAP_POINTS_TXT)); + LOG_OUT() << "Writing points: " << filenamePoints << std::endl; + std::ofstream file(filenamePoints, binary ? std::ios::trunc|std::ios::binary : std::ios::trunc); + if (!file.good()) { + VERBOSE("error: unable to open file '%s'", filenamePoints.c_str()); + return false; + } + uint64_t numPoints3D = 0; + if (binary) { + numPoints3D = scene.vertices.size(); + } else { + file << _T("# 3D point list with one line of data per point:") << std::endl; + file << _T("# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)") << std::endl; + } + for (uint32_t ID=0; ID<(uint32_t)scene.vertices.size(); ++ID) { + const Interface::Vertex& vertex = scene.vertices[ID]; + COLMAP::Point point; + point.ID = ID; + point.p = vertex.X; + for (const Interface::Vertex::View& view: vertex.views) { + COLMAP::Image& img = images[view.imageID]; + point.tracks.emplace_back(COLMAP::Point::Track{view.imageID, (uint32_t)img.projs.size()}); + COLMAP::Image::Proj proj; + proj.idPoint = ID; + const Point3 X(vertex.X); + ProjectVertex_3x4_3_2(cameras[view.imageID].P.val, X.ptr(), proj.p.data()); + // account for different pixel center conventions as COLMAP uses pixel center at (0.5,0.5) + proj.p[0] += REAL(0.5); + proj.p[1] += REAL(0.5); + img.projs.emplace_back(proj); + } + point.c = scene.verticesColor.empty() ? Interface::Col3(255,255,255) : scene.verticesColor[ID].c; + point.e = 0; + if (numPoints3D != 0) { + point.numPoints3D = numPoints3D; + numPoints3D = 0; + } + if (!point.Write(file, binary)) + return false; + } + } + + Util::ensureFolder(strFolder+COLMAP_STEREO_FOLDER); + + // write fusion list + { + const String filenameFusion(strFolder+COLMAP_FUSION); + LOG_OUT() << "Writing fusion configuration: " << filenameFusion << std::endl; + std::ofstream file(filenameFusion); + if (!file.good()) { + VERBOSE("error: unable to open file '%s'", filenameFusion.c_str()); + return false; + } + for (const COLMAP::Image& img: images) { + if (img.projs.empty()) + continue; + file << img.name << std::endl; + if (file.fail()) + return false; + } + } + + // write patch-match list + { + const String filenameFusion(strFolder+COLMAP_PATCHMATCH); + LOG_OUT() << "Writing patch-match configuration: " << filenameFusion << std::endl; + std::ofstream file(filenameFusion); + if (!file.good()) { + VERBOSE("error: unable to open file '%s'", filenameFusion.c_str()); + return false; + } + for (const COLMAP::Image& img: images) { + if (img.projs.empty()) + continue; + file << img.name << std::endl; + if (file.fail()) + return false; + file << _T("__auto__, 20") << std::endl; + if (file.fail()) + return false; + } + } + + Util::ensureFolder(strFolder+COLMAP_STEREO_CONSISTENCYGRAPHS_FOLDER); + Util::ensureFolder(strFolder+COLMAP_STEREO_DEPTHMAPS_FOLDER); + Util::ensureFolder(strFolder+COLMAP_STEREO_NORMALMAPS_FOLDER); + } + if (!bSparsePointCloud) { + // export dense point-cloud + const String filenameDensePoints(strFolder+COLMAP_DENSE_POINTS); + const String filenameDenseVisPoints(strFolder+COLMAP_DENSE_POINTS_VISIBILITY); + LOG_OUT() << "Writing points: " << filenameDensePoints << " and " << filenameDenseVisPoints << std::endl; + File file(filenameDenseVisPoints, File::WRITE, File::CREATE | File::TRUNCATE); + if (!file.isOpen()) { + VERBOSE("error: unable to write file '%s'", filenameDenseVisPoints.c_str()); + return false; + } + const uint64_t numPoints(scene.vertices.size()); + file.write(&numPoints, sizeof(uint64_t)); + PointCloud pointcloud; + for (size_t i=0; iGetWidth(), pImage->GetHeight()); + Eigen::Matrix4d K4(Eigen::Matrix4d::Identity()); + K4.topLeftCorner<3,3>() = Eigen::Map(K.val); + Util::ensureFolder(fileName); + std::ofstream out(fileName); + if (!out.good()) { + VERBOSE("error: unable to open file '%s'", fileName.c_str()); + return false; + } + out << std::setprecision(12); + out << K4(0,0) << _T(" ") << K4(0,1) << _T(" ") << K4(0,2) << _T(" ") << K4(0,3) << _T("\n"); + out << K4(1,0) << _T(" ") << K4(1,1) << _T(" ") << K4(1,2) << _T(" ") << K4(1,3) << _T("\n"); + out << K4(2,0) << _T(" ") << K4(2,1) << _T(" ") << K4(2,2) << _T(" ") << K4(2,3) << _T("\n"); + out << K4(3,0) << _T(" ") << K4(3,1) << _T(" ") << K4(3,2) << _T(" ") << K4(3,3) << _T("\n"); + return !out.fail(); +} + + +// export image poses in log format +// see: http://redwood-data.org/indoor/fileformat.html +// to support: https://www.tanksandtemples.org/tutorial +bool ExportImagesLog(const String& fileName, const Interface& scene) +{ + LOG_OUT() << "Writing poses: " << fileName << std::endl; + Util::ensureFolder(fileName); + std::ofstream out(fileName); + if (!out.good()) { + VERBOSE("error: unable to open file '%s'", fileName.c_str()); + return false; + } + out << std::setprecision(12); + IIndexArr orderedImages((uint32_t)scene.images.size()); + std::iota(orderedImages.begin(), orderedImages.end(), 0u); + orderedImages.Sort([&scene](IIndex i, IIndex j) { + return scene.images[i].ID < scene.images[j].ID; + }); + for (IIndex ID: orderedImages) { + const Interface::Image& image = scene.images[ID]; + Eigen::Matrix3d R(Eigen::Matrix3d::Identity()); + Eigen::Vector3d t(Eigen::Vector3d::Zero()); + if (image.poseID != NO_ID) { + const Interface::Platform& platform = scene.platforms[image.platformID]; + const Interface::Platform::Pose& pose = platform.poses[image.poseID]; + R = Eigen::Map(pose.R.val).transpose(); + t = Eigen::Map(&pose.C.x); + } + Eigen::Matrix4d T(Eigen::Matrix4d::Identity()); + T.topLeftCorner<3,3>() = R; + T.topRightCorner<3,1>() = t; + out << ID << _T(" ") << ID << _T(" ") << 0 << _T("\n"); + out << T(0,0) << _T(" ") << T(0,1) << _T(" ") << T(0,2) << _T(" ") << T(0,3) << _T("\n"); + out << T(1,0) << _T(" ") << T(1,1) << _T(" ") << T(1,2) << _T(" ") << T(1,3) << _T("\n"); + out << T(2,0) << _T(" ") << T(2,1) << _T(" ") << T(2,2) << _T(" ") << T(2,3) << _T("\n"); + out << T(3,0) << _T(" ") << T(3,1) << _T(" ") << T(3,2) << _T(" ") << T(3,3) << _T("\n"); + } + return !out.fail(); +} + + +// export poses in Strecha camera format: +// Strecha model is P = K[R^T|-R^T t] +// our model is P = K[R|t], t = -RC +bool ExportImagesCamera(const String& pathName, const Interface& scene) +{ + LOG_OUT() << "Writing poses: " << pathName << std::endl; + Util::ensureFolder(pathName); + for (uint32_t ID=0; ID<(uint32_t)scene.images.size(); ++ID) { + const Interface::Image& image = scene.images[ID]; + String imageFileName(image.name); + Util::ensureValidPath(imageFileName); + const String fileName(pathName+Util::getFileNameExt(imageFileName)+".camera"); + std::ofstream out(fileName); + if (!out.good()) { + VERBOSE("error: unable to open file '%s'", fileName.c_str()); + return false; + } + out << std::setprecision(12); + KMatrix K(KMatrix::IDENTITY); + RMatrix R(RMatrix::IDENTITY); + CMatrix t(CMatrix::ZERO); + unsigned width(0), height(0); + if (image.platformID != NO_ID && image.cameraID != NO_ID) { + const Interface::Platform& platform = scene.platforms[image.platformID]; + const Interface::Platform::Camera& camera = platform.cameras[image.cameraID]; + if (camera.HasResolution()) { + width = camera.width; + height = camera.height; + K = camera.K; + } else { + IMAGEPTR pImage = Image::ReadImageHeader(image.name); + width = pImage->GetWidth(); + height = pImage->GetHeight(); + K = platform.GetFullK(image.cameraID, width, height); + } + if (image.poseID != NO_ID) { + const Interface::Platform::Pose& pose = platform.poses[image.poseID]; + R = pose.R.t(); + t = pose.C; + } + } + out << K(0,0) << _T(" ") << K(0,1) << _T(" ") << K(0,2) << _T("\n"); + out << K(1,0) << _T(" ") << K(1,1) << _T(" ") << K(1,2) << _T("\n"); + out << K(2,0) << _T(" ") << K(2,1) << _T(" ") << K(2,2) << _T("\n"); + out << _T("0 0 0") << _T("\n"); + out << R(0,0) << _T(" ") << R(0,1) << _T(" ") << R(0,2) << _T("\n"); + out << R(1,0) << _T(" ") << R(1,1) << _T(" ") << R(1,2) << _T("\n"); + out << R(2,0) << _T(" ") << R(2,1) << _T(" ") << R(2,2) << _T("\n"); + out << t.x << _T(" ") << t.y << _T(" ") << t.z << _T("\n"); + out << width << _T(" ") << height << _T("\n"); + if (out.fail()) { + VERBOSE("error: unable to write file '%s'", fileName.c_str()); + return false; + } + } + return true; +} + +} // unnamed namespace + +// Main function with command-line arguments +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application app; // Instance of the application + if (!app.Initialize(argc, argv)) // Initialize app with command line arguments + return EXIT_FAILURE; // Exit if initialization fails + + TD_TIMER_START(); + + if (OPT::IsFromOpenMVS) { + // read MVS input data + Interface scene; // Scene data structure for MVS input + if (!ARCHIVE::SerializeLoad(scene, MAKE_PATH_SAFE(OPT::strInputFileName))) + return EXIT_FAILURE; // Exit if loading fails + + // Check output file type and export accordingly + if (Util::getFileExt(OPT::strOutputFileName) == _T(".log")) { + // write poses in log format + ExportIntrinsicsTxt(MAKE_PATH_FULL(WORKING_FOLDER_FULL, String("intrinsics.txt")), scene); + ExportImagesLog(MAKE_PATH_SAFE(OPT::strOutputFileName), scene); + } else + if (Util::getFileExt(OPT::strOutputFileName) == _T(".camera")) { + // write poses in Strecha camera format + ExportImagesCamera((OPT::strOutputFileName=Util::getFileFullName(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strOutputFileName)))+PATH_SEPARATOR, scene); + } else { + // write COLMAP input data + if (!OPT::strPointCloudFileName.empty() && !ImportPointCloud(MAKE_PATH_SAFE(OPT::strPointCloudFileName), scene)) + return EXIT_FAILURE; + Util::ensureFolderSlash(OPT::strOutputFileName); + ExportScene(MAKE_PATH_SAFE(OPT::strOutputFileName), scene, OPT::bForceSparsePointCloud); + } + VERBOSE("Input data exported: %u images & %u vertices (%s)", scene.images.size(), scene.vertices.size(), TD_TIMER_GET_FMT().c_str()); + } else { + // read COLMAP input data + Interface scene; + const String strOutFolder(Util::getFilePath(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strOutputFileName))); + PointCloud pointcloud; + if (!ImportScene(MAKE_PATH_SAFE(OPT::strInputFileName), strOutFolder, scene, pointcloud)) + return EXIT_FAILURE; + // write MVS input data + Util::ensureFolder(strOutFolder); + if (!ARCHIVE::SerializeSave(scene, MAKE_PATH_SAFE(OPT::strOutputFileName))) + return EXIT_FAILURE; + if (!pointcloud.IsEmpty() && !pointcloud.Save(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName)) + _T(".ply"), true)) + return EXIT_FAILURE; + VERBOSE("Exported data: %u images, %u points%s (%s)", + scene.images.size(), scene.vertices.size(), pointcloud.IsEmpty()?"":String::FormatString(", %d dense points", pointcloud.GetSize()).c_str(), + TD_TIMER_GET_FMT().c_str()); + } + + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/InterfaceCOLMAP/endian.h b/apps/InterfaceCOLMAP/endian.h new file mode 100644 index 0000000..12a22ca --- /dev/null +++ b/apps/InterfaceCOLMAP/endian.h @@ -0,0 +1,167 @@ +// Copyright (c) 2018, ETH Zurich and UNC Chapel Hill. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de) + +#ifndef COLMAP_SRC_UTIL_ENDIAN_H_ +#define COLMAP_SRC_UTIL_ENDIAN_H_ + +#include +#include +#include + +namespace colmap { + +// Reverse the order of each byte. +template +T ReverseBytes(const T& data); + +// Check the order in which bytes are stored in computer memory. +bool IsLittleEndian(); +bool IsBigEndian(); + +// Convert data between endianness and the native format. Note that, for float +// and double types, these functions are only valid if the format is IEEE-754. +// This is the case for pretty much most processors. +template +T LittleEndianToNative(const T x); +template +T BigEndianToNative(const T x); +template +T NativeToLittleEndian(const T x); +template +T NativeToBigEndian(const T x); + +// Read data in little endian format for cross-platform support. +template +T ReadBinaryLittleEndian(std::istream* stream); +template +void ReadBinaryLittleEndian(std::istream* stream, std::vector* data); + +// Write data in little endian format for cross-platform support. +template +void WriteBinaryLittleEndian(std::ostream* stream, const T& data); +template +void WriteBinaryLittleEndian(std::ostream* stream, const std::vector& data); + +//////////////////////////////////////////////////////////////////////////////// +// Implementation +//////////////////////////////////////////////////////////////////////////////// + +template +T ReverseBytes(const T& data) { + T data_reversed = data; + std::reverse(reinterpret_cast(&data_reversed), + reinterpret_cast(&data_reversed) + sizeof(T)); + return data_reversed; +} + +inline bool IsLittleEndian() { +#ifdef BOOST_BIG_ENDIAN + return false; +#else + return true; +#endif +} + +inline bool IsBigEndian() { +#ifdef BOOST_BIG_ENDIAN + return true; +#else + return false; +#endif +} + +template +T LittleEndianToNative(const T x) { + if (IsLittleEndian()) { + return x; + } else { + return ReverseBytes(x); + } +} + +template +T BigEndianToNative(const T x) { + if (IsBigEndian()) { + return x; + } else { + return ReverseBytes(x); + } +} + +template +T NativeToLittleEndian(const T x) { + if (IsLittleEndian()) { + return x; + } else { + return ReverseBytes(x); + } +} + +template +T NativeToBigEndian(const T x) { + if (IsBigEndian()) { + return x; + } else { + return ReverseBytes(x); + } +} + +template +T ReadBinaryLittleEndian(std::istream* stream) { + T data_little_endian; + stream->read(reinterpret_cast(&data_little_endian), sizeof(T)); + return LittleEndianToNative(data_little_endian); +} + +template +void ReadBinaryLittleEndian(std::istream* stream, std::vector* data) { + for (size_t i = 0; i < data->size(); ++i) { + (*data)[i] = ReadBinaryLittleEndian(stream); + } +} + +template +void WriteBinaryLittleEndian(std::ostream* stream, const T& data) { + const T data_little_endian = NativeToLittleEndian(data); + stream->write(reinterpret_cast(&data_little_endian), sizeof(T)); +} + +template +void WriteBinaryLittleEndian(std::ostream* stream, const std::vector& data) { + for (const auto& elem : data) { + WriteBinaryLittleEndian(stream, elem); + } +} + +} // namespace colmap + +#endif // COLMAP_SRC_UTIL_ENDIAN_H_ + diff --git a/apps/InterfaceCOLMAPTest/CMakeLists.txt b/apps/InterfaceCOLMAPTest/CMakeLists.txt new file mode 100644 index 0000000..5d48c79 --- /dev/null +++ b/apps/InterfaceCOLMAPTest/CMakeLists.txt @@ -0,0 +1,13 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(InterfaceCOLMAP "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS InterfaceCOLMAP + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/InterfaceCOLMAPTest/InterfaceCOLMAPTest.cpp b/apps/InterfaceCOLMAPTest/InterfaceCOLMAPTest.cpp new file mode 100644 index 0000000..0c333b3 --- /dev/null +++ b/apps/InterfaceCOLMAPTest/InterfaceCOLMAPTest.cpp @@ -0,0 +1,1493 @@ +/* + * InterfaceCOLMAP.cpp + * + * Copyright (c) 2014-2018 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#include +#include "endian.h" + +using namespace MVS; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("InterfaceCOLMAP") +#define MVS_EXT _T(".mvs") +#define COLMAP_IMAGES_FOLDER _T("images/") +#define COLMAP_SPARSE_FOLDER _T("sparse/") +#define COLMAP_CAMERAS_TXT COLMAP_SPARSE_FOLDER _T("cameras.txt") +#define COLMAP_IMAGES_TXT COLMAP_SPARSE_FOLDER _T("images.txt") +#define COLMAP_POINTS_TXT COLMAP_SPARSE_FOLDER _T("points3D.txt") +#define COLMAP_CAMERAS_BIN COLMAP_SPARSE_FOLDER _T("cameras.bin") +#define COLMAP_IMAGES_BIN COLMAP_SPARSE_FOLDER _T("images.bin") +#define COLMAP_POINTS_BIN COLMAP_SPARSE_FOLDER _T("points3D.bin") +#define COLMAP_DENSE_POINTS _T("fused.ply") +#define COLMAP_DENSE_POINTS_VISIBILITY _T("fused.ply.vis") +#define COLMAP_STEREO_FOLDER _T("stereo/") +#define COLMAP_FUSION COLMAP_STEREO_FOLDER _T("fusion.cfg") +#define COLMAP_PATCHMATCH COLMAP_STEREO_FOLDER _T("patch-match.cfg") +#define COLMAP_STEREO_CONSISTENCYGRAPHS_FOLDER COLMAP_STEREO_FOLDER _T("consistency_graphs/") +#define COLMAP_STEREO_DEPTHMAPS_FOLDER COLMAP_STEREO_FOLDER _T("depth_maps/") +#define COLMAP_STEREO_NORMALMAPS_FOLDER COLMAP_STEREO_FOLDER _T("normal_maps/") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { +bool IsFromOpenMVS; // conversion direction +bool bNormalizeIntrinsics; +bool bForceSparsePointCloud; +String strInputFileName; +String strPointCloudFileName; +String strOutputFileName; +String strImageFolder; +unsigned nArchiveType; +int nProcessPriority; +unsigned nMaxThreads; +String strConfigFileName; +boost::program_options::variables_map vm; +} // namespace OPT + +class Application { +public: + Application() {} + ~Application() { CleanUp(); } + + bool Initialize(size_t argc, LPCTSTR* argv); + void CleanUp(); +}; // Application + +// Initializes the application and parses command-line parameters +bool Application::Initialize(size_t argc, LPCTSTR* argv) +{ + // Open the log file and console for output + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "imports SfM or MVS scene stored in COLMAP undistoreted format OR exports MVS scene to COLMAP format") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: -1-interface, 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description configOptions("Main options"); + configOptions.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input COLMAP folder containing cameras, images and points files OR input MVS project file") + ("pointcloud-file,p", boost::program_options::value(&OPT::strPointCloudFileName), "point-cloud with views file name (overwrite existing point-cloud)") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the MVS project") + ("image-folder", boost::program_options::value(&OPT::strImageFolder)->default_value(COLMAP_IMAGES_FOLDER), "folder to the undistorted images") + ("normalize,f", boost::program_options::value(&OPT::bNormalizeIntrinsics)->default_value(false), "normalize intrinsics while exporting to MVS format") + ("force-points,e", boost::program_options::value(&OPT::bForceSparsePointCloud)->default_value(false), "force exporting point-cloud as sparse points also even if dense point-cloud detected") + ; + + // Combine all command-line options + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(configOptions); + + // Only config file options + boost::program_options::options_description config_file_options; + config_file_options.add(configOptions); + + boost::program_options::positional_options_description positionalOptions; + positionalOptions.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(positionalOptions).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + + // Initialize working folder + INIT_WORKING_FOLDER; + + // Parse configuration file if exists + std::ifstream configFile(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (configFile) { + boost::program_options::store(parse_config_file(configFile, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // Open and configure the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-")+Util::getUniqueName(0)+_T(".log"))); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // Check and validate paths + Util::ensureValidPath(OPT::strInputFileName); + Util::ensureValidPath(OPT::strPointCloudFileName); + + // Handle help option or invalid command + const bool bInvalidCommand(OPT::strInputFileName.empty()); + if (OPT::vm.count("help") || bInvalidCommand) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(configOptions); + GET_LOG() << _T("\n" + "Import/export 3D reconstruction from COLMAP (TXT/BIN format) and to COLMAP (TXT format). \n" + "In order to import a scene, run COLMAP SfM and next undistort the images (only PINHOLE\n" + "camera model supported for the moment)." + "\n") + << visible; + } + + if (bInvalidCommand) + return false; + + // initialize optional options + Util::ensureValidFolderPath(OPT::strImageFolder); + Util::ensureValidPath(OPT::strOutputFileName); + OPT::strImageFolder = MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strImageFolder); + + // Determine if the source file is an OpenMVS project by checking the file extension + const String inputFileExtension(Util::getFileExt(OPT::strInputFileName).ToLower()); + OPT::IsFromOpenMVS = (inputFileExtension == MVS_FILE_EXTENSION); + + // Set default output filename based on input filename if not explicitly specified + if (OPT::IsFromOpenMVS) { + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = Util::getFilePath(OPT::strInputFileName); + } else { + // Ensure the input filename ends with a directory slash if needed + Util::ensureFolderSlash(OPT::strInputFileName); + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = OPT::strInputFileName + _T("scene") MVS_FILE_EXTENSION; + } + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::CleanUp() +{ + MVS::CleanUp(); + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +namespace COLMAP { + +using namespace colmap; + +// See colmap/src/util/types.h +typedef uint32_t camera_t; +typedef uint32_t image_t; +typedef uint64_t image_pair_t; +typedef uint32_t point2D_t; +typedef uint64_t point3D_t; + +const std::vector mapCameraModel = { + "SIMPLE_PINHOLE", + "PINHOLE", + "SIMPLE_RADIAL", + "RADIAL", + "OPENCV", + "OPENCV_FISHEYE", + "FULL_OPENCV", + "FOV", + "SIMPLE_RADIAL_FISHEYE", + "RADIAL_FISHEYE", + "THIN_PRISM_FISHEYE" +}; + +// tools +bool NextLine(std::istream& stream, std::istringstream& in, bool bIgnoreEmpty=true) { + String line; + do { + std::getline(stream, line); + Util::strTrim(line, _T(" ")); + if (stream.fail()) + return false; + } while (((bIgnoreEmpty && line.empty()) || line[0u] == '#') && stream.good()); + in.clear(); + in.str(line); + return true; +} +// structure describing a camera +struct Camera { + uint32_t ID; // ID of the camera + String model; // camera model name + uint32_t width, height; // camera resolution + std::vector params; // camera parameters + uint64_t numCameras{0}; // only for binary format + + Camera() {} + Camera(uint32_t _ID) : ID(_ID) {} + bool operator < (const Camera& rhs) const { return ID < rhs.ID; } + + struct CameraHash { + size_t operator()(const Camera& camera) const { + const size_t h1(std::hash()(camera.model)); + const size_t h2(std::hash()(camera.width)); + const size_t h3(std::hash()(camera.height)); + size_t h(h1 ^ ((h2 ^ (h3 << 1)) << 1)); + for (REAL p: camera.params) + h = std::hash()(p) ^ (h << 1); + return h; + } + }; + struct CameraEqualTo { + bool operator()(const Camera& _Left, const Camera& _Right) const { + return _Left.model == _Right.model && + _Left.width == _Right.width && _Left.height == _Right.height && + _Left.params == _Right.params; + } + }; + + bool Read(std::istream& stream, bool binary) { + if (binary) + return ReadBIN(stream); + return ReadTXT(stream); + } + + bool Write(std::ostream& stream, bool binary) { + if (binary) + return WriteBIN(stream); + return WriteTXT(stream); + } + + // Camera list with one line of data per camera: + // CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[] + bool ReadTXT(std::istream& stream) { + std::istringstream in; + if (!NextLine(stream, in)) + return false; + in >> ID >> model >> width >> height; + if (in.fail()) + return false; + --ID; + if (model != _T("PINHOLE")) + return false; + params.resize(4); + in >> params[0] >> params[1] >> params[2] >> params[3]; + return !in.fail(); + } + + // See: colmap/src/base/reconstruction.cc + // void Reconstruction::ReadCamerasBinary(const std::string& path) + bool ReadBIN(std::istream& stream) { + if (stream.peek() == EOF) + return false; + + if (numCameras == 0) { + // Read the first entry in the binary file + numCameras = ReadBinaryLittleEndian(&stream); + } + + ID = ReadBinaryLittleEndian(&stream)-1; + model = mapCameraModel[ReadBinaryLittleEndian(&stream)]; + width = (uint32_t)ReadBinaryLittleEndian(&stream); + height = (uint32_t)ReadBinaryLittleEndian(&stream); + if (model != _T("PINHOLE")) + return false; + params.resize(4); + ReadBinaryLittleEndian(&stream, ¶ms); + return true; + } + + bool WriteTXT(std::ostream& out) const { + out << ID+1 << _T(" ") << model << _T(" ") << width << _T(" ") << height; + if (out.fail()) + return false; + for (REAL param: params) { + out << _T(" ") << param; + if (out.fail()) + return false; + } + out << std::endl; + return true; + } + + bool WriteBIN(std::ostream& stream) { + if (numCameras != 0) { + // Write the first entry in the binary file + WriteBinaryLittleEndian(&stream, numCameras); + numCameras = 0; + } + + WriteBinaryLittleEndian(&stream, ID+1); + const int64 modelId(std::distance(mapCameraModel.begin(), std::find(mapCameraModel.begin(), mapCameraModel.end(), model))); + WriteBinaryLittleEndian(&stream, (int)modelId); + WriteBinaryLittleEndian(&stream, width); + WriteBinaryLittleEndian(&stream, height); + for (REAL param: params) + WriteBinaryLittleEndian(&stream, param); + return !stream.fail(); + } +}; +typedef std::vector Cameras; +// structure describing an image +struct Image { + struct Proj { + Eigen::Vector2f p; + uint32_t idPoint; + }; + uint32_t ID; // ID of the image + Eigen::Quaterniond q; // rotation + Eigen::Vector3d t; // translation + uint32_t idCamera; // ID of the associated camera + String name; // image file name + std::vector projs; // known image projections + uint64_t numRegImages{0}; // only for binary format + + Image() {} + Image(uint32_t _ID) : ID(_ID) {} + bool operator < (const Image& rhs) const { return ID < rhs.ID; } + + bool Read(std::istream& stream, bool binary) { + if (binary) + return ReadBIN(stream); + return ReadTXT(stream); + } + + bool Write(std::ostream& stream, bool binary) { + if (binary) + return WriteBIN(stream); + return WriteTXT(stream); + } + + // Image list with two lines of data per image: + // IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME + // POINTS2D[] as (X, Y, POINT3D_ID) + bool ReadTXT(std::istream& stream) { + std::istringstream in; + if (!NextLine(stream, in)) + return false; + in >> ID + >> q.w() >> q.x() >> q.y() >> q.z() + >> t(0) >> t(1) >> t(2) + >> idCamera >> name; + if (in.fail()) + return false; + --ID; --idCamera; + Util::ensureValidPath(name); + if (!NextLine(stream, in, false)) + return false; + projs.clear(); + while (true) { + Proj proj; + in >> proj.p(0) >> proj.p(1) >> (int&)proj.idPoint; + if (in.fail()) + break; + --proj.idPoint; + projs.emplace_back(proj); + } + return true; + } + + // See: colmap/src/base/reconstruction.cc + // void Reconstruction::ReadImagesBinary(const std::string& path) + bool ReadBIN(std::istream& stream) { + if (stream.peek() == EOF) + return false; + + if (!numRegImages) { + // Read the first entry in the binary file + numRegImages = ReadBinaryLittleEndian(&stream); + } + + ID = ReadBinaryLittleEndian(&stream)-1; + q.w() = ReadBinaryLittleEndian(&stream); + q.x() = ReadBinaryLittleEndian(&stream); + q.y() = ReadBinaryLittleEndian(&stream); + q.z() = ReadBinaryLittleEndian(&stream); + t(0) = ReadBinaryLittleEndian(&stream); + t(1) = ReadBinaryLittleEndian(&stream); + t(2) = ReadBinaryLittleEndian(&stream); + idCamera = ReadBinaryLittleEndian(&stream)-1; + + name = ""; + while (true) { + char nameChar; + stream.read(&nameChar, 1); + if (nameChar == '\0') + break; + name += nameChar; + } + Util::ensureValidPath(name); + + const size_t numPoints2D = ReadBinaryLittleEndian(&stream); + projs.clear(); + for (size_t j = 0; j < numPoints2D; ++j) { + Proj proj; + proj.p(0) = (float)ReadBinaryLittleEndian(&stream); + proj.p(1) = (float)ReadBinaryLittleEndian(&stream); + proj.idPoint = (uint32_t)ReadBinaryLittleEndian(&stream)-1; + projs.emplace_back(proj); + } + return true; + } + + bool WriteTXT(std::ostream& out) const { + out << ID+1 << _T(" ") + << q.w() << _T(" ") << q.x() << _T(" ") << q.y() << _T(" ") << q.z() << _T(" ") + << t(0) << _T(" ") << t(1) << _T(" ") << t(2) << _T(" ") + << idCamera+1 << _T(" ") << name + << std::endl; + for (const Proj& proj: projs) { + out << proj.p(0) << _T(" ") << proj.p(1) << _T(" ") << (int)proj.idPoint+1 << _T(" "); + if (out.fail()) + return false; + } + out << std::endl; + return !out.fail(); + } + + bool WriteBIN(std::ostream& stream) { + if (numRegImages != 0) { + // Write the first entry in the binary file + WriteBinaryLittleEndian(&stream, numRegImages); + numRegImages = 0; + } + + WriteBinaryLittleEndian(&stream, ID+1); + + WriteBinaryLittleEndian(&stream, q.w()); + WriteBinaryLittleEndian(&stream, q.x()); + WriteBinaryLittleEndian(&stream, q.y()); + WriteBinaryLittleEndian(&stream, q.z()); + + WriteBinaryLittleEndian(&stream, t(0)); + WriteBinaryLittleEndian(&stream, t(1)); + WriteBinaryLittleEndian(&stream, t(2)); + + WriteBinaryLittleEndian(&stream, idCamera+1); + + stream.write(name.c_str(), name.size()+1); + + WriteBinaryLittleEndian(&stream, projs.size()); + for (const Proj& proj: projs) { + WriteBinaryLittleEndian(&stream, proj.p(0)); + WriteBinaryLittleEndian(&stream, proj.p(1)); + WriteBinaryLittleEndian(&stream, proj.idPoint+1); + } + return !stream.fail(); + } +}; +typedef std::vector Images; +// structure describing a 3D point +struct Point { + struct Track { + uint32_t idImage; + uint32_t idProj; + }; + uint32_t ID; // ID of the point + Interface::Pos3f p; // position + Interface::Col3 c; // BGR color + float e; // error + std::vector tracks; // point track + uint64_t numPoints3D{0}; // only for binary format + + Point() {} + Point(uint32_t _ID) : ID(_ID) {} + bool operator < (const Image& rhs) const { return ID < rhs.ID; } + + bool Read(std::istream& stream, bool binary) { + if (binary) + return ReadBIN(stream); + return ReadTXT(stream); + } + + bool Write(std::ostream& stream, bool binary) { + if (binary) + return WriteBIN(stream); + return WriteTXT(stream); + } + + // 3D point list with one line of data per point: + // POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX) + bool ReadTXT(std::istream& stream) { + std::istringstream in; + if (!NextLine(stream, in)) + return false; + int r,g,b; + in >> ID + >> p.x >> p.y >> p.z + >> r >> g >> b + >> e; + c.x = CLAMP(b,0,255); + c.y = CLAMP(g,0,255); + c.z = CLAMP(r,0,255); + if (in.fail()) + return false; + --ID; + tracks.clear(); + while (true) { + Track track; + in >> track.idImage >> track.idProj; + if (in.fail()) + break; + --track.idImage; --track.idProj; + tracks.emplace_back(track); + } + return !tracks.empty(); + } + + // See: colmap/src/base/reconstruction.cc + // void Reconstruction::ReadPoints3DBinary(const std::string& path) + bool ReadBIN(std::istream& stream) { + if (stream.peek() == EOF) + return false; + + if (!numPoints3D) { + // Read the first entry in the binary file + numPoints3D = ReadBinaryLittleEndian(&stream); + } + + int r,g,b; + ID = (uint32_t)ReadBinaryLittleEndian(&stream)-1; + p.x = (float)ReadBinaryLittleEndian(&stream); + p.y = (float)ReadBinaryLittleEndian(&stream); + p.z = (float)ReadBinaryLittleEndian(&stream); + r = ReadBinaryLittleEndian(&stream); + g = ReadBinaryLittleEndian(&stream); + b = ReadBinaryLittleEndian(&stream); + e = (float)ReadBinaryLittleEndian(&stream); + c.x = CLAMP(b,0,255); + c.y = CLAMP(g,0,255); + c.z = CLAMP(r,0,255); + + const size_t trackLength = ReadBinaryLittleEndian(&stream); + tracks.clear(); + for (size_t j = 0; j < trackLength; ++j) { + Track track; + track.idImage = ReadBinaryLittleEndian(&stream)-1; + track.idProj = ReadBinaryLittleEndian(&stream)-1; + tracks.emplace_back(track); + } + return !tracks.empty(); + } + + bool WriteTXT(std::ostream& out) const { + ASSERT(!tracks.empty()); + const int r(c.z),g(c.y),b(c.x); + out << ID+1 << _T(" ") + << p.x << _T(" ") << p.y << _T(" ") << p.z << _T(" ") + << r << _T(" ") << g << _T(" ") << b << _T(" ") + << e << _T(" "); + for (const Track& track: tracks) { + out << track.idImage+1 << _T(" ") << track.idProj+1 << _T(" "); + if (out.fail()) + return false; + } + out << std::endl; + return !out.fail(); + } + + bool WriteBIN(std::ostream& stream) { + ASSERT(!tracks.empty()); + if (numPoints3D != 0) { + // Write the first entry in the binary file + WriteBinaryLittleEndian(&stream, numPoints3D); + numPoints3D = 0; + } + + WriteBinaryLittleEndian(&stream, ID+1); + WriteBinaryLittleEndian(&stream, p.x); + WriteBinaryLittleEndian(&stream, p.y); + WriteBinaryLittleEndian(&stream, p.z); + WriteBinaryLittleEndian(&stream, c.z); + WriteBinaryLittleEndian(&stream, c.y); + WriteBinaryLittleEndian(&stream, c.x); + WriteBinaryLittleEndian(&stream, e); + + WriteBinaryLittleEndian(&stream, tracks.size()); + for (const Track& track: tracks) { + WriteBinaryLittleEndian(&stream, track.idImage+1); + WriteBinaryLittleEndian(&stream, track.idProj+1); + } + return !stream.fail(); + } +}; +typedef std::vector Points; +// structure describing an 2D dynamic matrix +template +struct Mat { + size_t width_ = 0; + size_t height_ = 0; + size_t depth_ = 0; + std::vector data_; + + size_t GetNumBytes() const { + return data_.size() * sizeof(T); + } + const T* GetChannelPtr(size_t c) const { + return data_.data()+width_*height_*c; + } + + // See: colmap/src/mvs/mat.h + void Read(const std::string& path) { + std::streampos pos; { + std::fstream text_file(path, std::ios::in | std::ios::binary); + char unused_char; + text_file >> width_ >> unused_char >> height_ >> unused_char >> depth_ >> + unused_char; + pos = text_file.tellg(); + } + data_.resize(width_ * height_ * depth_); + std::fstream binary_file(path, std::ios::in | std::ios::binary); + binary_file.seekg(pos); + ReadBinaryLittleEndian(&binary_file, &data_); + } + void Write(const std::string& path) const { + { + std::fstream text_file(path, std::ios::out); + text_file << width_ << "&" << height_ << "&" << depth_ << "&"; + } + std::fstream binary_file(path, std::ios::out | std::ios::binary | std::ios::app); + WriteBinaryLittleEndian(&binary_file, data_); + } +}; +} // namespace COLMAP + +typedef Eigen::Matrix EMat33d; +typedef Eigen::Matrix EVec3d; + +/** + * Determines whether to use the TXT or BIN version of a file based on availability. + * It tries to open the TXT file first; if unsuccessful, it attempts to open the BIN file. + * + * @param filenameTXT The path to the TXT file. + * @param filenameBIN The path to the BIN file. + * @param fileStream Reference to the ifstream object that will be used for file operations. + * @param chosenFilename Reference to a string that will store the path of the chosen file. + * @param isBinaryFormat Reference to a boolean that indicates whether the chosen file is binary. + * @return true if either file was successfully opened, false otherwise. + */ +bool DetermineInputSource(const String& filenameTXT, const String& filenameBIN, std::ifstream& fileStream, String& filenameCamera, bool& isBinaryFormat) +{ + // Try to open the TXT file first. + fileStream.open(filenameTXT); + if (fileStream.good()) { + filenameCamera = filenameTXT; + isBinaryFormat = false; + return true; + } + fileStream.open(filenameBIN, std::ios::binary); + if (fileStream.good()) { + filenameCamera = filenameBIN; + isBinaryFormat = true; + return true; + } + VERBOSE("error: unable to open file '%s'", filenameTXT.c_str()); + VERBOSE("error: unable to open file '%s'", filenameBIN.c_str()); + return false; +} + + +bool ImportScene(const String& inputFolder, const String& outputFolder, Interface& scene, PointCloud& pointcloud) +{ + // Define a map to store camera IDs and associated platform index. + typedef std::unordered_map CamerasMap; + CamerasMap mapCameras; + { + // Determine the camera file source between TXT and BIN formats. + const String filenameCamerasTXT(inputFolder+COLMAP_CAMERAS_TXT); + const String filenameCamerasBIN(inputFolder+COLMAP_CAMERAS_BIN); + std::ifstream cameraFile; + bool isBinaryFormat; + String filenameCamera; + if (!DetermineInputSource(filenameCamerasTXT, filenameCamerasBIN, cameraFile, filenameCamera, isBinaryFormat)) { + return false; // Exit if file source determination fails. + } + LOG_OUT() << "Reading cameras: " << filenameCamera << std::endl; + + typedef std::unordered_map CamerasSet; + CamerasSet setCameras; + COLMAP::Camera colmapCamera; + while (cameraFile.good() && colmapCamera.Read(cameraFile, isBinaryFormat)) { + const auto setIt(setCameras.emplace(colmapCamera, (uint32_t)scene.platforms.size())); + mapCameras.emplace(colmapCamera.ID, setIt.first->second); + if (!setIt.second) { + // reuse existing platform + continue; + } + // create new platform + Interface::Platform platform; + platform.name = String::FormatString(_T("platform%03u"), colmapCamera.ID); // only one camera per platform supported + Interface::Platform::Camera camera; + camera.name = colmapCamera.model; + camera.K = Interface::Mat33d::eye(); + // account for different pixel center conventions as COLMAP uses pixel center at (0.5,0.5) + camera.K(0,0) = colmapCamera.params[0]; + camera.K(1,1) = colmapCamera.params[1]; + camera.K(0,2) = colmapCamera.params[2]-REAL(0.5); + camera.K(1,2) = colmapCamera.params[3]-REAL(0.5); + camera.R = Interface::Mat33d::eye(); + camera.C = Interface::Pos3d(0,0,0); + if (OPT::bNormalizeIntrinsics) { + // normalize camera intrinsics + camera.K = Camera::ScaleK(camera.K, 1.0/Camera::GetNormalizationScale(colmapCamera.width, colmapCamera.height)); + } else { + camera.width = colmapCamera.width; + camera.height = colmapCamera.height; + } + platform.cameras.emplace_back(camera); + scene.platforms.emplace_back(platform); + } + } + if (mapCameras.empty()) { + VERBOSE("error: no valid cameras (make sure they are in PINHOLE model)"); + return false; + } + + // Function to read and process image data from input files + typedef std::map ImagesMap; + ImagesMap mapImages; + { + // Define a map to associate COLMAP image structures with scene image indices + const String filenameImagesTXT(inputFolder+COLMAP_IMAGES_TXT); + const String filenameImagesBIN(inputFolder+COLMAP_IMAGES_BIN); + std::ifstream file; + bool binary; + String filenameImages; + if (!DetermineInputSource(filenameImagesTXT, filenameImagesBIN, file, filenameImages, binary)) { + return false; + } + LOG_OUT() << "Reading images: " << filenameImages << std::endl; + + // Read image data from the file and store in the scene structure + COLMAP::Image imageColmap; + while (file.good() && imageColmap.Read(file, binary)) { + mapImages.emplace(imageColmap, (uint32_t)scene.images.size()); + Interface::Platform::Pose pose; + Eigen::Map(pose.R.val) = imageColmap.q.toRotationMatrix(); + EnsureRotationMatrix((Matrix3x3d&)pose.R); + Eigen::Map(&pose.C.x) = -(imageColmap.q.inverse() * imageColmap.t); + + Interface::Image image; + image.name = MAKE_PATH_REL(outputFolder,OPT::strImageFolder+imageColmap.name); + image.platformID = mapCameras.at(imageColmap.idCamera); + image.cameraID = 0; + image.ID = imageColmap.ID; + + Interface::Platform& platform = scene.platforms[image.platformID]; + image.poseID = (uint32_t)platform.poses.size(); + platform.poses.emplace_back(pose); + scene.images.emplace_back(image); + } + } + + // read points list + const String filenameDensePoints(inputFolder+COLMAP_DENSE_POINTS); + const String filenameDenseVisPoints(inputFolder+COLMAP_DENSE_POINTS_VISIBILITY); + { + // parse sparse point-cloud + const String filenamePointsTXT(inputFolder+COLMAP_POINTS_TXT); + const String filenamePointsBIN(inputFolder+COLMAP_POINTS_BIN); + std::ifstream file; + bool binary; + String filenamePoints; + if (!DetermineInputSource(filenamePointsTXT, filenamePointsBIN, file, filenamePoints, binary)) { + return false; + } + LOG_OUT() << "Reading points: " << filenamePoints << std::endl; + COLMAP::Point point; + while (file.good() && point.Read(file, binary)) { + Interface::Vertex vertex; + vertex.X = point.p; + for (const COLMAP::Point::Track& track: point.tracks) { + Interface::Vertex::View view; + view.imageID = mapImages.at(COLMAP::Image(track.idImage)); + view.confidence = 0; + vertex.views.emplace_back(view); + } + std::sort(vertex.views.begin(), vertex.views.end(), + [](const Interface::Vertex::View& view0, const Interface::Vertex::View& view1) { return view0.imageID < view1.imageID; }); + scene.vertices.emplace_back(std::move(vertex)); + scene.verticesColor.emplace_back(Interface::Color{point.c}); + } + } + pointcloud.Release(); + if (File::access(filenameDensePoints) && File::access(filenameDenseVisPoints)) { + // parse dense point-cloud + LOG_OUT() << "Reading points: " << filenameDensePoints << " and " << filenameDenseVisPoints << std::endl; + if (!pointcloud.Load(filenameDensePoints)) { + VERBOSE("error: unable to open file '%s'", filenameDensePoints.c_str()); + return false; + } + File file(filenameDenseVisPoints, File::READ, File::OPEN); + if (!file.isOpen()) { + VERBOSE("error: unable to open file '%s'", filenameDenseVisPoints.c_str()); + return false; + } + uint64_t numPoints(0); + file.read(&numPoints, sizeof(uint64_t)); + if (pointcloud.GetSize() != numPoints) { + VERBOSE("error: point-cloud and visibility have different size"); + return false; + } + pointcloud.pointViews.resize(numPoints); + for (size_t i=0; isecond]; + CLISTDEF2(String) neighborNames; + Util::strSplit(neighbors, _T(','), neighborNames); + FOREACH(i, neighborNames) { + String& neighborName = neighborNames[i]; + Util::strTrim(neighborName, _T(" ")); + const ImagesMap::const_iterator it_neighbor = std::find_if(mapImages.begin(), mapImages.end(), + [&neighborName](const ImagesMap::value_type& image) { + return image.first.name == neighborName; + }); + if (it_neighbor == mapImages.end()) { + if (i == 0) + break; + continue; + } + imageNeighbors.emplace_back(scene.images[it_neighbor->second].ID); + } + } + } + LOG_OUT() << "Reading depth-maps/normal-maps: " << pathDepthMaps << " and " << pathNormalMaps << std::endl; + Util::ensureFolder(outputFolder); + const String strType[] = {".geometric.bin", ".photometric.bin"}; + FOREACH(idx, scene.images) { + const Interface::Image& image = scene.images[idx]; + COLMAP::Mat colDepthMap, colNormalMap; + const String filenameImage(Util::getFileNameExt(image.name)); + for (int i=0; i<2; ++i) { + const String filenameDepthMaps(pathDepthMaps+filenameImage+strType[i]); + if (File::isFile(filenameDepthMaps)) { + colDepthMap.Read(filenameDepthMaps); + const String filenameNormalMaps(pathNormalMaps+filenameImage+strType[i]); + if (File::isFile(filenameNormalMaps)) { + colNormalMap.Read(filenameNormalMaps); + } + break; + } + } + if (!colDepthMap.data_.empty()) { + IIndexArr IDs = {image.ID}; + IDs.Join(imagesNeighbors[(IIndex)idx]); + const Interface::Platform& platform = scene.platforms[image.platformID]; + const Interface::Platform::Pose pose(platform.GetPose(image.cameraID, image.poseID)); + const Interface::Mat33d K(platform.GetFullK(image.cameraID, (uint32_t)colDepthMap.width_, (uint32_t)colDepthMap.height_)); + MVS::DepthMap depthMap((int)colDepthMap.height_, (int)colDepthMap.width_); + memcpy(depthMap.getData(), colDepthMap.data_.data(), colDepthMap.GetNumBytes()); + MVS::NormalMap normalMap; + if (!colNormalMap.data_.empty()) { + normalMap.create((int)colNormalMap.height_, (int)colNormalMap.width_); + cv::merge(std::vector{ + cv::Mat((int)colNormalMap.height_, (int)colNormalMap.width_, CV_32F, (void*)colNormalMap.GetChannelPtr(0)), + cv::Mat((int)colNormalMap.height_, (int)colNormalMap.width_, CV_32F, (void*)colNormalMap.GetChannelPtr(1)), + cv::Mat((int)colNormalMap.height_, (int)colNormalMap.width_, CV_32F, (void*)colNormalMap.GetChannelPtr(2)) + }, normalMap); + } + MVS::ConfidenceMap confMap; + MVS::ViewsMap viewsMap; + const auto depthMM(std::minmax_element(colDepthMap.data_.cbegin(), colDepthMap.data_.cend())); + const MVS::Depth dMin(*depthMM.first), dMax(*depthMM.second); + if (!ExportDepthDataRaw(outputFolder+String::FormatString("depth%04u.dmap", image.ID), MAKE_PATH_FULL(outputFolder, image.name), IDs, depthMap.size(), K, pose.R, pose.C, dMin, dMax, depthMap, normalMap, confMap, viewsMap)) + return false; + } + } + } + return true; +} + + +bool ImportPointCloud(const String& strPointCloudFileName, Interface& scene) +{ + PointCloud pointcloud; + if (!pointcloud.Load(strPointCloudFileName)) { + VERBOSE("error: cannot load point-cloud file"); + return false; + } + if (!pointcloud.IsValid()) { + VERBOSE("error: loaded point-cloud does not have visibility information"); + return false; + } + // replace scene point-cloud with the loaded one + scene.vertices.clear(); + scene.verticesColor.clear(); + scene.verticesNormal.clear(); + scene.vertices.reserve(pointcloud.points.size()); + if (!pointcloud.colors.empty()) + scene.verticesColor.reserve(pointcloud.points.size()); + if (!pointcloud.normals.empty()) + scene.verticesNormal.reserve(pointcloud.points.size()); + FOREACH(i, pointcloud.points) { + Interface::Vertex vertex; + vertex.X = pointcloud.points[i]; + vertex.views.reserve(pointcloud.pointViews[i].size()); + FOREACH(j, pointcloud.pointViews[i]) { + Interface::Vertex::View& view = vertex.views.emplace_back(); + view.imageID = pointcloud.pointViews[i][j]; + view.confidence = (pointcloud.pointWeights.empty() ? 0.f : pointcloud.pointWeights[i][j]); + } + scene.vertices.emplace_back(std::move(vertex)); + if (!pointcloud.colors.empty()) { + const Pixel8U& c = pointcloud.colors[i]; + scene.verticesColor.emplace_back(Interface::Color{Interface::Col3{c.b, c.g, c.r}}); + } + if (!pointcloud.normals.empty()) + scene.verticesNormal.emplace_back(Interface::Normal{pointcloud.normals[i]}); + } + return true; +} + +bool ExportScene(const String& strFolder, const Interface& scene, bool bForceSparsePointCloud = false, bool binary = true) +{ + Util::ensureFolder(strFolder+COLMAP_SPARSE_FOLDER); + + // write camera list + CLISTDEF0IDX(KMatrix,uint32_t) Ks; + CLISTDEF0IDX(COLMAP::Camera,uint32_t) cams; + { + // Construct the filename for camera data based on the format preference + const String filenameCameras(strFolder+(binary?COLMAP_CAMERAS_BIN:COLMAP_CAMERAS_TXT)); + LOG_OUT() << "Writing cameras: " << filenameCameras << std::endl; + std::ofstream file(filenameCameras, binary ? std::ios::trunc|std::ios::binary : std::ios::trunc); + + if (!file.good()) { + VERBOSE("error: unable to open file '%s'", filenameCameras.c_str()); + return false; + } + + COLMAP::Camera cam; + if (binary) { + cam.numCameras = 0; + for (const Interface::Platform& platform: scene.platforms) + cam.numCameras += (uint32_t)platform.cameras.size(); + } else { + file << _T("# Camera list with one line of data per camera:") << std::endl; + file << _T("# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]") << std::endl; + } + cam.model = _T("PINHOLE"); + cam.params.resize(4); + for (uint32_t ID=0; ID<(uint32_t)scene.platforms.size(); ++ID) { + const Interface::Platform& platform = scene.platforms[ID]; + ASSERT(platform.cameras.size() == 1); // only one camera per platform supported + const Interface::Platform::Camera& camera = platform.cameras[0]; + cam.ID = ID; + KMatrix K; + if (camera.width == 0 || camera.height == 0) { + // find one image using this camera + const Interface::Image* pImage(NULL); + for (uint32_t i=0; i<(uint32_t)scene.images.size(); ++i) { + const Interface::Image& image = scene.images[i]; + if (image.platformID == ID && image.cameraID == 0 && image.poseID != NO_ID) { + pImage = ℑ + break; + } + } + if (pImage == NULL) { + LOG("error: no image using camera %u of platform %u", 0, ID); + continue; + } + IMAGEPTR ptrImage(Image::ReadImageHeader(MAKE_PATH_SAFE(pImage->name))); + if (ptrImage == NULL) + return false; + cam.width = ptrImage->GetWidth(); + cam.height = ptrImage->GetHeight(); + // unnormalize camera intrinsics + K = platform.GetFullK(0, cam.width, cam.height); + } else { + cam.width = camera.width; + cam.height = camera.height; + K = camera.K; + } + // account for different pixel center conventions as COLMAP uses pixel center at (0.5,0.5) + cam.params[0] = K(0,0); + cam.params[1] = K(1,1); + cam.params[2] = K(0,2)+REAL(0.5); + cam.params[3] = K(1,2)+REAL(0.5); + if (!cam.Write(file, binary)) + return false; + Ks.emplace_back(K); + cams.emplace_back(cam); + } + } + + // create images list + COLMAP::Images images; + CameraArr cameras; + float maxNumPointsSparse(0); + const float avgViewsPerPoint(3.f); + const uint32_t avgResolutionSmallView(640*480), avgResolutionLargeView(6000*4000); + const uint32_t avgPointsPerSmallView(3000), avgPointsPerLargeView(12000); + { + images.resize(scene.images.size()); + cameras.resize((unsigned)scene.images.size()); + for (uint32_t ID=0; ID<(uint32_t)scene.images.size(); ++ID) { + const Interface::Image& image = scene.images[ID]; + if (image.poseID == NO_ID) + continue; + const Interface::Platform& platform = scene.platforms[image.platformID]; + const Interface::Platform::Pose& pose = platform.poses[image.poseID]; + ASSERT(image.cameraID == 0); + COLMAP::Image& img = images[ID]; + img.ID = ID; + img.q = Eigen::Quaterniond(Eigen::Map(pose.R.val)); + img.t = -(img.q * Eigen::Map(&pose.C.x)); + img.idCamera = image.platformID; + img.name = MAKE_PATH_REL(OPT::strImageFolder, MAKE_PATH_FULL(WORKING_FOLDER_FULL, image.name)); + Camera& camera = cameras[ID]; + camera.K = Ks[image.platformID]; + camera.R = pose.R; + camera.C = pose.C; + camera.ComposeP(); + const COLMAP::Camera& cam = cams[image.platformID]; + const uint32_t resolutionView(cam.width*cam.height); + const float linearFactor(float(avgResolutionLargeView-resolutionView)/(avgResolutionLargeView-avgResolutionSmallView)); + maxNumPointsSparse += (avgPointsPerSmallView+(avgPointsPerLargeView-avgPointsPerSmallView)*linearFactor)/avgViewsPerPoint; + } + } + + // auto-select dense or sparse mode based on number of points + const bool bSparsePointCloud(scene.vertices.size() < (size_t)maxNumPointsSparse); + if (bSparsePointCloud || bForceSparsePointCloud) { + // write points list + { + const String filenamePoints(strFolder+(binary?COLMAP_POINTS_BIN:COLMAP_POINTS_TXT)); + LOG_OUT() << "Writing points: " << filenamePoints << std::endl; + std::ofstream file(filenamePoints, binary ? std::ios::trunc|std::ios::binary : std::ios::trunc); + if (!file.good()) { + VERBOSE("error: unable to open file '%s'", filenamePoints.c_str()); + return false; + } + uint64_t numPoints3D = 0; + if (binary) { + numPoints3D = scene.vertices.size(); + } else { + file << _T("# 3D point list with one line of data per point:") << std::endl; + file << _T("# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)") << std::endl; + } + for (uint32_t ID=0; ID<(uint32_t)scene.vertices.size(); ++ID) { + const Interface::Vertex& vertex = scene.vertices[ID]; + COLMAP::Point point; + point.ID = ID; + point.p = vertex.X; + for (const Interface::Vertex::View& view: vertex.views) { + COLMAP::Image& img = images[view.imageID]; + point.tracks.emplace_back(COLMAP::Point::Track{view.imageID, (uint32_t)img.projs.size()}); + COLMAP::Image::Proj proj; + proj.idPoint = ID; + const Point3 X(vertex.X); + ProjectVertex_3x4_3_2(cameras[view.imageID].P.val, X.ptr(), proj.p.data()); + // account for different pixel center conventions as COLMAP uses pixel center at (0.5,0.5) + proj.p[0] += REAL(0.5); + proj.p[1] += REAL(0.5); + img.projs.emplace_back(proj); + } + point.c = scene.verticesColor.empty() ? Interface::Col3(255,255,255) : scene.verticesColor[ID].c; + point.e = 0; + if (numPoints3D != 0) { + point.numPoints3D = numPoints3D; + numPoints3D = 0; + } + if (!point.Write(file, binary)) + return false; + } + } + + Util::ensureFolder(strFolder+COLMAP_STEREO_FOLDER); + + // write fusion list + { + const String filenameFusion(strFolder+COLMAP_FUSION); + LOG_OUT() << "Writing fusion configuration: " << filenameFusion << std::endl; + std::ofstream file(filenameFusion); + if (!file.good()) { + VERBOSE("error: unable to open file '%s'", filenameFusion.c_str()); + return false; + } + for (const COLMAP::Image& img: images) { + if (img.projs.empty()) + continue; + file << img.name << std::endl; + if (file.fail()) + return false; + } + } + + // write patch-match list + { + const String filenameFusion(strFolder+COLMAP_PATCHMATCH); + LOG_OUT() << "Writing patch-match configuration: " << filenameFusion << std::endl; + std::ofstream file(filenameFusion); + if (!file.good()) { + VERBOSE("error: unable to open file '%s'", filenameFusion.c_str()); + return false; + } + for (const COLMAP::Image& img: images) { + if (img.projs.empty()) + continue; + file << img.name << std::endl; + if (file.fail()) + return false; + file << _T("__auto__, 20") << std::endl; + if (file.fail()) + return false; + } + } + + Util::ensureFolder(strFolder+COLMAP_STEREO_CONSISTENCYGRAPHS_FOLDER); + Util::ensureFolder(strFolder+COLMAP_STEREO_DEPTHMAPS_FOLDER); + Util::ensureFolder(strFolder+COLMAP_STEREO_NORMALMAPS_FOLDER); + } + if (!bSparsePointCloud) { + // export dense point-cloud + const String filenameDensePoints(strFolder+COLMAP_DENSE_POINTS); + const String filenameDenseVisPoints(strFolder+COLMAP_DENSE_POINTS_VISIBILITY); + LOG_OUT() << "Writing points: " << filenameDensePoints << " and " << filenameDenseVisPoints << std::endl; + File file(filenameDenseVisPoints, File::WRITE, File::CREATE | File::TRUNCATE); + if (!file.isOpen()) { + VERBOSE("error: unable to write file '%s'", filenameDenseVisPoints.c_str()); + return false; + } + const uint64_t numPoints(scene.vertices.size()); + file.write(&numPoints, sizeof(uint64_t)); + PointCloud pointcloud; + for (size_t i=0; iGetWidth(), pImage->GetHeight()); + Eigen::Matrix4d K4(Eigen::Matrix4d::Identity()); + K4.topLeftCorner<3,3>() = Eigen::Map(K.val); + Util::ensureFolder(fileName); + std::ofstream out(fileName); + if (!out.good()) { + VERBOSE("error: unable to open file '%s'", fileName.c_str()); + return false; + } + out << std::setprecision(12); + out << K4(0,0) << _T(" ") << K4(0,1) << _T(" ") << K4(0,2) << _T(" ") << K4(0,3) << _T("\n"); + out << K4(1,0) << _T(" ") << K4(1,1) << _T(" ") << K4(1,2) << _T(" ") << K4(1,3) << _T("\n"); + out << K4(2,0) << _T(" ") << K4(2,1) << _T(" ") << K4(2,2) << _T(" ") << K4(2,3) << _T("\n"); + out << K4(3,0) << _T(" ") << K4(3,1) << _T(" ") << K4(3,2) << _T(" ") << K4(3,3) << _T("\n"); + return !out.fail(); +} + + +// export image poses in log format +// see: http://redwood-data.org/indoor/fileformat.html +// to support: https://www.tanksandtemples.org/tutorial +bool ExportImagesLog(const String& fileName, const Interface& scene) +{ + LOG_OUT() << "Writing poses: " << fileName << std::endl; + Util::ensureFolder(fileName); + std::ofstream out(fileName); + if (!out.good()) { + VERBOSE("error: unable to open file '%s'", fileName.c_str()); + return false; + } + out << std::setprecision(12); + IIndexArr orderedImages((uint32_t)scene.images.size()); + std::iota(orderedImages.begin(), orderedImages.end(), 0u); + orderedImages.Sort([&scene](IIndex i, IIndex j) { + return scene.images[i].ID < scene.images[j].ID; + }); + for (IIndex ID: orderedImages) { + const Interface::Image& image = scene.images[ID]; + Eigen::Matrix3d R(Eigen::Matrix3d::Identity()); + Eigen::Vector3d t(Eigen::Vector3d::Zero()); + if (image.poseID != NO_ID) { + const Interface::Platform& platform = scene.platforms[image.platformID]; + const Interface::Platform::Pose& pose = platform.poses[image.poseID]; + R = Eigen::Map(pose.R.val).transpose(); + t = Eigen::Map(&pose.C.x); + } + Eigen::Matrix4d T(Eigen::Matrix4d::Identity()); + T.topLeftCorner<3,3>() = R; + T.topRightCorner<3,1>() = t; + out << ID << _T(" ") << ID << _T(" ") << 0 << _T("\n"); + out << T(0,0) << _T(" ") << T(0,1) << _T(" ") << T(0,2) << _T(" ") << T(0,3) << _T("\n"); + out << T(1,0) << _T(" ") << T(1,1) << _T(" ") << T(1,2) << _T(" ") << T(1,3) << _T("\n"); + out << T(2,0) << _T(" ") << T(2,1) << _T(" ") << T(2,2) << _T(" ") << T(2,3) << _T("\n"); + out << T(3,0) << _T(" ") << T(3,1) << _T(" ") << T(3,2) << _T(" ") << T(3,3) << _T("\n"); + } + return !out.fail(); +} + + +// export poses in Strecha camera format: +// Strecha model is P = K[R^T|-R^T t] +// our model is P = K[R|t], t = -RC +bool ExportImagesCamera(const String& pathName, const Interface& scene) +{ + LOG_OUT() << "Writing poses: " << pathName << std::endl; + Util::ensureFolder(pathName); + for (uint32_t ID=0; ID<(uint32_t)scene.images.size(); ++ID) { + const Interface::Image& image = scene.images[ID]; + String imageFileName(image.name); + Util::ensureValidPath(imageFileName); + const String fileName(pathName+Util::getFileNameExt(imageFileName)+".camera"); + std::ofstream out(fileName); + if (!out.good()) { + VERBOSE("error: unable to open file '%s'", fileName.c_str()); + return false; + } + out << std::setprecision(12); + KMatrix K(KMatrix::IDENTITY); + RMatrix R(RMatrix::IDENTITY); + CMatrix t(CMatrix::ZERO); + unsigned width(0), height(0); + if (image.platformID != NO_ID && image.cameraID != NO_ID) { + const Interface::Platform& platform = scene.platforms[image.platformID]; + const Interface::Platform::Camera& camera = platform.cameras[image.cameraID]; + if (camera.HasResolution()) { + width = camera.width; + height = camera.height; + K = camera.K; + } else { + IMAGEPTR pImage = Image::ReadImageHeader(image.name); + width = pImage->GetWidth(); + height = pImage->GetHeight(); + K = platform.GetFullK(image.cameraID, width, height); + } + if (image.poseID != NO_ID) { + const Interface::Platform::Pose& pose = platform.poses[image.poseID]; + R = pose.R.t(); + t = pose.C; + } + } + out << K(0,0) << _T(" ") << K(0,1) << _T(" ") << K(0,2) << _T("\n"); + out << K(1,0) << _T(" ") << K(1,1) << _T(" ") << K(1,2) << _T("\n"); + out << K(2,0) << _T(" ") << K(2,1) << _T(" ") << K(2,2) << _T("\n"); + out << _T("0 0 0") << _T("\n"); + out << R(0,0) << _T(" ") << R(0,1) << _T(" ") << R(0,2) << _T("\n"); + out << R(1,0) << _T(" ") << R(1,1) << _T(" ") << R(1,2) << _T("\n"); + out << R(2,0) << _T(" ") << R(2,1) << _T(" ") << R(2,2) << _T("\n"); + out << t.x << _T(" ") << t.y << _T(" ") << t.z << _T("\n"); + out << width << _T(" ") << height << _T("\n"); + if (out.fail()) { + VERBOSE("error: unable to write file '%s'", fileName.c_str()); + return false; + } + } + return true; +} + +} // unnamed namespace + +// Main function with command-line arguments +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application app; // Instance of the application + if (!app.Initialize(argc, argv)) // Initialize app with command line arguments + return EXIT_FAILURE; // Exit if initialization fails + + TD_TIMER_START(); + + if (OPT::IsFromOpenMVS) { + // read MVS input data + Interface scene; // Scene data structure for MVS input + if (!ARCHIVE::SerializeLoad(scene, MAKE_PATH_SAFE(OPT::strInputFileName))) + return EXIT_FAILURE; // Exit if loading fails + + // Check output file type and export accordingly + if (Util::getFileExt(OPT::strOutputFileName) == _T(".log")) { + // write poses in log format + ExportIntrinsicsTxt(MAKE_PATH_FULL(WORKING_FOLDER_FULL, String("intrinsics.txt")), scene); + ExportImagesLog(MAKE_PATH_SAFE(OPT::strOutputFileName), scene); + } else + if (Util::getFileExt(OPT::strOutputFileName) == _T(".camera")) { + // write poses in Strecha camera format + ExportImagesCamera((OPT::strOutputFileName=Util::getFileFullName(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strOutputFileName)))+PATH_SEPARATOR, scene); + } else { + // write COLMAP input data + if (!OPT::strPointCloudFileName.empty() && !ImportPointCloud(MAKE_PATH_SAFE(OPT::strPointCloudFileName), scene)) + return EXIT_FAILURE; + Util::ensureFolderSlash(OPT::strOutputFileName); + ExportScene(MAKE_PATH_SAFE(OPT::strOutputFileName), scene, OPT::bForceSparsePointCloud); + } + VERBOSE("Input data exported: %u images & %u vertices (%s)", scene.images.size(), scene.vertices.size(), TD_TIMER_GET_FMT().c_str()); + } else { + // read COLMAP input data + Interface scene; + const String strOutFolder(Util::getFilePath(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strOutputFileName))); + PointCloud pointcloud; + if (!ImportScene(MAKE_PATH_SAFE(OPT::strInputFileName), strOutFolder, scene, pointcloud)) + return EXIT_FAILURE; + // write MVS input data + Util::ensureFolder(strOutFolder); + if (!ARCHIVE::ofstream(scene, MAKE_PATH_SAFE(OPT::strOutputFileName))) + return EXIT_FAILURE; + if (!pointcloud.IsEmpty() && !pointcloud.Save(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName)) + _T(".ply"), true)) + return EXIT_FAILURE; + VERBOSE("Exported data: %u images, %u points%s (%s)", + scene.images.size(), scene.vertices.size(), pointcloud.IsEmpty()?"":String::FormatString(", %d dense points", pointcloud.GetSize()).c_str(), + TD_TIMER_GET_FMT().c_str()); + } + + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/InterfaceCOLMAPTest/endian.h b/apps/InterfaceCOLMAPTest/endian.h new file mode 100644 index 0000000..12a22ca --- /dev/null +++ b/apps/InterfaceCOLMAPTest/endian.h @@ -0,0 +1,167 @@ +// Copyright (c) 2018, ETH Zurich and UNC Chapel Hill. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of +// its contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de) + +#ifndef COLMAP_SRC_UTIL_ENDIAN_H_ +#define COLMAP_SRC_UTIL_ENDIAN_H_ + +#include +#include +#include + +namespace colmap { + +// Reverse the order of each byte. +template +T ReverseBytes(const T& data); + +// Check the order in which bytes are stored in computer memory. +bool IsLittleEndian(); +bool IsBigEndian(); + +// Convert data between endianness and the native format. Note that, for float +// and double types, these functions are only valid if the format is IEEE-754. +// This is the case for pretty much most processors. +template +T LittleEndianToNative(const T x); +template +T BigEndianToNative(const T x); +template +T NativeToLittleEndian(const T x); +template +T NativeToBigEndian(const T x); + +// Read data in little endian format for cross-platform support. +template +T ReadBinaryLittleEndian(std::istream* stream); +template +void ReadBinaryLittleEndian(std::istream* stream, std::vector* data); + +// Write data in little endian format for cross-platform support. +template +void WriteBinaryLittleEndian(std::ostream* stream, const T& data); +template +void WriteBinaryLittleEndian(std::ostream* stream, const std::vector& data); + +//////////////////////////////////////////////////////////////////////////////// +// Implementation +//////////////////////////////////////////////////////////////////////////////// + +template +T ReverseBytes(const T& data) { + T data_reversed = data; + std::reverse(reinterpret_cast(&data_reversed), + reinterpret_cast(&data_reversed) + sizeof(T)); + return data_reversed; +} + +inline bool IsLittleEndian() { +#ifdef BOOST_BIG_ENDIAN + return false; +#else + return true; +#endif +} + +inline bool IsBigEndian() { +#ifdef BOOST_BIG_ENDIAN + return true; +#else + return false; +#endif +} + +template +T LittleEndianToNative(const T x) { + if (IsLittleEndian()) { + return x; + } else { + return ReverseBytes(x); + } +} + +template +T BigEndianToNative(const T x) { + if (IsBigEndian()) { + return x; + } else { + return ReverseBytes(x); + } +} + +template +T NativeToLittleEndian(const T x) { + if (IsLittleEndian()) { + return x; + } else { + return ReverseBytes(x); + } +} + +template +T NativeToBigEndian(const T x) { + if (IsBigEndian()) { + return x; + } else { + return ReverseBytes(x); + } +} + +template +T ReadBinaryLittleEndian(std::istream* stream) { + T data_little_endian; + stream->read(reinterpret_cast(&data_little_endian), sizeof(T)); + return LittleEndianToNative(data_little_endian); +} + +template +void ReadBinaryLittleEndian(std::istream* stream, std::vector* data) { + for (size_t i = 0; i < data->size(); ++i) { + (*data)[i] = ReadBinaryLittleEndian(stream); + } +} + +template +void WriteBinaryLittleEndian(std::ostream* stream, const T& data) { + const T data_little_endian = NativeToLittleEndian(data); + stream->write(reinterpret_cast(&data_little_endian), sizeof(T)); +} + +template +void WriteBinaryLittleEndian(std::ostream* stream, const std::vector& data) { + for (const auto& elem : data) { + WriteBinaryLittleEndian(stream, elem); + } +} + +} // namespace colmap + +#endif // COLMAP_SRC_UTIL_ENDIAN_H_ + diff --git a/apps/InterfaceMVSNet/CMakeLists.txt b/apps/InterfaceMVSNet/CMakeLists.txt new file mode 100644 index 0000000..e38a82c --- /dev/null +++ b/apps/InterfaceMVSNet/CMakeLists.txt @@ -0,0 +1,13 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(InterfaceMVSNet "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS InterfaceMVSNet + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/InterfaceMVSNet/InterfaceMVSNet.cpp b/apps/InterfaceMVSNet/InterfaceMVSNet.cpp new file mode 100644 index 0000000..7081fb2 --- /dev/null +++ b/apps/InterfaceMVSNet/InterfaceMVSNet.cpp @@ -0,0 +1,706 @@ +/* + * InterfaceMVSNet.cpp + * + * Copyright (c) 2014-2021 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#define JSON_NOEXCEPTION +#include "../../libs/IO/json.hpp" +#include + +using namespace MVS; + + +// D E F I N E S /////////////////////////////////////////////////// + +// uncomment to enable multi-threading based on OpenMP +#ifdef _USE_OPENMP +#define MVSNET_USE_OPENMP +#endif + +#define APPNAME _T("InterfaceMVSNet") +#define MVS_FILE_EXTENSION _T(".mvs") +#define MVSNET_IMAGES_FOLDER _T("images") +#define MVSNET_CAMERAS_FOLDER _T("cams") +#define MVSNET_IMAGES_EXT _T(".jpg") +#define MVSNET_CAMERAS_NAME _T("_cam.txt") +#define RTMV_CAMERAS_EXT _T(".json") +#define NERFSTUDIO_TRANSFORMS _T("transforms.json") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { + String strInputFileName; + String strOutputFileName; + unsigned nArchiveType; + int nProcessPriority; + unsigned nMaxThreads; + String strConfigFileName; + boost::program_options::variables_map vm; +} // namespace OPT + +class Application { +public: + Application() {} + ~Application() { Finalize(); } + + bool Initialize(size_t argc, LPCTSTR* argv); + void Finalize(); +}; // Application + +// initialize and parse the command line parameters +bool Application::Initialize(size_t argc, LPCTSTR* argv) +{ + // initialize log and console + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "produce this help message") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: -1-interface, 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description config("Main options"); + config.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input filename containing camera poses and image list") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the mesh") + ; + + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(config); + + boost::program_options::options_description config_file_options; + config_file_options.add(config); + + boost::program_options::positional_options_description p; + p.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(p).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + INIT_WORKING_FOLDER; + // parse configuration file + std::ifstream ifs(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (ifs) { + boost::program_options::store(parse_config_file(ifs, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // initialize the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-") + Util::getUniqueName(0) + _T(".log")).c_str()); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // validate input + Util::ensureValidPath(OPT::strInputFileName); + const String strInputFileNameExt(Util::getFileExt(OPT::strInputFileName).ToLower()); + const bool bInvalidCommand(OPT::strInputFileName.empty()); + if (OPT::vm.count("help") || bInvalidCommand) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(config); + GET_LOG() << visible; + } + if (bInvalidCommand) + return false; + + // initialize optional options + Util::ensureValidPath(OPT::strOutputFileName); + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = Util::getFileName(OPT::strInputFileName) + "scene" MVS_FILE_EXTENSION; + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::Finalize() +{ + MVS::Finalize(); + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +void ImageListParse(const LPSTR* argv, Point3& C) +{ + // read position vector + C.x = String::FromString(argv[0]); + C.y = String::FromString(argv[1]); + C.z = String::FromString(argv[2]); +} + +void ImageListParse(const LPSTR* argv, Matrix3x3& R) +{ + // read rotation matrix + R(0, 0) = String::FromString(argv[0]); + R(0, 1) = String::FromString(argv[1]); + R(0, 2) = String::FromString(argv[2]); + R(1, 0) = String::FromString(argv[3]); + R(1, 1) = String::FromString(argv[4]); + R(1, 2) = String::FromString(argv[5]); + R(2, 0) = String::FromString(argv[6]); + R(2, 1) = String::FromString(argv[7]); + R(2, 2) = String::FromString(argv[8]); +} + +void ImageListParse(const LPSTR* argv, Matrix3x4& P) +{ + // read projection matrix + P(0, 0) = String::FromString(argv[0]); + P(0, 1) = String::FromString(argv[1]); + P(0, 2) = String::FromString(argv[2]); + P(0, 3) = String::FromString(argv[3]); + P(1, 0) = String::FromString(argv[4]); + P(1, 1) = String::FromString(argv[5]); + P(1, 2) = String::FromString(argv[6]); + P(1, 3) = String::FromString(argv[7]); + P(2, 0) = String::FromString(argv[8]); + P(2, 1) = String::FromString(argv[9]); + P(2, 2) = String::FromString(argv[10]); + P(2, 3) = String::FromString(argv[11]); +} + +// convert a range-map to depth-map +void RangeToDepthMap(const Image32F& rangeMap, const Camera& camera, DepthMap& depthMap) +{ + depthMap.create(rangeMap.size()); + for (int y = 0; y < depthMap.rows; ++y) { + const float* const rangeRow = rangeMap.ptr(y); + float* const depthRow = depthMap.ptr(y); + for (int x = 0; x < depthMap.cols; ++x) { + const float range = rangeRow[x]; + depthRow[x] = (Depth)(range <= 0 ? 0 : normalized(camera.TransformPointI2C(Point2(x,y))).z*range); + } + } +} + +// parse scene stored in MVSNet format composed of undistorted images and camera poses +// for example see GigaVision benchmark (http://gigamvs.net): +// |--sceneX +// |--images +// |--xxx.jpg +// |--xxx.jpg +// .... +// |--cams +// |--xxx_cam.txt +// |--xxx_cam.txt +// .... +// |--render_cams +// |--xxx_cam.txt +// |--xxx_cam.txt +// .... +// +// where the camera parameter of one image stored in a cam.txt file contains the camera +// extrinsic E = [R|t], intrinsic K and the depth range: +// extrinsic +// E00 E01 E02 E03 +// E10 E11 E12 E13 +// E20 E21 E22 E23 +// E30 E31 E32 E33 +// +// intrinsic +// K00 K01 K02 +// K10 K11 K12 +// K20 K21 K22 +// +// DEPTH_MIN DEPTH_INTERVAL (DEPTH_NUM DEPTH_MAX) +bool ParseSceneMVSNet(Scene& scene, const String& strPath) +{ + #if defined(_SUPPORT_CPP17) && (!defined(__GNUC__) || (__GNUC__ > 7)) + IIndex prevPlatformID = NO_ID; + for (const std::filesystem::directory_entry& entry : std::filesystem::directory_iterator((strPath + MVSNET_IMAGES_FOLDER).c_str())) { + if (entry.path().extension() != MVSNET_IMAGES_EXT) + continue; + // parse camera + const std::string strCamFileName(strPath + MVSNET_CAMERAS_FOLDER PATH_SEPARATOR_STR + entry.path().stem().string().c_str() + MVSNET_CAMERAS_NAME); + std::ifstream fcam(strCamFileName); + if (!fcam) + continue; + String line; + do { + std::getline(fcam, line); + } while (line != "extrinsic" && fcam.good()); + String strP; + for (int i = 0; i < 3; ++i) { + std::getline(fcam, line); + strP += ' ' + line; + } + if (!fcam.good()) + continue; + size_t argc; + CAutoPtrArr argv; + argv = Util::CommandLineToArgvA(strP, argc); + if (argc != 12) + continue; + Matrix3x4 P; + ImageListParse(argv, P); + do { + std::getline(fcam, line); + } while (line != "intrinsic" && fcam.good()); + strP.clear(); + for (int i = 0; i < 3; ++i) { + std::getline(fcam, line); + strP += ' ' + line; + } + if (!fcam.good()) + continue; + argv = Util::CommandLineToArgvA(strP, argc); + if (argc != 9) + continue; + Matrix3x3 K; + ImageListParse(argv, K); + // setup camera + IIndex platformID; + if (prevPlatformID == NO_ID || !K.IsEqual(scene.platforms[prevPlatformID].cameras[0].K, 1e-3)) { + prevPlatformID = platformID = scene.platforms.size(); + Platform& platform = scene.platforms.emplace_back(); + Platform::Camera& camera = platform.cameras.emplace_back(); + camera.K = K; + camera.R = RMatrix::IDENTITY; + camera.C = CMatrix::ZERO; + } else { + platformID = prevPlatformID; + } + Platform& platform = scene.platforms[platformID]; + // setup image + const IIndex ID = scene.images.size(); + Image& imageData = scene.images.emplace_back(); + imageData.platformID = platformID; + imageData.cameraID = 0; // only one camera per platform supported by this format + imageData.poseID = NO_ID; + imageData.ID = ID; + imageData.name = entry.path().string(); + Util::ensureUnifySlash(imageData.name); + imageData.name = MAKE_PATH_FULL(strPath, imageData.name); + // set image resolution + IMAGEPTR pimage = Image::ReadImageHeader(imageData.name); + imageData.width = pimage->GetWidth(); + imageData.height = pimage->GetHeight(); + imageData.scale = 1; + // set camera pose + imageData.poseID = platform.poses.size(); + Platform::Pose& pose = platform.poses.emplace_back(); + DecomposeProjectionMatrix(P, pose.R, pose.C); + imageData.camera = platform.GetCamera(imageData.cameraID, imageData.poseID); + } + if (scene.images.size() < 2) + return false; + scene.nCalibratedImages = (unsigned)scene.images.size(); + return true; + #else + VERBOSE("error: C++17 is required to parse MVSNet format"); + return false; + #endif // _SUPPORT_CPP17 +} + +// RTMV scene format: http://www.cs.umd.edu/~mmeshry/projects/rtmv +// |--sceneX +// |--images +// |--xxx.jpg +// |--xxx.jpg +// .... +// |--outputs (optional) +// |--depthxxxx.exr +// |--normalxxxx.exr +// .... +// |--transforms.json +bool ParseSceneNerfstudio(Scene& scene, const String& strPath) +{ + const nlohmann::json data = nlohmann::json::parse(std::ifstream(strPath + NERFSTUDIO_TRANSFORMS)); + if (data.empty()) + return false; + // parse camera + const cv::Size resolution(data["w"].get(), data["h"].get()); + const IIndex platformID = scene.platforms.size(); + Platform& platform = scene.platforms.emplace_back(); + Platform::Camera& camera = platform.cameras.emplace_back(); + camera.K = KMatrix::IDENTITY; + camera.R = RMatrix::IDENTITY; + camera.C = CMatrix::ZERO; + camera.K(0,0) = data["fl_x"].get(); + camera.K(1,1) = data["fl_y"].get(); + camera.K(0,2) = data["cx"].get(); + camera.K(1,2) = data["cy"].get(); + const String cameraModel = data["camera_model"].get(); + if (cameraModel == "SIMPLE_PINHOLE") { + } else + // check ZERO radial distortion for all "PERSPECTIVE" type cameras + if (cameraModel == "PINHOLE" || cameraModel == "SIMPLE_RADIAL" || cameraModel == "RADIAL" || cameraModel == "OPENCV") { + const REAL k1 = data["k1"].get(); + const REAL k2 = data["k2"].get(); + const REAL p1 = data["p1"].get(); + const REAL p2 = data["p2"].get(); + if (k1 != 0 || k2 != 0 || p1 != 0 || p2 != 0) { + VERBOSE("error: radial distortion not supported"); + return false; + } + } else { + VERBOSE("error: camera model not supported"); + return false; + } + // parse images + const nlohmann::json& frames = data["frames"]; + for (const nlohmann::json& frame: frames) { + // set image + // frames expected to be ordered in JSON + const IIndex imageID = scene.images.size(); + const String strFileName(strPath + frame["file_path"].get().c_str()); + Image& imageData = scene.images.emplace_back(); + imageData.platformID = platformID; + imageData.cameraID = 0; // only one camera per platform supported by this format + imageData.poseID = NO_ID; + imageData.ID = imageID; + imageData.name = strFileName; + ASSERT(Util::isFullPath(imageData.name)); + // set image resolution + imageData.width = resolution.width; + imageData.height = resolution.height; + imageData.scale = 1; + // load camera pose + imageData.poseID = platform.poses.size(); + Platform::Pose& pose = platform.poses.emplace_back(); + const auto Ps = frame["transform_matrix"].get>>(); + Eigen::Matrix4d P{ + {Ps[0][0], Ps[0][1], Ps[0][2], Ps[0][3]}, + {Ps[1][0], Ps[1][1], Ps[1][2], Ps[1][3]}, + {Ps[2][0], Ps[2][1], Ps[2][2], Ps[2][3]}, + {Ps[3][0], Ps[3][1], Ps[3][2], Ps[3][3]} + }; + // revert nerfstudio conversion: + // convert from COLMAP's camera coordinate system (OpenCV) to ours (OpenGL) + // c2w[0:3, 1:3] *= -1 + // c2w = c2w[np.array([1, 0, 2, 3]), :] + // c2w[2, :] *= -1 + P.row(2) *= -1; + P.row(0).swap(P.row(1)); + P.col(2) *= -1; + P.col(1) *= -1; + // set camera pose + pose.R = P.topLeftCorner<3, 3>().transpose().eval(); + pose.R.EnforceOrthogonality(); + pose.C = P.topRightCorner<3, 1>().eval(); + imageData.camera = platform.GetCamera(imageData.cameraID, imageData.poseID); + // try reading depth-map and normal-map + DepthMap depthMap; { + const String depthPath(strPath + String::FormatString("outputs/depth%04u.exr", imageID)); + const Image32F rangeMap = cv::imread(depthPath, cv::IMREAD_UNCHANGED); + if (rangeMap.empty()) { + VERBOSE("Unable to load depthmap %s.", depthPath.c_str()); + continue; + } + RangeToDepthMap(rangeMap, imageData.camera, depthMap); + } + NormalMap normalMap; { + const String normalPath(strPath + String::FormatString("outputs/normal%04u.exr", imageID)); + normalMap = cv::imread(normalPath, cv::IMREAD_UNCHANGED); + if (normalMap.empty()) { + VERBOSE("Unable to load normalMap %s.", normalPath.c_str()); + continue; + } + } + const ConfidenceMap confMap; + const ViewsMap viewsMap; + const IIndexArr IDs = {imageID}; + double dMin, dMax; + cv::minMaxIdx(depthMap, &dMin, &dMax, NULL, NULL, depthMap > 0); + const String dmapPath(strPath + String::FormatString("depth%04u.dmap", imageID)); + if (!ExportDepthDataRaw(dmapPath, + imageData.name, IDs, resolution, + camera.K, pose.R, pose.C, + (float)dMin, (float)dMax, + depthMap, normalMap, confMap, viewsMap)) + { + VERBOSE("Unable to save dmap: %s", dmapPath.c_str()); + continue; + } + } + if (scene.images.size() < 2) + return false; + scene.nCalibratedImages = (unsigned)scene.images.size(); + return true; +} + +// RTMV scene format: http://www.cs.umd.edu/~mmeshry/projects/rtmv +// |--sceneX +// |--xxx.exr +// |--xxx.seg.exr +// |--xxx.depth.exr +// |--xxx.json +// .... +bool ParseSceneRTMV(Scene& scene, const String& strPath) +{ + const String strImagePath(strPath + "images/"); + Util::ensureFolder(strImagePath); + std::vector strImageNames; + #if defined(_SUPPORT_CPP17) && (!defined(__GNUC__) || (__GNUC__ > 7)) + for (const std::filesystem::directory_entry& entry : std::filesystem::directory_iterator(strPath.c_str())) { + if (entry.path().extension() != RTMV_CAMERAS_EXT) + continue; + strImageNames.emplace_back(entry.path().stem().string()); + } + #else + VERBOSE("error: C++17 is required to parse RTMV format"); + return false; + #endif // _SUPPORT_CPP17 + IIndex prevPlatformID = NO_ID; + scene.images.resize((IIndex)strImageNames.size()); + scene.platforms.reserve((IIndex)strImageNames.size()); + #ifdef MVSNET_USE_OPENMP + #pragma omp parallel for schedule(dynamic) + for (int_t i=0; i<(int_t)strImageNames.size(); ++i) { + #else + FOREACH(i, strImageNames) { + #endif + const IIndex imageID((IIndex)i); + const String& strImageName(strImageNames[imageID]); + // parse camera + const String strFileName(strPath + strImageName); + const nlohmann::json dataCamera = nlohmann::json::parse(std::ifstream(strFileName+RTMV_CAMERAS_EXT)); + if (dataCamera.empty()) + continue; + const nlohmann::json& data = dataCamera["camera_data"]; + const cv::Size resolution(data["width"].get(), data["height"].get()); + // set platform + Matrix3x3 K = Matrix3x3::IDENTITY; + K(0,0) = data["intrinsics"]["fx"].get(); + K(1,1) = data["intrinsics"]["fy"].get(); + K(0,2) = data["intrinsics"]["cx"].get(); + K(1,2) = data["intrinsics"]["cy"].get(); + IIndex platformID; + if (prevPlatformID == NO_ID || !K.IsEqual(scene.platforms[prevPlatformID].cameras[0].K, 1e-3)) { + #ifdef MVSNET_USE_OPENMP + #pragma omp critical + #endif + { + prevPlatformID = platformID = scene.platforms.size(); + Platform& platform = scene.platforms.emplace_back(); + Platform::Camera& camera = platform.cameras.emplace_back(); + platform.poses.reserve((IIndex)strImageNames.size()); + camera.K = K; + camera.R = RMatrix::IDENTITY; + camera.C = CMatrix::ZERO; + } + } else { + platformID = prevPlatformID; + } + Platform& platform = scene.platforms[platformID]; + // set image + Image& imageData = scene.images[imageID]; + imageData.platformID = platformID; + imageData.cameraID = 0; // only one camera per platform supported by this format + imageData.poseID = NO_ID; + imageData.ID = imageID; + imageData.name = strImagePath+strImageName+".jpg"; + ASSERT(Util::isFullPath(imageData.name)); + { + cv::Mat image = cv::imread(strFileName+".exr", cv::IMREAD_UNCHANGED); + ASSERT(image.type() == CV_32FC4); + std::vector channels; + cv::split(image, channels); + cv::merge(std::vector{channels[0], channels[1], channels[2]}, image); + image.convertTo(imageData.image, CV_8UC3, 255); + } + ASSERT(resolution == imageData.image.size()); + if (imageData.image.empty()) { + VERBOSE("Unable to load image %s.", (strFileName+".exr").c_str()); + continue; + } + cv::imwrite(imageData.name, imageData.image); + imageData.ReleaseImage(); + // set image resolution + imageData.width = resolution.width; + imageData.height = resolution.height; + imageData.scale = 1; + // load camera pose + #ifdef MVSNET_USE_OPENMP + #pragma omp critical + #endif + { + imageData.poseID = platform.poses.size(); + platform.poses.emplace_back(); + } + Platform::Pose& pose = platform.poses[imageData.poseID]; + const auto Ps = data["cam2world"].get>>(); + Eigen::Matrix4d P{ + {Ps[0][0], Ps[1][0], Ps[2][0], Ps[3][0]}, + {Ps[0][1], Ps[1][1], Ps[2][1], Ps[3][1]}, + {Ps[0][2], Ps[1][2], Ps[2][2], Ps[3][2]}, + {Ps[0][3], Ps[1][3], Ps[2][3], Ps[3][3]} + }; + // apply the same transforms as nerfstudio converter + P.row(2) *= -1; + P.row(0).swap(P.row(1)); + P.col(2) *= -1; + P.col(1) *= -1; + // set camera pose + pose.R = P.topLeftCorner<3, 3>().transpose().eval(); + pose.R.EnforceOrthogonality(); + pose.C = P.topRightCorner<3, 1>().eval(); + imageData.camera = platform.GetCamera(imageData.cameraID, imageData.poseID); + // try reading the segmentation mask + { + cv::Mat imgMask = cv::imread(strFileName+".seg.exr", cv::IMREAD_UNCHANGED); + if (imgMask.empty()) { + VERBOSE("Unable to load segmentation mask %s.", (strFileName+".seg.exr").c_str()); + continue; + } + ASSERT(imgMask.type() == CV_32FC4); + ASSERT(resolution == imgMask.size()); + std::vector channels; + cv::split(imgMask, channels); + channels[0].convertTo(imgMask, CV_16U); + imageData.maskName = strImagePath+strImageName+".mask.png"; + cv::imwrite(imageData.maskName, imgMask); + } + // try reading the depth-map + DepthMap depthMap; { + const cv::Mat imgDepthMap = cv::imread(strFileName+".depth.exr", cv::IMREAD_UNCHANGED); + if (imgDepthMap.empty()) { + VERBOSE("Unable to load depthmap %s.", (strFileName+".depth.exr").c_str()); + continue; + } + ASSERT(imgDepthMap.type() == CV_32FC4); + ASSERT(resolution == imgDepthMap.size()); + std::vector channels; + cv::split(imgDepthMap, channels); + RangeToDepthMap(channels[0], imageData.camera, depthMap); + } + const NormalMap normalMap; + const ConfidenceMap confMap; + const ViewsMap viewsMap; + const IIndexArr IDs = {imageID}; + double dMin, dMax; + cv::minMaxIdx(depthMap, &dMin, &dMax, NULL, NULL, depthMap > 0); + const String dmapPath(strPath + String::FormatString("depth%04u.dmap", imageID)); + if (!ExportDepthDataRaw(dmapPath, + imageData.name, IDs, resolution, + K, pose.R, pose.C, + (float)dMin, (float)dMax, + depthMap, normalMap, confMap, viewsMap)) + { + VERBOSE("Unable to save dmap: %s", dmapPath.c_str()); + continue; + } + } + if (scene.images.size() < 2) + return false; + scene.nCalibratedImages = (unsigned)scene.images.size(); + return true; +} + +bool ParseScene(Scene& scene) +{ + #if defined(_SUPPORT_CPP17) && (!defined(__GNUC__) || (__GNUC__ > 7)) + String strPath(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strInputFileName)); + Util::ensureValidFolderPath(strPath); + const std::filesystem::path path(static_cast(strPath)); + enum Type { + MVSNet = 0, + NERFSTUDIO, + RTMV, + } sceneType = MVSNet; + for (const std::filesystem::directory_entry& entry : std::filesystem::directory_iterator(path)) { + if (entry.path().extension() == RTMV_CAMERAS_EXT) { + if (entry.path().filename() == NERFSTUDIO_TRANSFORMS) { + sceneType = NERFSTUDIO; + break; + } + sceneType = RTMV; + } + } + switch (sceneType) { + case NERFSTUDIO: return ParseSceneNerfstudio(scene, strPath); + case RTMV: return ParseSceneRTMV(scene, strPath); + default: return ParseSceneMVSNet(scene, strPath); + } + #else + VERBOSE("error: C++17 is required to parse MVSNet format"); + return false; + #endif // _SUPPORT_CPP17 +} + +} // unnamed namespace + +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application application; + if (!application.Initialize(argc, argv)) + return EXIT_FAILURE; + + TD_TIMER_START(); + + Scene scene(OPT::nMaxThreads); + + // convert data from MVSNet format to OpenMVS + if (!ParseScene(scene)) + return EXIT_FAILURE; + + // write OpenMVS input data + scene.Save(MAKE_PATH_SAFE(OPT::strOutputFileName), (ARCHIVE_TYPE)OPT::nArchiveType); + + VERBOSE("Imported data: %u platforms, %u images, %u vertices (%s)", + scene.platforms.size(), scene.images.size(), scene.pointcloud.GetSize(), + TD_TIMER_GET_FMT().c_str()); + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/InterfaceMetashape/CMakeLists.txt b/apps/InterfaceMetashape/CMakeLists.txt new file mode 100644 index 0000000..100e829 --- /dev/null +++ b/apps/InterfaceMetashape/CMakeLists.txt @@ -0,0 +1,13 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(InterfaceMetashape "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS InterfaceMetashape + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/InterfaceMetashape/InterfaceMetashape.cpp b/apps/InterfaceMetashape/InterfaceMetashape.cpp new file mode 100644 index 0000000..d618a65 --- /dev/null +++ b/apps/InterfaceMetashape/InterfaceMetashape.cpp @@ -0,0 +1,887 @@ +/* + * InterfaceMetashape.cpp + * + * Copyright (c) 2014-2021 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#include "../../libs/IO/TinyXML2.h" +#include + +using namespace MVS; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("InterfaceMetashape") // previously PhotoScan +#define MVS_FILE_EXTENSION _T(".mvs") +#define XML_EXT _T(".xml") +#define PLY_EXT _T(".ply") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { +String strInputFileName; +String strPointsFileName; +String strOutputFileName; +String strOutputImageFolder; +unsigned nArchiveType; +int nProcessPriority; +unsigned nMaxThreads; +String strConfigFileName; +boost::program_options::variables_map vm; +} // namespace OPT + +class Application { +public: + Application() {} + ~Application() { Finalize(); } + + bool Initialize(size_t argc, LPCTSTR* argv); + void Finalize(); +}; // Application + +// initialize and parse the command line parameters +bool Application::Initialize(size_t argc, LPCTSTR* argv) +{ + // initialize log and console + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "imports SfM scene stored either in Metashape Agisoft/BlocksExchange or ContextCapture BlocksExchange XML format") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: -1-interface, 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description config("Main options"); + config.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input filename containing camera poses and image list") + ("points-file,p", boost::program_options::value(&OPT::strPointsFileName), "input filename containing the 3D points") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the scene") + ("output-image-folder", boost::program_options::value(&OPT::strOutputImageFolder)->default_value("undistorted_images"), "output folder to store undistorted images") + ; + + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(config); + + boost::program_options::options_description config_file_options; + config_file_options.add(config); + + boost::program_options::positional_options_description p; + p.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(p).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + INIT_WORKING_FOLDER; + // parse configuration file + std::ifstream ifs(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (ifs) { + boost::program_options::store(parse_config_file(ifs, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // initialize the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-")+Util::getUniqueName(0)+_T(".log")).c_str()); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // validate input + Util::ensureValidPath(OPT::strPointsFileName); + Util::ensureValidPath(OPT::strInputFileName); + Util::ensureValidFolderPath(OPT::strOutputImageFolder); + const bool bInvalidCommand(OPT::strInputFileName.empty()); + if (OPT::vm.count("help") || bInvalidCommand) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(config); + GET_LOG() << visible; + } + if (bInvalidCommand) + return false; + + // initialize optional options + Util::ensureValidPath(OPT::strOutputFileName); + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = Util::getFileName(OPT::strInputFileName) + MVS_FILE_EXTENSION; + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::Finalize() +{ + MVS::Finalize(); + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +struct DistCoeff { + union { + REAL coeff[8]; + struct { + REAL k1, k2, p1, p2, k3, k4, k5, k6; + }; + }; + DistCoeff() : k1(0), k2(0), p1(0), p2(0), k3(0), k4(0), k5(0), k6(0) {} + bool HasDistortion() const { return k1 != 0 || k2 != 0 || k3 != 0 || k4 != 0 || k5 != 0 || k6 != 0; } +}; +typedef cList DistCoeffs; +typedef cList PlatformDistCoeffs; + +void ImageListParseC(const LPSTR* argv, Point3& C) +{ + // read position vector + C.x = String::FromString(argv[0]); + C.y = String::FromString(argv[1]); + C.z = String::FromString(argv[2]); +} + +void ImageListParseR(const LPSTR* argv, Matrix3x3& R) +{ + // read rotation matrix + R(0, 0) = String::FromString(argv[0]); + R(0, 1) = String::FromString(argv[1]); + R(0, 2) = String::FromString(argv[2]); + R(1, 0) = String::FromString(argv[3]); + R(1, 1) = String::FromString(argv[4]); + R(1, 2) = String::FromString(argv[5]); + R(2, 0) = String::FromString(argv[6]); + R(2, 1) = String::FromString(argv[7]); + R(2, 2) = String::FromString(argv[8]); +} + +void ImageListParseP(const LPSTR* argv, Matrix3x4& P) +{ + // read projection matrix + P(0, 0) = String::FromString(argv[0]); + P(0, 1) = String::FromString(argv[1]); + P(0, 2) = String::FromString(argv[2]); + P(0, 3) = String::FromString(argv[3]); + P(1, 0) = String::FromString(argv[4]); + P(1, 1) = String::FromString(argv[5]); + P(1, 2) = String::FromString(argv[6]); + P(1, 3) = String::FromString(argv[7]); + P(2, 0) = String::FromString(argv[8]); + P(2, 1) = String::FromString(argv[9]); + P(2, 2) = String::FromString(argv[10]); + P(2, 3) = String::FromString(argv[11]); +} + +// parse images list containing calibration and pose information +// and load the corresponding 3D point-cloud +bool ParseImageListXML(tinyxml2::XMLDocument& doc, Scene& scene, PlatformDistCoeffs& pltDistCoeffs, size_t& nCameras, size_t& nPoses) +{ + String strInputFileName(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strInputFileName)); + Util::ensureValidPath(strInputFileName); + tinyxml2::XMLElement* elem; + if (doc.ErrorID() != tinyxml2::XML_SUCCESS) + goto InvalidDocument; + { + tinyxml2::XMLElement* document = doc.FirstChildElement(_T("document"))->FirstChildElement(_T("chunk")); + if (document == NULL) + goto InvalidDocument; + { + bool bMetashapeFile(false); + CLISTDEF0(cv::Size) resolutions; + std::unordered_map mapPlatformID; + std::unordered_map mapImageID; + + // parse platform and camera models + { + tinyxml2::XMLElement* sensors = document->FirstChildElement(_T("sensors")); + if (sensors == NULL) + goto InvalidDocument; + { + for (tinyxml2::XMLElement* sensor=sensors->FirstChildElement(); sensor!=NULL; sensor=sensor->NextSiblingElement()) { + unsigned ID; + if (0 != _tcsicmp(sensor->Value(), _T("sensor")) || sensor->QueryUnsignedAttribute(_T("id"), &ID) != tinyxml2::XML_SUCCESS) + goto InvalidDocument; + { + // add new camera + enum CameraModel {METASHAPE=0, VSFM}; + int model(METASHAPE); + sensor->QueryIntAttribute(_T("model"), &model); + mapPlatformID.emplace(ID, scene.platforms.size()); + Platform& platform = scene.platforms.AddEmpty(); + LPCTSTR name; + if ((name=sensor->Attribute(_T("label"))) != NULL) + platform.name = name; + // parse intrinsics + tinyxml2::XMLElement* calibration = sensor->FirstChildElement(_T("calibration")); + if (calibration == NULL) + goto InvalidDocument; + { + if ((elem=calibration->FirstChildElement(_T("resolution"))) != NULL) { + resolutions.emplace_back( + elem->UnsignedAttribute(_T("width")), + elem->UnsignedAttribute(_T("height")) + ); + ASSERT(model == METASHAPE); + bMetashapeFile = true; + } + Platform::Camera& camera = platform.cameras.AddEmpty(); + camera.K = KMatrix::IDENTITY; + camera.R = RMatrix::IDENTITY; + camera.C = CMatrix::ZERO; + DistCoeff& dc = pltDistCoeffs.AddEmpty().AddEmpty(); + for (elem=calibration->FirstChildElement(); elem!=NULL; elem=elem->NextSiblingElement()) { + if (0 == _tcsicmp(elem->Value(), _T("f"))) { + camera.K(0,0) = camera.K(1,1) = String::FromString(elem->GetText()); + } else + if (0 == _tcsicmp(elem->Value(), _T("fx"))) { + elem->QueryDoubleText(&camera.K(0,0)); + } else + if (0 == _tcsicmp(elem->Value(), _T("fy"))) { + elem->QueryDoubleText(&camera.K(1,1)); + } else + if (0 == _tcsicmp(elem->Value(), _T("cx"))) { + elem->QueryDoubleText(&camera.K(0,2)); + } else + if (0 == _tcsicmp(elem->Value(), _T("cy"))) { + elem->QueryDoubleText(&camera.K(1,2)); + } else + if (0 == _tcsicmp(elem->Value(), _T("k1"))) { + elem->QueryDoubleText(&dc.k1); + } else + if (0 == _tcsicmp(elem->Value(), _T("k2"))) { + elem->QueryDoubleText(&dc.k2); + } else + if (0 == _tcsicmp(elem->Value(), _T("k3"))) { + elem->QueryDoubleText(&dc.k3); + } else + if (0 == _tcsicmp(elem->Value(), _T("p1"))) { + elem->QueryDoubleText(&dc.p1); + } else + if (0 == _tcsicmp(elem->Value(), _T("p2"))) { + elem->QueryDoubleText(&dc.p2); + } else + if (0 == _tcsicmp(elem->Value(), _T("k4"))) { + elem->QueryDoubleText(&dc.k4); + } else + if (0 == _tcsicmp(elem->Value(), _T("k5"))) { + elem->QueryDoubleText(&dc.k5); + } else + if (0 == _tcsicmp(elem->Value(), _T("k6"))) { + elem->QueryDoubleText(&dc.k6); + } + } + if (bMetashapeFile) { + const cv::Size& resolution = resolutions.back(); + camera.K(0,2) += resolution.width*REAL(0.5); + camera.K(1,2) += resolution.height*REAL(0.5); + camera.K = camera.GetScaledK(REAL(1)/Camera::GetNormalizationScale(resolution.width, resolution.height)); + std::swap(dc.p1, dc.p2); + } + ++nCameras; + } + } + } + } + } + + // parse poses + { + tinyxml2::XMLElement* cameras = document->FirstChildElement(_T("cameras")); + if (cameras == NULL) + goto InvalidDocument; + { + PMatrix P; + size_t argc; + const String strPath(Util::getFilePath(strInputFileName)); + for (tinyxml2::XMLElement* camera=cameras->FirstChildElement(); camera!=NULL; camera=camera->NextSiblingElement()) { + unsigned ID; + if (0 != _tcsicmp(camera->Value(), _T("camera")) || camera->QueryUnsignedAttribute(_T("id"), &ID) != tinyxml2::XML_SUCCESS) + goto InvalidDocument; + { + // add new image + mapImageID.emplace(ID, scene.images.size()); + Image& imageData = scene.images.AddEmpty(); + LPCTSTR name; + if ((name=camera->Attribute(_T("type"))) != NULL && _tcsicmp(name, _T("frame")) != 0) { + DEBUG_EXTRA("warning: unsupported camera calibration '%s'", name); + continue; + } + if ((name=camera->Attribute(_T("label"))) != NULL) + imageData.name = name; + Util::ensureUnifySlash(imageData.name); + if (Util::getFileExt(imageData.name).empty()) + imageData.name += _T(".jpg"); + imageData.name = MAKE_PATH_FULL(strPath, imageData.name); + imageData.platformID = mapPlatformID.at(camera->UnsignedAttribute(_T("sensor_id"))); + imageData.cameraID = 0; // only one camera per platform supported by this format + imageData.ID = mapImageID.at(ID); + const cv::Size& resolution = resolutions[imageData.platformID]; + imageData.width = resolution.width; + imageData.height = resolution.height; + imageData.scale = 1; + if (!bMetashapeFile && !camera->BoolAttribute(_T("enabled"))) { + imageData.poseID = NO_ID; + DEBUG_EXTRA("warning: uncalibrated image '%s'", name); + continue; + } + // set pose + CAutoPtrArr argv; + if ((elem=camera->FirstChildElement(_T("transform"))) == NULL || + (argv=Util::CommandLineToArgvA(elem->GetText(), argc)) == NULL || + (argc != (bMetashapeFile ? 16 : 12))) + { + VERBOSE("Invalid image list camera: %u", ID); + continue; + } + Platform& platform = scene.platforms[imageData.platformID]; + imageData.poseID = platform.poses.size(); + Platform::Pose& pose = platform.poses.AddEmpty(); + ImageListParseP(argv, P); + DecomposeProjectionMatrix(P, pose.R, pose.C); + if (bMetashapeFile) { + pose.C = pose.R*(-pose.C); + pose.R = pose.R.t(); + } + imageData.camera = platform.GetCamera(imageData.cameraID, imageData.poseID); + ++nPoses; + } + } + scene.nCalibratedImages = (unsigned)nPoses; + } + + // parse bounding-box + { + tinyxml2::XMLElement* region = document->FirstChildElement(_T("region")); + if (region == NULL) + goto InvalidDocument; + { + size_t argc; + CAutoPtrArr argv; + Point3 C, E; Matrix3x3 R; + if ((elem=region->FirstChildElement(_T("center"))) == NULL || + (argv=Util::CommandLineToArgvA(elem->GetText(), argc)) == NULL || + argc != 3) + { + VERBOSE("Invalid image list region: %s", elem->GetText()); + goto InvalidDocument; + } + ImageListParseC(argv, C); + if ((elem=region->FirstChildElement(_T("size"))) == NULL || + (argv=Util::CommandLineToArgvA(elem->GetText(), argc)) == NULL || + argc != 3) + { + VERBOSE("Invalid image list region: %s", elem->GetText()); + goto InvalidDocument; + } + ImageListParseC(argv, E); + E *= REAL(0.5); + if ((elem=region->FirstChildElement(_T("R"))) == NULL || + (argv=Util::CommandLineToArgvA(elem->GetText(), argc)) == NULL || + argc != 9) + { + VERBOSE("Invalid image list region: %s", elem->GetText()); + goto InvalidDocument; + } + ImageListParseR(argv, R); + scene.obb.m_rot = Cast(R); + scene.obb.m_pos = Cast(C); + scene.obb.m_ext = Cast(E); + } + } + } + } + } + + return true; + InvalidDocument: + VERBOSE("Invalid camera list"); + return false; +} + +// parse scene stored in ContextCapture BlocksExchange format containing cameras, images and sparse point-cloud +bool ParseBlocksExchangeXML(tinyxml2::XMLDocument& doc, Scene& scene, PlatformDistCoeffs& pltDistCoeffs, size_t& nCameras, size_t& nPoses) { + String strInputFileName(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strInputFileName)); + Util::ensureValidPath(strInputFileName); + const tinyxml2::XMLElement* blocksExchange; + const tinyxml2::XMLElement* document; + const tinyxml2::XMLElement* photogroups; + if (doc.ErrorID() != tinyxml2::XML_SUCCESS || + (blocksExchange=doc.FirstChildElement("BlocksExchange")) == NULL || + (document=blocksExchange->FirstChildElement("Block")) == NULL || + (photogroups=document->FirstChildElement("Photogroups")) == NULL) { + VERBOSE("error: invalid scene file"); + return false; + } + CLISTDEF0(cv::Size) resolutions; + std::unordered_map mapImageID; + const String strPath(Util::getFilePath(strInputFileName)); + const tinyxml2::XMLElement* elem; + for (const tinyxml2::XMLElement* photogroup=photogroups->FirstChildElement(); photogroup!=NULL; photogroup=photogroup->NextSiblingElement()) { + if ((elem=photogroup->FirstChildElement("CameraModelType")) == NULL || + std::strcmp(elem->GetText(), "Perspective") != 0) + continue; + if ((elem=photogroup->FirstChildElement("ImageDimensions")) == NULL) + continue; + const IIndex platformID = scene.platforms.size(); + Platform& platform = scene.platforms.AddEmpty(); + platform.name = photogroup->FirstChildElement("Name")->GetText(); + resolutions.emplace_back( + elem->FirstChildElement("Width")->UnsignedText(), + elem->FirstChildElement("Height")->UnsignedText() + ); + // parse camera + Platform::Camera& camera = platform.cameras.AddEmpty(); + camera.K = KMatrix::IDENTITY; + camera.R = RMatrix::IDENTITY; + camera.C = CMatrix::ZERO; + const float resolutionScale = Camera::GetNormalizationScale(resolutions.back().width, resolutions.back().height); + if ((elem=photogroup->FirstChildElement("FocalLengthPixels")) != NULL) { + camera.K(0,0) = camera.K(1,1) = photogroup->FirstChildElement("FocalLengthPixels")->DoubleText(); + } else { + camera.K(0,0) = camera.K(1,1) = photogroup->FirstChildElement("FocalLength")->DoubleText() * resolutionScale / photogroup->FirstChildElement("SensorSize")->DoubleText(); + } + if ((elem=photogroup->FirstChildElement("PrincipalPoint")) != NULL) { + camera.K(0,2) = elem->FirstChildElement("x")->DoubleText(); + camera.K(1,2) = elem->FirstChildElement("y")->DoubleText(); + } else { + camera.K(0,2) = resolutions.back().width*REAL(0.5); + camera.K(1,2) = resolutions.back().height*REAL(0.5); + } + if ((elem=photogroup->FirstChildElement("AspectRatio")) != NULL) + camera.K(1,1) *= elem->DoubleText(); + if ((elem=photogroup->FirstChildElement("Skew")) != NULL) + camera.K(0,1) = elem->DoubleText(); + camera.K = camera.GetScaledK(REAL(1)/resolutionScale); + // parse distortion parameters + DistCoeff& dc = pltDistCoeffs.AddEmpty().AddEmpty(); { + const tinyxml2::XMLElement* distortion=photogroup->FirstChildElement("Distortion"); + if (distortion) { + if ((elem=distortion->FirstChildElement("K1")) != NULL) + dc.k1 = elem->DoubleText(); + if ((elem=distortion->FirstChildElement("K2")) != NULL) + dc.k2 = elem->DoubleText(); + if ((elem=distortion->FirstChildElement("K3")) != NULL) + dc.k3 = elem->DoubleText(); + if ((elem=distortion->FirstChildElement("P1")) != NULL) + dc.p2 = elem->DoubleText(); + if ((elem=distortion->FirstChildElement("P2")) != NULL) + dc.p1 = elem->DoubleText(); + } + } + ++nCameras; + for (const tinyxml2::XMLElement* photo=photogroup->FirstChildElement("Photo"); photo!=NULL; photo=photo->NextSiblingElement()) { + const IIndex idxImage = scene.images.size(); + Image& imageData = scene.images.AddEmpty(); + imageData.platformID = platformID; + imageData.cameraID = 0; // only one camera per platform supported by this format + imageData.poseID = NO_ID; + imageData.ID = photo->FirstChildElement("Id")->UnsignedText(); + imageData.name = photo->FirstChildElement("ImagePath")->GetText(); + Util::ensureUnifySlash(imageData.name); + imageData.name = MAKE_PATH_FULL(strPath, imageData.name); + mapImageID.emplace(imageData.ID, idxImage); + // set image resolution + const cv::Size& resolution = resolutions[imageData.platformID]; + imageData.width = resolution.width; + imageData.height = resolution.height; + imageData.scale = 1; + // set camera pose + const tinyxml2::XMLElement* photoPose = photo->FirstChildElement("Pose"); + if (photoPose == NULL) + continue; + if ((elem=photoPose->FirstChildElement("Rotation")) == NULL) + continue; + imageData.poseID = platform.poses.size(); + Platform::Pose& pose = platform.poses.AddEmpty(); + pose.R = Matrix3x3( + elem->FirstChildElement("M_00")->DoubleText(), + elem->FirstChildElement("M_01")->DoubleText(), + elem->FirstChildElement("M_02")->DoubleText(), + elem->FirstChildElement("M_10")->DoubleText(), + elem->FirstChildElement("M_11")->DoubleText(), + elem->FirstChildElement("M_12")->DoubleText(), + elem->FirstChildElement("M_20")->DoubleText(), + elem->FirstChildElement("M_21")->DoubleText(), + elem->FirstChildElement("M_22")->DoubleText()); + if ((elem=photoPose->FirstChildElement("Center")) == NULL) + continue; + pose.C = Point3( + elem->FirstChildElement("x")->DoubleText(), + elem->FirstChildElement("y")->DoubleText(), + elem->FirstChildElement("z")->DoubleText()); + imageData.camera = platform.GetCamera(imageData.cameraID, imageData.poseID); + // set depth stats + if ((elem=photo->FirstChildElement("MedianDepth")) != NULL) + imageData.avgDepth = (float)elem->DoubleText(); + else if (photo->FirstChildElement("NearDepth") != NULL && photo->FirstChildElement("FarDepth") != NULL) + imageData.avgDepth = (float)((photo->FirstChildElement("NearDepth")->DoubleText() + photo->FirstChildElement("FarDepth")->DoubleText())/2); + else + imageData.avgDepth = 0; + ++nPoses; + } + } + if (scene.images.size() < 2) + return false; + scene.nCalibratedImages = (unsigned)nPoses; + // transform poses to a local coordinate system + const bool bLocalCoords(document->FirstChildElement("SRSId") == NULL || + ((elem=blocksExchange->FirstChildElement("SpatialReferenceSystems")) != NULL && (elem=elem->FirstChildElement("SRS")) != NULL && (elem=elem->FirstChildElement("Name")) != NULL && _tcsncmp(elem->GetText(), "Local Coordinates", 17) == 0)); + Point3 center = Point3::ZERO; + if (!bLocalCoords) { + for (const Image& imageData : scene.images) + center += imageData.camera.C; + center /= scene.images.size(); + for (Platform& platform : scene.platforms) + for (Platform::Pose& pose : platform.poses) + pose.C -= center; + } + // try to read also the sparse point-cloud + const tinyxml2::XMLElement* tiepoints = document->FirstChildElement("TiePoints"); + if (tiepoints == NULL) + return true; + for (const tinyxml2::XMLElement* tiepoint=tiepoints->FirstChildElement(); tiepoint!=NULL; tiepoint=tiepoint->NextSiblingElement()) { + if ((elem=tiepoint->FirstChildElement("Position")) == NULL) + continue; + scene.pointcloud.points.emplace_back( + (float)elem->FirstChildElement("x")->DoubleText(), + (float)elem->FirstChildElement("y")->DoubleText(), + (float)elem->FirstChildElement("z")->DoubleText()); + if (!bLocalCoords) + scene.pointcloud.points.back() -= Cast(center); + if ((elem=tiepoint->FirstChildElement("Color")) != NULL) + scene.pointcloud.colors.emplace_back( + (uint8_t)CLAMP(elem->FirstChildElement("Red")->DoubleText()*255, 0.0, 255.0), + (uint8_t)CLAMP(elem->FirstChildElement("Green")->DoubleText()*255, 0.0, 255.0), + (uint8_t)CLAMP(elem->FirstChildElement("Blue")->DoubleText()*255, 0.0, 255.0)); + PointCloud::ViewArr views; + for (const tinyxml2::XMLElement* view=tiepoint->FirstChildElement("Measurement"); view!=NULL; view=view->NextSiblingElement()) + views.emplace_back(mapImageID.at(view->FirstChildElement("PhotoId")->UnsignedText())); + scene.pointcloud.pointViews.emplace_back(std::move(views)); + } + return true; +} + +// parse scene stored either in Metashape images list format or ContextCapture BlocksExchange format +bool ParseSceneXML(Scene& scene, PlatformDistCoeffs& pltDistCoeffs, size_t& nCameras, size_t& nPoses) +{ + // parse XML file + const String strInputFileName(MAKE_PATH_SAFE(OPT::strInputFileName)); + tinyxml2::XMLDocument doc; { + ISTREAMPTR pStream(new File(strInputFileName, File::READ, File::OPEN)); + if (!((File*)(ISTREAM*)pStream)->isOpen()) { + VERBOSE("error: failed opening the input scene file"); + return false; + } + const size_t nLen(pStream->getSize()); + String str; str.resize(nLen); + pStream->read(&str[0], nLen); + doc.Parse(str.c_str(), nLen); + } + if (doc.ErrorID() != tinyxml2::XML_SUCCESS) { + VERBOSE("error: invalid XML file"); + return false; + } + // parse scene + if (doc.FirstChildElement("BlocksExchange") == NULL) + return ParseImageListXML(doc, scene, pltDistCoeffs, nCameras, nPoses); + return ParseBlocksExchangeXML(doc, scene, pltDistCoeffs, nCameras, nPoses); +} + +// undistort image using Brown's model +bool UndistortBrown(Image& imageData, uint32_t ID, const DistCoeff& dc, const String& pathData) +{ + // do we need to undistort? + if (!dc.HasDistortion()) + return true; + + // load image pixels + if (!imageData.ReloadImage()) + return false; + + // initialize intrinsics + const cv::Vec& distCoeffs = *reinterpret_cast*>(dc.coeff); + const KMatrix prevK(imageData.camera.GetK(imageData.width, imageData.height)); + #if 1 + const KMatrix& K(prevK); + #else + const KMatrix K(cv::getOptimalNewCameraMatrix(prevK, distCoeffs, imageData.size(), 0.0, cv::Size(), NULL, true)); + ASSERT(K(0,2) == Camera::ComposeK(prevK(0,0), prevK(1,1), imageData.width(), imageData.height())(0,2)); + ASSERT(K(1,2) == Camera::ComposeK(prevK(0,0), prevK(1,1), imageData.width(), imageData.height())(1,2)); + if (K.IsEqual(prevK)) { + int i(0); + while (distCoeffs(i++) == 0.0) { + if (i == 8) + return true; // nothing to do + } + } + #endif + + // undistort image + Image8U3 imgUndist; + cv::undistort(imageData.image, imgUndist, prevK, distCoeffs, K); + imageData.ReleaseImage(); + + // save undistorted image + imageData.image = imgUndist; + imageData.name = pathData + String::FormatString(_T("%05u.jpg"), ID); + Util::ensureFolder(imageData.name); + return imageData.image.Save(imageData.name); +} + +// project all points in this image and keep those looking at the camera and are most in front +void AssignPoints(const Image& imageData, uint32_t ID, PointCloud& pointcloud) +{ + ASSERT(pointcloud.IsValid()); + const int CHalfSize(1); + const int FHalfSize(5); + const Depth thCloseDepth(0.1f); + + // sort points by depth + IndexScoreArr points(0, pointcloud.points.size()); + FOREACH(p, pointcloud.points) { + const PointCloud::Point& X(pointcloud.points[p]); + const float d((float)imageData.camera.PointDepth(X)); + if (d <= 0) + continue; + points.emplace_back((uint32_t)p, d); + } + points.Sort(); + + // project all points to this view + DepthMap depthMap(imageData.GetSize()); + TImage pointMap(imageData.GetSize()); + depthMap.fill(FLT_MAX); + pointMap.memset((uint8_t)NO_ID); + RFOREACHPTR(pPD, points) { + const Point3 X(pointcloud.points[pPD->idx]); + const Point3f Xc(imageData.camera.TransformPointW2C(X)); + // (also the view to point vector cause the face is in camera view space) + // point skip already in the previous step if the (cos) angle between + // the view to point vector and the view direction is negative + ASSERT(Xc.z > 0); + // skip point if the (cos) angle between + // its normal and the point to view vector is negative + if (!pointcloud.normals.empty() && Xc.dot(pointcloud.normals[pPD->idx]) > 0) + continue; + const Point2f x(imageData.camera.TransformPointC2I(Xc)); + const ImageRef ir(ROUND2INT(x)); + if (!depthMap.isInside(ir)) + continue; + // skip point if the there is a very near by point closer + for (int i=-CHalfSize; i<=CHalfSize; ++i) { + const int rw(ir.y+i); + for (int j=-CHalfSize; j<=CHalfSize; ++j) { + const int cw(ir.x+j); + if (!depthMap.isInside(ImageRef(cw,rw))) + continue; + if (depthMap(rw,cw) < Xc.z) + goto NEXT_POINT; + } + } + // skip the point if there is a near by point much closer + for (int i=-FHalfSize; i<=FHalfSize; ++i) { + const int rw(ir.y+i); + for (int j=-FHalfSize; j<=FHalfSize; ++j) { + const int cw(ir.x+j); + if (!depthMap.isInside(ImageRef(cw,rw))) + continue; + const Depth depth(depthMap(rw,cw)); + if (depth < Xc.z && !IsDepthSimilar(depth, Xc.z, thCloseDepth)) + goto NEXT_POINT; + } + } + // store this point + depthMap(ir) = Xc.z; + pointMap(ir) = pPD->idx; + NEXT_POINT:; + } + + // add all points viewed by this camera + const int HalfSize(1); + const int RowsEnd(pointMap.rows-HalfSize); + const int ColsEnd(pointMap.cols-HalfSize); + unsigned nNumPoints(0); + #ifdef _USE_OPENMP + #pragma omp critical + #endif + for (int r=HalfSize; r at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application application; + if (!application.Initialize(argc, argv)) + return EXIT_FAILURE; + + TD_TIMER_START(); + + Scene scene(OPT::nMaxThreads); + + // convert data from Metashape format to OpenMVS + PlatformDistCoeffs pltDistCoeffs; + size_t nCameras(0), nPoses(0); + if (!ParseSceneXML(scene, pltDistCoeffs, nCameras, nPoses)) + return EXIT_FAILURE; + + // read the 3D point-cloud if available + if (!OPT::strPointsFileName.empty() && !scene.pointcloud.Load(MAKE_PATH_SAFE(OPT::strPointsFileName))) + return EXIT_FAILURE; + const bool bAssignPoints(!scene.pointcloud.IsEmpty() && !scene.pointcloud.IsValid()); + if (bAssignPoints) + scene.pointcloud.pointViews.resize(scene.pointcloud.GetSize()); + + // undistort images + const String pathData(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strOutputImageFolder)); + Util::Progress progress(_T("Processed images"), scene.images.size()); + GET_LOGCONSOLE().Pause(); + #ifdef _USE_OPENMP + bool bAbort(false); + #pragma omp parallel for shared(bAbort) schedule(dynamic) + for (int ID=0; ID<(int)scene.images.size(); ++ID) { + #pragma omp flush (bAbort) + if (bAbort) + continue; + #else + FOREACH(ID, scene.images) { + #endif + ++progress; + Image& imageData = scene.images[ID]; + if (!imageData.IsValid()) + continue; + if (!UndistortBrown(imageData, ID, pltDistCoeffs[imageData.platformID][imageData.cameraID], pathData)) { + #ifdef _USE_OPENMP + bAbort = true; + #pragma omp flush (bAbort) + continue; + #else + return EXIT_FAILURE; + #endif + } + imageData.UpdateCamera(scene.platforms); + if (bAssignPoints) + AssignPoints(imageData, ID, scene.pointcloud); + } + GET_LOGCONSOLE().Play(); + #ifdef _USE_OPENMP + if (bAbort) + return EXIT_FAILURE; + #endif + progress.close(); + + if (scene.pointcloud.IsValid()) { + // filter invalid points + RFOREACH(i, scene.pointcloud.points) + if (scene.pointcloud.pointViews[i].size() < 2) + scene.pointcloud.RemovePoint(i); + // compute average scene depth per image + if (!std::any_of(scene.images.begin(), scene.images.end(), [](const Image& imageData) { return imageData.avgDepth > 0; })) { + std::vector avgDepths(scene.images.size(), 0.f); + std::vector numDepths(scene.images.size(), 0u); + FOREACH(idxPoint, scene.pointcloud.points) { + const Point3 X(scene.pointcloud.points[idxPoint]); + for (const PointCloud::View& idxImage: scene.pointcloud.pointViews[idxPoint]) { + const Image& imageData = scene.images[idxImage]; + const float depth((float)imageData.camera.PointDepth(X)); + if (depth > 0) { + avgDepths[idxImage] += depth; + ++numDepths[idxImage]; + } + } + } + FOREACH(idxImage, scene.images) { + Image& imageData = scene.images[idxImage]; + if (numDepths[idxImage] > 0) + imageData.avgDepth = avgDepths[idxImage] / numDepths[idxImage]; + } + } + } + + // print average scene depth per image stats + MeanStdMinMax acc; + for (const Image& imageData: scene.images) + if (imageData.avgDepth > 0) + acc.Update(imageData.avgDepth); + + // write OpenMVS input data + scene.Save(MAKE_PATH_SAFE(OPT::strOutputFileName), (ARCHIVE_TYPE)OPT::nArchiveType); + + VERBOSE("Exported data: %u platforms, %u cameras, %u poses, %u images, %u vertices, %g min / %g mean (%g std) / %g max average scene depth per image (%s)", + scene.platforms.size(), nCameras, nPoses, scene.images.size(), scene.pointcloud.GetSize(), + acc.minVal, acc.GetMean(), acc.GetStdDev(), acc.maxVal, + TD_TIMER_GET_FMT().c_str()); + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/InterfaceOpenMVG/CMakeLists.txt b/apps/InterfaceOpenMVG/CMakeLists.txt new file mode 100644 index 0000000..6377497 --- /dev/null +++ b/apps/InterfaceOpenMVG/CMakeLists.txt @@ -0,0 +1,23 @@ +FIND_PACKAGE(OpenMVG QUIET) +IF (OPENMVG_FOUND) + INCLUDE_DIRECTORIES(${OPENMVG_INCLUDE_DIRS}) + add_definitions(-D_USE_OPENMVG) + set(LIBS_DEPEND "MVS;${OPENMVG_LIBRARIES}") +ELSE() + set(LIBS_DEPEND "MVS") + MESSAGE("OPENMVG_NOT FOUND : OpenMVG importer with JSON support will not be build") +ENDIF() + +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(InterfaceOpenMVG "Apps" "${cxx_default}" "${LIBS_DEPEND};${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS InterfaceOpenMVG + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/InterfaceOpenMVG/InterfaceOpenMVG.cpp b/apps/InterfaceOpenMVG/InterfaceOpenMVG.cpp new file mode 100644 index 0000000..9361af0 --- /dev/null +++ b/apps/InterfaceOpenMVG/InterfaceOpenMVG.cpp @@ -0,0 +1,755 @@ +/* + * InterfaceOpenMVG.cpp + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * Pierre MOULON + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#include +#ifdef _USE_OPENMVG +#undef D2R +#undef R2D +#include +#include +#include +#endif + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("InterfaceOpenMVG") +#define MVS_EXT _T(".mvs") +#define MVG_EXT _T(".baf") +#define MVG2_EXT _T(".json") +#define MVG3_EXT _T(".bin") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace openMVS { +namespace MVS_IO { + +typedef REAL RealT; +typedef Eigen::Matrix Mat33; +typedef Eigen::Matrix Vec3; + +// Structure to model the pinhole camera projection model +struct Camera +{ + Mat33 K; // camera's normalized intrinsics matrix +}; +typedef std::vector vec_Camera; + +// structure describing a pose along the trajectory of a platform +struct Pose { + Mat33 R; // pose's rotation matrix + Vec3 C; // pose's translation vector +}; +typedef std::vector vec_Pose; + +// structure describing an image +struct Image { + uint32_t id_camera; // ID of the associated camera on the associated platform + uint32_t id_pose; // ID of the pose of the associated platform + std::string name; // image file name +}; +typedef std::vector vec_Image; + +// structure describing a 3D point +struct Vertex { + + typedef std::vector vec_View; + + Vec3 X; // 3D point position + vec_View views; // view visibility for this 3D feature +}; +typedef std::vector vec_Vertex; + +struct SfM_Scene +{ + vec_Pose poses; // array of poses + vec_Camera cameras; // array of cameras + vec_Image images; // array of images + vec_Vertex vertices; // array of reconstructed 3D points +}; + + +bool ImportScene(const std::string& sList_filename, const std::string& sBaf_filename, SfM_Scene& sceneBAF) +{ + LOG_OUT() << "Reading:\n" + << sList_filename << "\n" + << sBaf_filename << std::endl; + + // Read view list file (view filename, id_intrinsic, id_pose) + // Must be read first, since it allow to establish the link between the ViewId and the camera/poses ids. + std::map< std::pair, uint32_t > map_cam_pose_toViewId; + { + std::ifstream file(sList_filename.c_str()); + if (!file.good()) { + VERBOSE("error: unable to open file '%s'", sList_filename.c_str()); + return false; + } + Image image; + uint32_t count = 0; + while (file >> image.name >> image.id_camera >> image.id_pose) { + sceneBAF.images.push_back(image); + map_cam_pose_toViewId[std::make_pair(image.id_camera, image.id_pose)] = count++; + LOG_OUT() << image.name << ' ' << image.id_camera << ' ' << image.id_pose << std::endl; + } + } + + // Read BAF file + { + std::ifstream file(sBaf_filename.c_str()); + if (!file.good()) { + VERBOSE("error: unable to open file '%s'", sBaf_filename.c_str()); + return false; + } + + uint32_t num_intrinsics, num_poses, num_points; + + // Read header + file >> num_intrinsics; + file >> num_poses; + file >> num_points; + + LOG_OUT() << "Reading BAF file with:\n" + << " num_intrinsics: " << num_intrinsics << "\n" + << " num_poses: " << num_poses << "\n" + << " num_points: " << num_points << "\n"; + + // Read the intrinsics (only support reading Pinhole Radial 3). + { + for (uint32_t i = 0; i < num_intrinsics; ++i) { + double focal, ppx, ppy, k1, k2, k3; + file >> focal >> ppx >> ppy >> k1 >> k2 >> k3; + Camera cam; + cam.K << + focal, 0, ppx, + 0, focal, ppy, + 0, 0, 1; + LOG_OUT() << "\n" << cam.K << std::endl; + sceneBAF.cameras.push_back(cam); + } + } + + // Read poses + { + for (uint32_t i = 0; i < num_poses; ++i) { + Pose pose; + for (int r = 0; r < 3; ++r) { + for (int c = 0; c < 3; ++c) { + file >> pose.R(r,c); + } + } + file >> pose.C[0] >> pose.C[1] >> pose.C[2]; + #ifndef _RELEASE + LOG_OUT() << "\n" << pose.R << "\n\n" << pose.C.transpose() << std::endl; + #endif + sceneBAF.poses.push_back(pose); + } + } + + // Read structure and visibility + { + #ifdef _RELEASE + Util::Progress progress(_T("Processed points"), num_points); + #endif + for (uint32_t i = 0; i < num_points; ++i) { + Vertex vertex; + file >> vertex.X[0] >> vertex.X[1] >> vertex.X[2]; + uint32_t num_observations_for_point = 0; + file >> num_observations_for_point; + for (uint32_t j = 0; j < num_observations_for_point; ++j) { + uint32_t id_intrinsics, id_pose; + double x, y; + file >> id_intrinsics >> id_pose >> x >> y; + #ifndef _RELEASE + LOG_OUT() << "observation:" + << " " << id_intrinsics + << " " << id_pose + << " " << x << " " << y << std::endl; + #endif + const auto itIntrPose(map_cam_pose_toViewId.find(std::make_pair(id_intrinsics, id_pose))); + if (itIntrPose == map_cam_pose_toViewId.end()) { + LOG_OUT() << "error: intrinsics-pose pair not existing" << std::endl; + continue; + } + const uint32_t id_view(itIntrPose->second); + vertex.views.push_back(id_view); + } + sceneBAF.vertices.push_back(vertex); + #ifdef _RELEASE + progress.display(i); + #endif + } + #ifdef _RELEASE + progress.close(); + #endif + } + } + return true; +} + +bool ExportScene(const std::string& sList_filename, const std::string& sBaf_filename, const SfM_Scene& sceneBAF) +{ + LOG_OUT() << "Writing:\n" + << sList_filename << "\n" + << sBaf_filename << std::endl; + + // Write view list file (view filename, id_intrinsic, id_pose) + { + std::ofstream file(sList_filename.c_str()); + if (!file.good()) { + VERBOSE("error: unable to open file '%s'", sList_filename.c_str()); + return false; + } + for (uint32_t i=0; i(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_DEFAULT), "project archive type: 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description config("Main options"); + config.add_options() + ("images-list-file,l", boost::program_options::value(&OPT::strListFileName), "input filename containing image list") + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input filename containing camera poses and image list") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the mesh") + ("output-image-folder", boost::program_options::value(&OPT::strOutputImageFolder)->default_value("undistorted_images"), "output folder to store undistorted images") + ("normalize,f", boost::program_options::value(&OPT::bNormalizeIntrinsics)->default_value(true), "normalize intrinsics while exporting to OpenMVS format") + ; + + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(config); + + boost::program_options::options_description config_file_options; + config_file_options.add(config); + + boost::program_options::positional_options_description p; + p.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(p).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + INIT_WORKING_FOLDER; + // parse configuration file + std::ifstream ifs(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (ifs) { + boost::program_options::store(parse_config_file(ifs, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // initialize the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-")+Util::getUniqueName(0)+_T(".log")).c_str()); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // validate input + Util::ensureValidPath(OPT::strListFileName); + Util::ensureUnifySlash(OPT::strListFileName); + Util::ensureValidPath(OPT::strInputFileName); + Util::ensureUnifySlash(OPT::strInputFileName); + Util::ensureUnifySlash(OPT::strOutputImageFolder); + Util::ensureFolderSlash(OPT::strOutputImageFolder); + const String strInputFileNameExt(Util::getFileExt(OPT::strInputFileName).ToLower()); + OPT::bOpenMVS2OpenMVG = (strInputFileNameExt == MVS_FILE_EXTENSION); + #ifdef _USE_OPENMVG + OPT::bOpenMVGjson = (strInputFileNameExt == MVG2_EXT || strInputFileNameExt == MVG3_EXT); + const bool bInvalidCommand(OPT::strInputFileName.IsEmpty() || (OPT::strListFileName.IsEmpty() && !OPT::bOpenMVGjson && !OPT::bOpenMVS2OpenMVG)); + #else + const bool bInvalidCommand(OPT::strInputFileName.IsEmpty() || (OPT::strListFileName.IsEmpty() && !OPT::bOpenMVS2OpenMVG)); + #endif + if (OPT::vm.count("help") || bInvalidCommand) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(config); + GET_LOG() << visible; + #ifndef _USE_OPENMVG + GET_LOG() << "\nWARNING: Only " MVG_EXT " files supported! In order to import " MVG2_EXT " or " MVG3_EXT " files use the converter included in OpenMVG, or link OpenMVS to the latest version of OpenMVG during compile time.\n"; + #endif + } + if (bInvalidCommand) + return false; + + // initialize optional options + Util::ensureValidPath(OPT::strOutputFileName); + Util::ensureUnifySlash(OPT::strOutputFileName); + if (OPT::bOpenMVS2OpenMVG) { + if (OPT::strOutputFileName.IsEmpty()) + OPT::strOutputFileName = Util::getFileFullName(OPT::strInputFileName); + } else { + if (OPT::strOutputFileName.IsEmpty()) + OPT::strOutputFileName = Util::getFileFullName(OPT::strInputFileName) + MVS_FILE_EXTENSION; + } + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::Finalize() +{ + MVS::Finalize(); + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +} // unnamed namespace + +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application application; + if (!application.Initialize(argc, argv)) + return EXIT_FAILURE; + + TD_TIMER_START(); + + if (OPT::bOpenMVS2OpenMVG) { + // read OpenMVS input data + MVS::Scene scene(OPT::nMaxThreads); + if (!scene.Load(MAKE_PATH_SAFE(OPT::strInputFileName))) + return EXIT_FAILURE; + + // convert data from OpenMVS to OpenMVG + openMVS::MVS_IO::SfM_Scene sceneBAF; + FOREACH(p, scene.platforms) { + const MVS::Platform& platform = scene.platforms[p]; + if (platform.cameras.GetSize() != 1) { + LOG("error: unsupported scene structure"); + return EXIT_FAILURE; + } + const MVS::Platform::Camera& camera = platform.cameras[0]; + openMVS::MVS_IO::Camera cameraBAF; + cameraBAF.K = camera.K; + sceneBAF.cameras.push_back(cameraBAF); + } + FOREACH(i, scene.images) { + const MVS::Image& image = scene.images[i]; + const MVS::Platform& platform = scene.platforms[image.platformID]; + const MVS::Platform::Pose& pose = platform.poses[image.poseID]; + openMVS::MVS_IO::Image imageBAF; + imageBAF.name = image.name; + imageBAF.name = MAKE_PATH_REL(WORKING_FOLDER_FULL, imageBAF.name); + imageBAF.id_camera = image.platformID; + imageBAF.id_pose = (uint32_t)sceneBAF.poses.size(); + sceneBAF.images.push_back(imageBAF); + openMVS::MVS_IO::Pose poseBAF; + poseBAF.R = pose.R; + poseBAF.C = pose.C; + sceneBAF.poses.push_back(poseBAF); + } + sceneBAF.vertices.reserve(scene.pointcloud.points.GetSize()); + FOREACH(p, scene.pointcloud.points) { + const MVS::PointCloud::Point& point = scene.pointcloud.points[p]; + openMVS::MVS_IO::Vertex vertexBAF; + vertexBAF.X = ((const MVS::PointCloud::Point::EVec)point).cast(); + const MVS::PointCloud::ViewArr& views = scene.pointcloud.pointViews[p]; + FOREACH(v, views) { + unsigned viewBAF = views[(uint32_t)v]; + vertexBAF.views.push_back(viewBAF); + } + sceneBAF.vertices.push_back(vertexBAF); + } + + // write OpenMVG input data + const String strOutputFileNameMVG(OPT::strOutputFileName + MVG_EXT); + openMVS::MVS_IO::ExportScene(MAKE_PATH_SAFE(OPT::strListFileName), MAKE_PATH_SAFE(strOutputFileNameMVG), sceneBAF); + + VERBOSE("Input data exported: %u cameras & %u poses & %u images & %u vertices (%s)", sceneBAF.cameras.size(), sceneBAF.poses.size(), sceneBAF.images.size(), sceneBAF.vertices.size(), TD_TIMER_GET_FMT().c_str()); + } else { + // convert data from OpenMVG to OpenMVS + MVS::Scene scene(OPT::nMaxThreads); + size_t nCameras(0), nPoses(0); + + #ifdef _USE_OPENMVG + if (OPT::bOpenMVGjson) { + // read OpenMVG input data from a JSON file + using namespace openMVG::sfm; + using namespace openMVG::cameras; + SfM_Data sfm_data; + const String strSfM_Data_Filename(MAKE_PATH_SAFE(OPT::strInputFileName)); + if (!Load(sfm_data, strSfM_Data_Filename, ESfM_Data(ALL))) { + VERBOSE("error: the input SfM_Data file '%s' cannot be read", strSfM_Data_Filename.c_str()); + return EXIT_FAILURE; + } + VERBOSE("Imported data: %u cameras, %u poses, %u images, %u vertices", + sfm_data.GetIntrinsics().size(), + sfm_data.GetPoses().size(), + sfm_data.GetViews().size(), + sfm_data.GetLandmarks().size()); + + // OpenMVG can have not contiguous index, use a map to create the required OpenMVS contiguous ID index + std::map map_intrinsic, map_view; + + // define a platform with all the intrinsic group + nCameras = sfm_data.GetIntrinsics().size(); + for (const auto& intrinsic: sfm_data.GetIntrinsics()) { + if (isPinhole(intrinsic.second.get()->getType())) { + const Pinhole_Intrinsic * cam = dynamic_cast(intrinsic.second.get()); + if (map_intrinsic.count(intrinsic.first) == 0) + map_intrinsic.insert(std::make_pair(intrinsic.first, scene.platforms.GetSize())); + MVS::Platform& platform = scene.platforms.AddEmpty(); + // add the camera + MVS::Platform::Camera& camera = platform.cameras.AddEmpty(); + camera.K = cam->K(); + // sub-pose + camera.R = RMatrix::IDENTITY; + camera.C = CMatrix::ZERO; + } + } + + // define images & poses + const uint32_t nViews((uint32_t)sfm_data.GetViews().size()); + scene.images.Reserve(nViews); + scene.nCalibratedImages = 0; + Util::Progress progress(_T("Processed images"), nViews); + GET_LOGCONSOLE().Pause(); + #ifdef _USE_OPENMP + const std::vector views(sfm_data.GetViews().cbegin(), sfm_data.GetViews().cend()); + #pragma omp parallel for schedule(dynamic) + for (int i=0; i<(int)nViews; ++i) { + const Views::value_type& view = views[i]; + #pragma omp critical + map_view[view.first] = scene.images.GetSize(); + #else + for (const auto& view : sfm_data.GetViews()) { + map_view[view.first] = scene.images.GetSize(); + #endif + MVS::Image& image = scene.images.AddEmpty(); + image.name = view.second->s_Img_path; + Util::ensureUnifySlash(image.name); + Util::strTrim(image.name, PATH_SEPARATOR_STR); + String pathRoot(sfm_data.s_root_path); Util::ensureFolderSlash(pathRoot); + const String srcImage(MAKE_PATH_FULL(WORKING_FOLDER_FULL, pathRoot+image.name)); + image.name = MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strOutputImageFolder+image.name); + Util::ensureDirectory(image.name); + image.ID = static_cast(view.first); + image.platformID = map_intrinsic.at(view.second->id_intrinsic); + MVS::Platform& platform = scene.platforms[image.platformID]; + image.cameraID = 0; + if (sfm_data.IsPoseAndIntrinsicDefined(view.second.get()) && + File::access(srcImage)) { + MVS::Platform::Pose* pPose; + #ifdef _USE_OPENMP + #pragma omp critical + #endif + { + image.poseID = platform.poses.GetSize(); + pPose = &platform.poses.AddEmpty(); + ++scene.nCalibratedImages; + } + const openMVG::geometry::Pose3 poseMVG(sfm_data.GetPoseOrDie(view.second.get())); + pPose->R = poseMVG.rotation(); + pPose->C = poseMVG.center(); + // export undistorted images + const openMVG::cameras::IntrinsicBase * cam = sfm_data.GetIntrinsics().at(view.second->id_intrinsic).get(); + if (cam->have_disto()) { + // undistort and save the image + openMVG::image::Image imageRGB, imageRGB_ud; + openMVG::image::ReadImage(srcImage, &imageRGB); + openMVG::cameras::UndistortImage(imageRGB, cam, imageRGB_ud, openMVG::image::BLACK); + openMVG::image::WriteImage(image.name, imageRGB_ud); + } else { + // no distortion, copy the image + File::copyFile(srcImage, image.name); + } + ++nPoses; + } else { + // image have not valid pose, so set an undefined pose + image.poseID = NO_ID; + // just copy the image + File::copyFile(srcImage, image.name); + DEBUG_EXTRA("warning: uncalibrated image '%s'", view.second->s_Img_path.c_str()); + } + ++progress; + } + GET_LOGCONSOLE().Play(); + progress.close(); + + // define structure + scene.pointcloud.points.Reserve(sfm_data.GetLandmarks().size()); + scene.pointcloud.pointViews.Reserve(sfm_data.GetLandmarks().size()); + for (const auto& vertex: sfm_data.GetLandmarks()) { + const Landmark & landmark = vertex.second; + MVS::PointCloud::ViewArr& views = scene.pointcloud.pointViews.AddEmpty(); + for (const auto& observation: landmark.obs) { + const auto it(map_view.find(observation.first)); + if (it != map_view.end()) + views.InsertSort(it->second); + } + if (views.GetSize() < 2) { + scene.pointcloud.pointViews.RemoveLast(); + continue; + } + MVS::PointCloud::Point& point = scene.pointcloud.points.AddEmpty(); + point = landmark.X.cast(); + } + } else + #endif + { + // read OpenMVG input data from BAF file + openMVS::MVS_IO::SfM_Scene sceneBAF; + if (!openMVS::MVS_IO::ImportScene(MAKE_PATH_SAFE(OPT::strListFileName), MAKE_PATH_SAFE(OPT::strInputFileName), sceneBAF)) + return EXIT_FAILURE; + + // convert data from OpenMVG to OpenMVS + nCameras = sceneBAF.cameras.size(); + scene.platforms.Reserve((uint32_t)nCameras); + for (const auto& cameraBAF: sceneBAF.cameras) { + MVS::Platform& platform = scene.platforms.AddEmpty(); + MVS::Platform::Camera& camera = platform.cameras.AddEmpty(); + camera.K = cameraBAF.K; + camera.R = RMatrix::IDENTITY; + camera.C = CMatrix::ZERO; + } + nPoses = sceneBAF.images.size(); + scene.images.Reserve((uint32_t)nPoses); + for (const auto& imageBAF: sceneBAF.images) { + openMVS::MVS_IO::Pose& poseBAF = sceneBAF.poses[imageBAF.id_pose]; + MVS::Image& image = scene.images.AddEmpty(); + image.name = imageBAF.name; + Util::ensureUnifySlash(image.name); + image.name = MAKE_PATH_FULL(WORKING_FOLDER_FULL, image.name); + image.ID = imageBAF.id_camera; + image.platformID = imageBAF.id_camera; + MVS::Platform& platform = scene.platforms[image.platformID]; + image.cameraID = 0; + image.poseID = platform.poses.GetSize(); + MVS::Platform::Pose& pose = platform.poses.AddEmpty(); + pose.R = poseBAF.R; + pose.C = poseBAF.C; + } + scene.pointcloud.points.Reserve(sceneBAF.vertices.size()); + scene.pointcloud.pointViews.Reserve(sceneBAF.vertices.size()); + for (const auto& vertexBAF: sceneBAF.vertices) { + MVS::PointCloud::Point& point = scene.pointcloud.points.AddEmpty(); + point = vertexBAF.X.cast(); + MVS::PointCloud::ViewArr& views = scene.pointcloud.pointViews.AddEmpty(); + for (const auto& viewBAF: vertexBAF.views) + views.InsertSort(viewBAF); + } + } + + // read images meta-data + FOREACHPTR(pImage, scene.images) { + if (!pImage->ReloadImage(0, false)) + LOG("error: can not read image %s", pImage->name.c_str()); + } + if (OPT::bNormalizeIntrinsics) { + // normalize camera intrinsics + FOREACH(p, scene.platforms) { + MVS::Platform& platform = scene.platforms[p]; + FOREACH(c, platform.cameras) { + MVS::Platform::Camera& camera = platform.cameras[c]; + // find one image using this camera + MVS::Image* pImage(NULL); + FOREACHPTR(pImg, scene.images) { + if (pImg->platformID == p && pImg->cameraID == c && pImg->poseID != NO_ID) { + pImage = pImg; + break; + } + } + if (pImage == NULL) { + LOG("error: no image using camera %u of platform %u", c, p); + continue; + } + const REAL fScale(REAL(1)/MVS::Camera::GetNormalizationScale(pImage->width, pImage->height)); + camera.K(0,0) *= fScale; + camera.K(1,1) *= fScale; + camera.K(0,2) *= fScale; + camera.K(1,2) *= fScale; + } + } + } + + // write OpenMVS input data + scene.Save(MAKE_PATH_SAFE(OPT::strOutputFileName), (ARCHIVE_TYPE)OPT::nArchiveType); + + VERBOSE("Exported data: %u platforms, %u cameras, %u poses, %u images, %u vertices (%s)", + scene.platforms.GetSize(), nCameras, nPoses, scene.images.GetSize(), scene.pointcloud.GetSize(), + TD_TIMER_GET_FMT().c_str()); + } + + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/InterfacePolycam/CMakeLists.txt b/apps/InterfacePolycam/CMakeLists.txt new file mode 100644 index 0000000..de8fda1 --- /dev/null +++ b/apps/InterfacePolycam/CMakeLists.txt @@ -0,0 +1,13 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(InterfacePolycam "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS InterfacePolycam + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/InterfacePolycam/InterfacePolycam.cpp b/apps/InterfacePolycam/InterfacePolycam.cpp new file mode 100644 index 0000000..ccd16c5 --- /dev/null +++ b/apps/InterfacePolycam/InterfacePolycam.cpp @@ -0,0 +1,362 @@ +/* + * InterfacePolycam.cpp + * + * Copyright (c) 2014-2023 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#define JSON_NOEXCEPTION +#include "../../libs/IO/json.hpp" +#include + +using namespace MVS; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("InterfacePolycam") +#define MVS_FILE_EXTENSION _T(".mvs") +#define JSON_EXT _T(".json") +#define DEPTH_EXT _T(".png") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { +String strInputFileName; +String strOutputFileName; +unsigned nArchiveType; +int nProcessPriority; +unsigned nMaxThreads; +String strConfigFileName; +boost::program_options::variables_map vm; +} // namespace OPT + +class Application { +public: + Application() {} + ~Application() { Finalize(); } + + bool Initialize(size_t argc, LPCTSTR* argv); + void Finalize(); +}; // Application + +// initialize and parse the command line parameters +bool Application::Initialize(size_t argc, LPCTSTR* argv) +{ + // initialize log and console + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "imports SfM scene stored Polycam format") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: -1-interface, 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description config("Main options"); + config.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input folder containing Polycam camera poses, images and depth-maps") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the scene") + ; + + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(config); + + boost::program_options::options_description config_file_options; + config_file_options.add(config); + + boost::program_options::positional_options_description p; + p.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(p).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + INIT_WORKING_FOLDER; + // parse configuration file + std::ifstream ifs(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (ifs) { + boost::program_options::store(parse_config_file(ifs, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // initialize the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-")+Util::getUniqueName(0)+_T(".log")).c_str()); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // validate input + Util::ensureValidFolderPath(OPT::strInputFileName); + const bool bInvalidCommand(OPT::strInputFileName.empty()); + if (OPT::vm.count("help") || bInvalidCommand) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(config); + GET_LOG() << visible; + } + if (bInvalidCommand) + return false; + + // initialize optional options + Util::ensureValidFolderPath(OPT::strOutputFileName); + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = "scene" MVS_FILE_EXTENSION; + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::Finalize() +{ + MVS::Finalize(); + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +// parse image containing calibration, pose, and depth-map information +bool ParseImage(Scene& scene, const String& imagePath, const String& cameraPath, const String& depthPath, + const std::unordered_map& mapImageName) +{ + nlohmann::json data = nlohmann::json::parse(std::ifstream(cameraPath)); + if (data.empty()) + return false; + const cv::Size resolution(data["width"].get(), data["height"].get()); + // set platform + const IIndex platformID = scene.platforms.size(); + Platform& platform = scene.platforms.AddEmpty(); + Platform::Camera& camera = platform.cameras.AddEmpty(); + camera.K = KMatrix::IDENTITY; + camera.R = RMatrix::IDENTITY; + camera.C = CMatrix::ZERO; + camera.K(0,0) = data["fx"].get(); + camera.K(1,1) = data["fy"].get(); + camera.K(0,2) = data["cx"].get(); + camera.K(1,2) = data["cy"].get(); + // set image + const IIndex imageID = scene.images.size(); + Image& imageData = scene.images.AddEmpty(); + imageData.platformID = platformID; + imageData.cameraID = 0; // only one camera per platform supported by this format + imageData.poseID = NO_ID; + imageData.ID = imageID; + imageData.name = imagePath; + ASSERT(Util::isFullPath(imageData.name)); + // set image resolution + imageData.width = resolution.width; + imageData.height = resolution.height; + imageData.scale = 1; + // set camera pose + imageData.poseID = platform.poses.size(); + Platform::Pose& pose = platform.poses.AddEmpty(); + const Eigen::Matrix3d R_session_arkitcam{ + {data["t_00"].get(), data["t_01"].get(), data["t_02"].get()}, + {data["t_10"].get(), data["t_11"].get(), data["t_12"].get()}, + {data["t_20"].get(), data["t_21"].get(), data["t_22"].get()} + }; + const Eigen::Vector3d t_session_arkitcam{ + data["t_03"].get(), + data["t_13"].get(), + data["t_23"].get() + }; + const Eigen::Affine3d T_session_arkitcam{ + Eigen::Affine3d(Eigen::Translation3d(t_session_arkitcam)) * Eigen::Affine3d(Eigen::AngleAxisd(R_session_arkitcam)) + }; + const Eigen::Affine3d T_cam_arkitcam{ + Eigen::AngleAxisd(M_PI, Eigen::Vector3d::UnitX()) + }; + const Eigen::Matrix4d P{ + (T_cam_arkitcam * T_session_arkitcam.inverse()).matrix() + }; + pose.R = P.topLeftCorner<3, 3>().eval(); + pose.R.EnforceOrthogonality(); + const Point3d t = P.topRightCorner<3, 1>().eval(); + pose.C = pose.R.t() * (-t); + imageData.camera = platform.GetCamera(imageData.cameraID, imageData.poseID); + // set image neighbors if available + nlohmann::json::const_iterator itNeighbors = data.find("neighbors"); + if (itNeighbors != data.end()) { + const std::vector neighborTimestamps = itNeighbors->get>(); + for (uint64_t timestamp: neighborTimestamps) { + const String neighborName = std::to_string(timestamp); + const IIndex neighborID = mapImageName.at(neighborName); + if (neighborID != imageData.ID) + imageData.neighbors.emplace_back(ViewScore{neighborID, 0, 1.f, FD2R(15.f), 0.5f, 3.f}); + } + } + // load and convert depth-map + if (!depthPath.empty()) { + DepthMap depthMap; { + constexpr double depthScale{1000.0}; + const cv::Mat imgDepthMap = cv::imread(depthPath, cv::IMREAD_ANYDEPTH); + if (imgDepthMap.empty()) + return false; + imgDepthMap.convertTo(depthMap, CV_32FC1, 1.0/depthScale); + } + IIndexArr IDs = {imageData.ID}; + IDs.JoinFunctor(imageData.neighbors.size(), [&imageData](IIndex i) { + return imageData.neighbors[i].ID; + }); + double dMin, dMax; + cv::minMaxIdx(depthMap, &dMin, &dMax, NULL, NULL, depthMap > 0); + const NormalMap normalMap; + const ConfidenceMap confMap; + const ViewsMap viewsMap; + if (!ExportDepthDataRaw(MAKE_PATH(String::FormatString("depth%04u.dmap", imageData.ID)), + imageData.name, IDs, resolution, + camera.K, pose.R, pose.C, + (float)dMin, (float)dMax, + depthMap, normalMap, confMap, viewsMap)) + return false; + } + return true; +} + +// parse scene stored in Polycam format +bool ParseScene(Scene& scene, const String& scenePath) +{ + #if defined(_SUPPORT_CPP17) && (!defined(__GNUC__) || (__GNUC__ > 7)) + size_t numCorrectedFolders(0), numCorrectedDepthFolders(0), numFolders(0), numDepthFolders(0); + for (const auto& file: std::filesystem::directory_iterator(scenePath.c_str())) { + if (file.path().stem() == "corrected_cameras" || + file.path().stem() == "corrected_images") + ++numCorrectedFolders; + else if (file.path().stem() == "corrected_depth") + ++numCorrectedDepthFolders; + else if (file.path().stem() == "cameras" || + file.path().stem() == "images") + ++numFolders; + else if (file.path().stem() == "depth") + ++numDepthFolders; + } + if (numFolders != 2) { + VERBOSE("Invalid scene folder"); + return false; + } + if (numCorrectedFolders == 2) { + // corrected data + CLISTDEFIDX(String, IIndex) imagePaths; + for (const auto& file: std::filesystem::directory_iterator((scenePath + "corrected_images").c_str())) + imagePaths.emplace_back(file.path().string()); + VERBOSE("Parsing corrected data: %u...", imagePaths.size()); + std::unordered_map mapImageName; + mapImageName.reserve(imagePaths.size()); + for (String& imagePath: imagePaths) { + Util::ensureValidPath(imagePath); + mapImageName.emplace(Util::getFileName(imagePath), static_cast(mapImageName.size())); + } + for (const String& imagePath: imagePaths) { + const String imageName = Util::getFileName(imagePath); + const String cameraPath(scenePath + "corrected_cameras" + PATH_SEPARATOR_STR + imageName + JSON_EXT); + const String depthPath(numCorrectedDepthFolders ? scenePath + "corrected_depth" + PATH_SEPARATOR_STR + imageName + DEPTH_EXT : String()); + if (!ParseImage(scene, imagePath, cameraPath, depthPath, mapImageName)) + return false; + } + } else { + // raw data + CLISTDEFIDX(String, IIndex) imagePaths; + for (const auto& file: std::filesystem::directory_iterator((scenePath + "images").c_str())) + imagePaths.emplace_back(file.path().string()); + VERBOSE("Parsing raw data: %u...", imagePaths.size()); + std::unordered_map mapImageName; + mapImageName.reserve(imagePaths.size()); + for (String& imagePath: imagePaths) { + Util::ensureValidPath(imagePath); + mapImageName.emplace(Util::getFileName(imagePath), static_cast(mapImageName.size())); + } + for (const String& imagePath: imagePaths) { + const String imageName = Util::getFileName(imagePath); + const String cameraPath(scenePath + "cameras" + PATH_SEPARATOR_STR + imageName + JSON_EXT); + const String depthPath(numDepthFolders ? scenePath + "depth" + PATH_SEPARATOR_STR + imageName + DEPTH_EXT : String()); + if (!ParseImage(scene, imagePath, cameraPath, depthPath, mapImageName)) + return false; + } + } + return true; + #else + return false; + #endif +} + +} // unnamed namespace + +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application application; + if (!application.Initialize(argc, argv)) + return EXIT_FAILURE; + + TD_TIMER_START(); + + Scene scene(OPT::nMaxThreads); + + // convert data from Polycam format to OpenMVS + if (!ParseScene(scene, MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strInputFileName))) + return EXIT_FAILURE; + + // write OpenMVS input data + scene.Save(MAKE_PATH_SAFE(OPT::strOutputFileName), (ARCHIVE_TYPE)OPT::nArchiveType); + + VERBOSE("Exported data: %u platforms, %u cameras, %u poses, %u images (%s)", + scene.platforms.size(), scene.images.size(), scene.images.size(), scene.images.size(), + TD_TIMER_GET_FMT().c_str()); + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/InterfaceVisualSFM/CMakeLists.txt b/apps/InterfaceVisualSFM/CMakeLists.txt new file mode 100644 index 0000000..a1b16af --- /dev/null +++ b/apps/InterfaceVisualSFM/CMakeLists.txt @@ -0,0 +1,13 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(InterfaceVisualSFM "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS InterfaceVisualSFM + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/InterfaceVisualSFM/DataInterface.h b/apps/InterfaceVisualSFM/DataInterface.h new file mode 100644 index 0000000..4ef630c --- /dev/null +++ b/apps/InterfaceVisualSFM/DataInterface.h @@ -0,0 +1,383 @@ +//////////////////////////////////////////////////////////////////////////// +// File: DataInterface.h +// Author: Changchang Wu (ccwu@cs.washington.edu) +// Description : data interface, the data format been uploaded to GPU +// +// Copyright (c) 2011 Changchang Wu (ccwu@cs.washington.edu) +// and the University of Washington at Seattle +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation; either +// Version 3 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +//////////////////////////////////////////////////////////////////////////////// + +#ifndef DATA_INTERFACE_GPU_H +#define DATA_INTERFACE_GPU_H + +namespace PBA { + +// ----------------------------WARNING------------------------------ +// ----------------------------------------------------------------- +// ROTATION CONVERSION: +// The internal rotation representation is 3x3 float matrix. Reading +// back the rotations as quaternion or Rodrigues's representation will +// cause inaccuracy, IF you have wrongly reconstructed cameras with +// a very very large focal length (typically also very far away). +// In this case, any small change in the rotation matrix, will cause +// a large reprojection error. +// +// --------------------------------------------------------------------- +// RADIAL distortion is NOT enabled by default, use parameter "-md", -pd" +// or set ConfigBA::__use_radial_distortion to 1 or -1 to enable it. +// --------------------------------------------------------------------------- + +//transfer data type with 4-float alignment +#define CameraT CameraT_ +#define Point3D Point3D_ +template + +struct CameraT_ +{ + typedef FT float_t; + ////////////////////////////////////////////////////// + float_t f; // single focal length, K = [f, 0, 0; 0 f 0; 0 0 1] + float_t t[3]; // T in P = K[R T], T = - RC + float_t m[3][3]; // R in P = K[R T]. + float_t radial; // WARNING: BE careful with the radial distortion model. + float_t distortion_type; + float_t constant_camera; + + ////////////////////////////////////////////////////////// + CameraT_() { radial = 0; distortion_type = 0; constant_camera = 0; } + + ////////////////////////////////////////////// + template void SetCameraT(const CameraX & cam) + { + f = (float_t)cam.f; + t[0] = (float_t)cam.t[0]; t[1] = (float_t)cam.t[1]; t[2] = (float_t)cam.t[2]; + for(int i = 0; i < 3; ++i) for(int j = 0; j < 3; ++j) m[i][j] = (float_t)cam.m[i][j]; + radial = (float_t)cam.radial; + distortion_type = (float_t)cam.distortion_type; + constant_camera = (float_t)cam.constant_camera; + } + + ////////////////////////////////////////// + enum { + CAMERA_VARIABLE = 0, + CAMERA_FIXEDINTRINSIC = (1<<0), + CAMERA_FIXEDEXTRINSIC = (1<<1), + }; + void SetVariableCamera() {(int&)constant_camera = CAMERA_VARIABLE;} + void SetFixedIntrinsic() {(int&)constant_camera = CAMERA_FIXEDINTRINSIC;} + void SetFixedExtrinsic() {(int&)constant_camera = CAMERA_FIXEDEXTRINSIC;} + void SetConstantCamera() {(int&)constant_camera = CAMERA_FIXEDINTRINSIC|CAMERA_FIXEDEXTRINSIC;} + + ////////////////////////////////////// + template void SetFocalLength(Float F){ f = (float_t) F; } + float_t GetFocalLength() const{return f;} + + template void SetMeasurementDistortion(Float r) {radial = (float_t) r; distortion_type = -1;} + float_t GetMeasurementDistortion() const {return distortion_type == -1 ? radial : 0; } + + //normalize radial distortion that applies to angle will be (radial * f * f); + template void SetNormalizedMeasurementDistortion(Float r) {SetMeasurementDistortion(r / (f * f)); } + float_t GetNormalizedMeasurementDistortion() const{return GetMeasurementDistortion() * (f * f); } + + //use projection distortion + template void SetProjectionDistortion(Float r) {radial = float_t(r); distortion_type = 1; } + template void SetProjectionDistortion(const Float* r) {SetProjectionDistortion(r[0]); } + float_t GetProjectionDistortion() {return distortion_type == 1 ? radial : 0; } + + template void SetRodriguesRotation(const Float r[3]) + { + double a = sqrt(r[0]*r[0]+r[1]*r[1]+r[2]*r[2]); + double ct = a==0.0?0.5:(1.0-cos(a))/a/a; + double st = a==0.0?1:sin(a)/a; + m[0][0]=float_t(1.0 - (r[1]*r[1] + r[2]*r[2])*ct); + m[0][1]=float_t(r[0]*r[1]*ct - r[2]*st); + m[0][2]=float_t(r[2]*r[0]*ct + r[1]*st); + m[1][0]=float_t(r[0]*r[1]*ct + r[2]*st); + m[1][1]=float_t(1.0 - (r[2]*r[2] + r[0]*r[0])*ct); + m[1][2]=float_t(r[1]*r[2]*ct - r[0]*st); + m[2][0]=float_t(r[2]*r[0]*ct - r[1]*st); + m[2][1]=float_t(r[1]*r[2]*ct + r[0]*st); + m[2][2]=float_t(1.0 - (r[0]*r[0] + r[1]*r[1])*ct ); + } + template void GetRodriguesRotation(Float r[3]) const + { + double a = (m[0][0]+m[1][1]+m[2][2]-1.0)/2.0; + const double epsilon = 0.01; + if( fabs(m[0][1] - m[1][0]) < epsilon && + fabs(m[1][2] - m[2][1]) < epsilon && + fabs(m[0][2] - m[2][0]) < epsilon ) + { + if( fabs(m[0][1] + m[1][0]) < 0.1 && + fabs(m[1][2] + m[2][1]) < 0.1 && + fabs(m[0][2] + m[2][0]) < 0.1 && a > 0.9) + { + r[0] = 0; + r[1] = 0; + r[2] = 0; + } + else + { + const Float ha = Float(sqrt(0.5) * 3.14159265358979323846); + double xx = (m[0][0]+1.0)/2.0; + double yy = (m[1][1]+1.0)/2.0; + double zz = (m[2][2]+1.0)/2.0; + double xy = (m[0][1]+m[1][0])/4.0; + double xz = (m[0][2]+m[2][0])/4.0; + double yz = (m[1][2]+m[2][1])/4.0; + + if ((xx > yy) && (xx > zz)) + { + if (xx< epsilon) + { + r[0] = 0; r[1] = r[2] = ha; + } else + { + double t = sqrt(xx) ; + r[0] = Float(t * 3.14159265358979323846); + r[1] = Float(xy/t * 3.14159265358979323846); + r[2] = Float(xz/t * 3.14159265358979323846); + } + } else if (yy > zz) + { + if (yy< epsilon) + { + r[0] = r[2] = ha; r[1] = 0; + } else + { + double t = sqrt(yy); + r[0] = Float(xy/t* 3.14159265358979323846); + r[1] = Float( t * 3.14159265358979323846); + r[2] = Float(yz/t* 3.14159265358979323846); + } + } else + { + if (zz< epsilon) + { + r[0] = r[1] = ha; r[2] = 0; + } else + { + double t = sqrt(zz); + r[0] = Float(xz/ t* 3.14159265358979323846); + r[1] = Float(yz/ t* 3.14159265358979323846); + r[2] = Float( t * 3.14159265358979323846); + } + } + } + } + else + { + a = acos(a); + double b = 0.5*a/sin(a); + r[0] = Float(b*(m[2][1]-m[1][2])); + r[1] = Float(b*(m[0][2]-m[2][0])); + r[2] = Float(b*(m[1][0]-m[0][1])); + } + } + //////////////////////// + template void SetQuaternionRotation(const Float q[4]) + { + double qq = sqrt(q[0]*q[0]+q[1]*q[1]+q[2]*q[2]+q[3]*q[3]); + double qw, qx, qy, qz; + if(qq>0) + { + qw=q[0]/qq; + qx=q[1]/qq; + qy=q[2]/qq; + qz=q[3]/qq; + }else + { + qw = 1; + qx = qy = qz = 0; + } + m[0][0]=float_t(qw*qw + qx*qx- qz*qz- qy*qy ); + m[0][1]=float_t(2*qx*qy -2*qz*qw ); + m[0][2]=float_t(2*qy*qw + 2*qz*qx); + m[1][0]=float_t(2*qx*qy+ 2*qw*qz); + m[1][1]=float_t(qy*qy+ qw*qw - qz*qz- qx*qx); + m[1][2]=float_t(2*qz*qy- 2*qx*qw); + m[2][0]=float_t(2*qx*qz- 2*qy*qw); + m[2][1]=float_t(2*qy*qz + 2*qw*qx ); + m[2][2]=float_t(qz*qz+ qw*qw- qy*qy- qx*qx); + } + template void GetQuaternionRotation(Float q[4]) const + { + q[0]= 1 + m[0][0] + m[1][1] + m[2][2]; + if(q[0]>0.000000001) + { + q[0] = sqrt(q[0])/2.0; + q[1]= (m[2][1] - m[1][2])/( 4.0 *q[0]); + q[2]= (m[0][2] - m[2][0])/( 4.0 *q[0]); + q[3]= (m[1][0] - m[0][1])/( 4.0 *q[0]); + }else + { + double s; + if ( m[0][0] > m[1][1] && m[0][0] > m[2][2] ) + { + s = 2.0 * sqrt( 1.0 + m[0][0] - m[1][1] - m[2][2]); + q[1] = 0.25 * s; + q[2] = (m[0][1] + m[1][0] ) / s; + q[3] = (m[0][2] + m[2][0] ) / s; + q[0] = (m[1][2] - m[2][1] ) / s; + } else if (m[1][1] > m[2][2]) + { + s = 2.0 * sqrt( 1.0 + m[1][1] - m[0][0] - m[2][2]); + q[1] = (m[0][1] + m[1][0] ) / s; + q[2] = 0.25 * s; + q[3] = (m[1][2] + m[2][1] ) / s; + q[0] = (m[0][2] - m[2][0] ) / s; + } else + { + s = 2.0 * sqrt( 1.0 + m[2][2] - m[0][0] - m[1][1]); + q[1] = (m[0][2] + m[2][0] ) / s; + q[2] = (m[1][2] + m[2][1] ) / s; + q[3] = 0.25f * s; + q[0] = (m[0][1] - m[1][0] ) / s; + } + } + } + //////////////////////////////////////////////// + template void SetMatrixRotation(const Float * r) + { + for(int i = 0; i < 9; ++i) m[0][i] = float_t(r[i]); + } + template void GetMatrixRotation(Float * r) const + { + for(int i = 0; i < 9; ++i) r[i] = Float(m[0][i]); + } + float GetRotationMatrixDeterminant()const + { + return m[0][0]*m[1][1]*m[2][2] + + m[0][1]*m[1][2]*m[2][0] + + m[0][2]*m[1][0]*m[2][1] - + m[0][2]*m[1][1]*m[2][0] - + m[0][1]*m[1][0]*m[2][2] - + m[0][0]*m[1][2]*m[2][1]; + } + /////////////////////////////////////// + template void SetTranslation(const Float T[3]) + { + t[0] = (float_t)T[0]; + t[1] = (float_t)T[1]; + t[2] = (float_t)T[2]; + } + template void GetTranslation(Float T[3]) const + { + T[0] = (Float)t[0]; + T[1] = (Float)t[1]; + T[2] = (Float)t[2]; + } + ///////////////////////////////////////////// + template void SetCameraCenterAfterRotation(const Float c[3]) + { + //t = - R * C + for(int j = 0; j < 3; ++j) t[j] = -float_t(double(m[j][0])*double(c[0]) + double(m[j][1])*double(c[1]) + double(m[j][2])*double(c[2])); + } + template void GetCameraCenter(Float c[3]) const + { + //C = - R' * t + for(int j = 0; j < 3; ++j) c[j] = -Float(double(m[0][j])*double(t[0]) + double(m[1][j])*double(t[1]) + double(m[2][j])*double(t[2])); + } + //////////////////////////////////////////// + template void SetInvertedRT(const Float e[3], const Float T[3]) + { + SetRodriguesRotation(e); + for(int i = 3; i < 9; ++i) m[0][i] = - m[0][i]; + SetTranslation(T); t[1] = - t[1]; t[2] = -t[2]; + } + + template void GetInvertedRT (Float e[3], Float T[3]) const + { + CameraT ci; ci.SetMatrixRotation(m[0]); + for(int i = 3; i < 9; ++i) ci.m[0][i] = - ci.m[0][i]; + //for(int i = 1; i < 3; ++i) for(int j = 0; j < 3; ++j) ci.m[i][j] = - ci.m[i][j]; + ci.GetRodriguesRotation(e); + GetTranslation(T); T[1] = - T[1]; T[2] = -T[2]; + } + template void SetInvertedR9T(const Float e[9], const Float T[3]) + { + //for(int i = 0; i < 9; ++i) m[0][i] = (i < 3 ? e[i] : - e[i]); + //SetTranslation(T); t[1] = - t[1]; t[2] = -t[2]; + m[0][0] = e[0]; m[0][1] = e[1]; m[0][2] = e[2]; + m[1][0] = -e[3]; m[1][1] = -e[4]; m[1][2] = -e[5]; + m[2][0] = -e[6]; m[2][1] = -e[7]; m[2][2] = -e[8]; + t[0] = T[0]; t[1] = -T[1]; t[2] = -T[2]; + } + template void GetInvertedR9T(Float e[9], Float T[3]) const + { + e[0] = m[0][0]; e[1] = m[0][1]; e[2] = m[0][2]; + e[3] = - m[1][0]; e[4] = -m[1][1]; e[5] = -m[1][2]; + e[6] = -m[2][0]; e[7] = -m[2][1]; e[8] = -m[2][2] ; + T[0] = t[0]; T[1] = -t[1]; T[2] = -t[2]; + } +}; + + + +template +struct Point3D +{ + typedef FT float_t; + float_t xyz[3]; //3D point location + float_t reserved; //alignment + //////////////////////////////// + template void SetPoint(Float x, Float y, Float z) + { + xyz[0] = (float_t) x; + xyz[1] = (float_t) y; + xyz[2] = (float_t) z; + reserved = 0; + } + template void SetPoint(const Float * p) + { + xyz[0] = (float_t) p[0]; + xyz[1] = (float_t) p[1]; + xyz[2] = (float_t) p[2]; + reserved = 0; + } + template void GetPoint(Float* p) const + { + p[0] = (Float) xyz[0]; + p[1] = (Float) xyz[1]; + p[2] = (Float) xyz[2]; + } + template void GetPoint(Float&x, Float&y, Float&z) const + { + x = (Float) xyz[0]; + y = (Float) xyz[1]; + z = (Float) xyz[2]; + } +}; + +#undef CameraT +#undef Point3D + +typedef CameraT_ CameraT; +typedef CameraT_ CameraD; +typedef Point3D_ Point3D; +typedef Point3D_ Point3B; + +struct Point2D +{ + float x, y; + //////////////////////////////////////////////////////// + Point2D(){} + template Point2D(Float X, Float Y) {SetPoint2D(X, Y); } + template void SetPoint2D(Float X, Float Y) { x = (float) X; y = (float) Y; } + template void GetPoint2D(Float&X, Float&Y) const { X = (Float) x; Y = (Float) y; } +}; + +} // namespace PBA + +#endif + diff --git a/apps/InterfaceVisualSFM/InterfaceVisualSFM.cpp b/apps/InterfaceVisualSFM/InterfaceVisualSFM.cpp new file mode 100644 index 0000000..2cc8330 --- /dev/null +++ b/apps/InterfaceVisualSFM/InterfaceVisualSFM.cpp @@ -0,0 +1,607 @@ +/* + * InterfaceVisualSFM.cpp + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#define LOG_OUT() GET_LOG() +#define LOG_ERR() GET_LOG() +#include "Util.h" +#include + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("InterfaceVisualSFM") +#define MVS_EXT _T(".mvs") +#define VSFM_EXT _T(".nvm") +#define BUNDLE_EXT _T(".out") +#define CMPMVS_EXT _T(".lst") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { +String strInputFileName; +String strOutputFileName; +String strOutputImageFolder; +bool IsFromOpenMVS; // conversion direction +unsigned nArchiveType; +int nProcessPriority; +unsigned nMaxThreads; +String strConfigFileName; +boost::program_options::variables_map vm; +} // namespace OPT + +class Application { +public: + Application() {} + ~Application() { Finalize(); } + + bool Initialize(size_t argc, LPCTSTR* argv); + void Finalize(); +}; // Application + +// initialize and parse the command line parameters +bool Application::Initialize(size_t argc, LPCTSTR* argv) +{ + // initialize log and console + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "produce this help message") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_DEFAULT), "project archive type: 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description config("Main options"); + config.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input filename containing camera poses and image list (NVM, undistorted OUT + image_list.TXT, LST)") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the mesh") + ("output-image-folder", boost::program_options::value(&OPT::strOutputImageFolder)->default_value("undistorted_images"), "output folder to store undistorted images") + ; + + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(config); + + boost::program_options::options_description config_file_options; + config_file_options.add(config); + + boost::program_options::positional_options_description p; + p.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(p).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + INIT_WORKING_FOLDER; + // parse configuration file + std::ifstream ifs(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (ifs) { + boost::program_options::store(parse_config_file(ifs, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // initialize the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-")+Util::getUniqueName(0)+_T(".log")).c_str()); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // validate input + Util::ensureValidPath(OPT::strInputFileName); + Util::ensureUnifySlash(OPT::strInputFileName); + if (OPT::vm.count("help") || OPT::strInputFileName.IsEmpty()) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(config); + GET_LOG() << visible; + } + if (OPT::strInputFileName.IsEmpty()) + return false; + + // initialize optional options + if (OPT::strInputFileName.IsEmpty()) + return false; + Util::ensureValidPath(OPT::strOutputFileName); + Util::ensureUnifySlash(OPT::strOutputFileName); + Util::ensureUnifySlash(OPT::strOutputImageFolder); + Util::ensureFolderSlash(OPT::strOutputImageFolder); + const String strInputFileNameExt(Util::getFileExt(OPT::strInputFileName).ToLower()); + OPT::IsFromOpenMVS = (strInputFileNameExt == MVS_FILE_EXTENSION); + if (OPT::IsFromOpenMVS) { + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = Util::getFilePath(OPT::strInputFileName); + } else { + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = Util::getFilePath(OPT::strInputFileName) + _T("scene") MVS_FILE_EXTENSION; + else + OPT::strOutputImageFolder = Util::getRelativePath(Util::getFilePath(OPT::strOutputFileName), Util::getFilePath(OPT::strInputFileName)+OPT::strOutputImageFolder); + } + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::Finalize() +{ + MVS::Finalize(); + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +} // unnamed namespace + +#define PBA_PRECISION float + +namespace PBA { +template struct CameraT_; +typedef CameraT_ Camera; +template struct Point3D_; +typedef Point3D_ Point3D; +} // namespace PBA + +namespace MVS { +// given an undistorted pixel coordinate and one radial-undistortion parameter, +// compute the corresponding distorted coordinate +template +inline TPoint2 DistortPointR1(const TPoint2& pt, const REAL& k1) { + if (k1 == 0) + return pt; + const REAL y(pt.y == 0 ? REAL(1.e-12) : REAL(pt.y)); + const REAL t2(y*y); + const REAL t3(t2*t2*t2); + const REAL t4(pt.x*pt.x); + const REAL t7(k1*(t2+t4)); + const REAL t9(1.0/t7); + const REAL t10(t2*t9*y*0.5); + const REAL t11(t3*t9*t9*(0.25+t9/27.0)); + #ifndef _RELEASE + TPoint2 upt; + #endif + if (k1 > 0) { + const REAL t17(CBRT(t10+SQRT(t11))); + const REAL t18(t17-t2*t9/(t17*3)); + #ifndef _RELEASE + upt = + #else + return + #endif + TPoint2(TYPE(t18*pt.x/y), TYPE(t18)); + } else { + ASSERT(t11 <= 0); + const std::complex t16(t10, SQRT(-t11)); + const std::complex t17(pow(t16, 1.0/3.0)); + const std::complex t14((t2*t9)/(t17*3.0)); + const std::complex t18((t17+t14)*std::complex(0.0,SQRT_3)); + const std::complex t19(0.5*(t14-t17-t18)); + #ifndef _RELEASE + upt = + #else + return + #endif + TPoint2(TYPE(t19.real()*pt.x/y), TYPE(t19.real())); + } + #ifndef _RELEASE + ASSERT(ABS(TYPE((1.0+k1*(upt.x*upt.x+upt.y*upt.y))*upt.x) - pt.x) < TYPE(0.001)); + ASSERT(ABS(TYPE((1.0+k1*(upt.x*upt.x+upt.y*upt.y))*upt.y) - pt.y) < TYPE(0.001)); + return upt; + #endif +} + +void UndistortImage(const Camera& camera, const REAL& k1, const Image8U3 imgIn, Image8U3& imgOut) +{ + // allocate the undistorted image + if (imgOut.data == imgIn.data || + imgOut.cols != imgIn.cols || + imgOut.rows != imgIn.rows || + imgOut.type() != imgIn.type()) + imgOut = Image8U3(imgIn.rows, imgIn.cols); + + // compute each pixel + const int w = imgIn.cols; + const int h = imgIn.rows; + const Matrix3x3f K(camera.K); + const Matrix3x3f invK(camera.GetInvK()); + ASSERT(ISEQUAL(K(0,2),0.5f*(w-1)) && ISEQUAL(K(1,2),0.5f*(h-1))); + typedef Sampler::Cubic Sampler; + const Sampler sampler; + Point2f pt; + for (int v=0; v(sampler, pt).cast(); + } else { + // set to black + col = Pixel8U::BLACK; + } + } + } +} +} // namespace MVS + + +bool ExportSceneVSFM() +{ + TD_TIMER_START(); + + // read MVS input data + MVS::Scene scene(OPT::nMaxThreads); + if (!scene.Load(MAKE_PATH_SAFE(OPT::strInputFileName))) + return false; + + // convert and write data from OpenMVS to VisualSFM + std::vector cameras; + std::vector vertices; + std::vector measurements; // the array of 2D projections (only inliers) + std::vector correspondingPoint; // 3D point index corresponding to each 2D projection + std::vector correspondingView; // and camera index + std::vector names; + std::vector ptc; + cameras.reserve(scene.images.size()); + names.reserve(scene.images.size()); + MVS::IIndexArr mapIdx(scene.images.size()); + bool bFocalWarning(false), bPrincipalpointWarning(false); + FOREACH(idx, scene.images) { + const MVS::Image& image = scene.images[idx]; + if (!image.IsValid()) { + mapIdx[idx] = NO_ID; + continue; + } + if (!bFocalWarning && !ISEQUAL(image.camera.K(0, 0), image.camera.K(1, 1))) { + DEBUG("warning: fx != fy and NVM format does not support it"); + bFocalWarning = true; + } + if (!bPrincipalpointWarning && (!ISEQUAL(REAL(image.width-1)*0.5, image.camera.K(0, 2)) || !ISEQUAL(REAL(image.height-1)*0.5, image.camera.K(1, 2)))) { + DEBUG("warning: cx, cy are not the image center and NVM format does not support it"); + bPrincipalpointWarning = true; + } + PBA::Camera cameraNVM; + cameraNVM.SetFocalLength((image.camera.K(0, 0) + image.camera.K(1, 1)) * 0.5); + cameraNVM.SetMatrixRotation(image.camera.R.val); + cameraNVM.SetCameraCenterAfterRotation(image.camera.C.ptr()); + mapIdx[idx] = static_cast(cameras.size()); + cameras.emplace_back(cameraNVM); + names.emplace_back(MAKE_PATH_REL(WORKING_FOLDER_FULL, image.name)); + } + vertices.reserve(scene.pointcloud.points.size()); + measurements.reserve(scene.pointcloud.pointViews.size()); + correspondingPoint.reserve(scene.pointcloud.pointViews.size()); + correspondingView.reserve(scene.pointcloud.pointViews.size()); + FOREACH(idx, scene.pointcloud.points) { + const MVS::PointCloud::Point& X = scene.pointcloud.points[idx]; + const MVS::PointCloud::ViewArr& views = scene.pointcloud.pointViews[idx]; + const size_t prevMeasurements(measurements.size()); + for (MVS::IIndex idxView: views) { + const MVS::Image& image = scene.images[idxView]; + const Point2f pt(image.camera.TransformPointW2I(Cast(X))); + if (pt.x < 0 || pt.y < 0 || pt.x > image.width-1 || pt.y > image.height-1) + continue; + measurements.emplace_back(pt.x, pt.y); + correspondingView.emplace_back(static_cast(mapIdx[idxView])); + correspondingPoint.emplace_back(static_cast(vertices.size())); + } + if (prevMeasurements < measurements.size()) + vertices.emplace_back(PBA::Point3D{X.x, X.y, X.z}); + } + if (!scene.pointcloud.colors.empty()) { + ptc.reserve(scene.pointcloud.colors.size()*3); + FOREACH(idx, scene.pointcloud.points) { + const MVS::PointCloud::Color& c = scene.pointcloud.colors[idx]; + ptc.emplace_back(c.r); + ptc.emplace_back(c.g); + ptc.emplace_back(c.b); + } + } + PBA::SaveModelFile(MAKE_PATH_SAFE(OPT::strOutputFileName), cameras, vertices, measurements, correspondingPoint, correspondingView, names, ptc); + + VERBOSE("Input data exported: %u images & %u points (%s)", scene.images.size(), scene.pointcloud.GetSize(), TD_TIMER_GET_FMT().c_str()); + return true; +} + + +bool ImportSceneVSFM() +{ + TD_TIMER_START(); + + // read VisualSFM input data + std::vector cameras; + std::vector vertices; + std::vector measurements; // the array of 2D projections (only inliers) + std::vector correspondingPoint; // 3D point index corresponding to each 2D projection + std::vector correspondingView; // and camera index + std::vector names; + std::vector ptc; + if (!PBA::LoadModelFile(MAKE_PATH_SAFE(OPT::strInputFileName), cameras, vertices, measurements, correspondingPoint, correspondingView, names, ptc)) + return false; + + // convert data from VisualSFM to OpenMVS + MVS::Scene scene(OPT::nMaxThreads); + scene.platforms.Reserve((uint32_t)cameras.size()); + scene.images.Reserve((MVS::IIndex)cameras.size()); + scene.nCalibratedImages = 0; + for (size_t idx=0; idx(idx); + const PBA::Camera& cameraNVM = cameras[idx]; + camera.K = MVS::Platform::Camera::ComposeK(cameraNVM.GetFocalLength(), cameraNVM.GetFocalLength(), image.width, image.height); + camera.R = RMatrix::IDENTITY; + camera.C = CMatrix::ZERO; + // normalize camera intrinsics + camera.K = camera.GetScaledK(REAL(1)/MVS::Camera::GetNormalizationScale(image.width, image.height)); + // set pose + image.poseID = platform.poses.GetSize(); + MVS::Platform::Pose& pose = platform.poses.AddEmpty(); + cameraNVM.GetMatrixRotation(pose.R.val); + cameraNVM.GetCameraCenter(pose.C.ptr()); + image.UpdateCamera(scene.platforms); + ++scene.nCalibratedImages; + } + scene.pointcloud.points.Reserve(vertices.size()); + for (size_t idx=0; idx +void _ImageListParseP(const LPSTR* argv, TMatrix& P) +{ + // read projection matrix + P(0,0) = String::FromString(argv[0]); + P(0,1) = String::FromString(argv[1]); + P(0,2) = String::FromString(argv[2]); + P(0,3) = String::FromString(argv[3]); + P(1,0) = String::FromString(argv[4]); + P(1,1) = String::FromString(argv[5]); + P(1,2) = String::FromString(argv[6]); + P(1,3) = String::FromString(argv[7]); + P(2,0) = String::FromString(argv[8]); + P(2,1) = String::FromString(argv[9]); + P(2,2) = String::FromString(argv[10]); + P(2,3) = String::FromString(argv[11]); +} + +int ImportSceneCMPMVS() +{ + TD_TIMER_START(); + + MVS::Scene scene(OPT::nMaxThreads); + + // read CmpMVS input data as a list of images and their projection matrices + std::ifstream iFilein(MAKE_PATH_SAFE(OPT::strInputFileName)); + if (!iFilein.is_open()) + return false; + while (iFilein.good()) { + String strImageName; + std::getline(iFilein, strImageName); + if (strImageName.empty()) + continue; + if (!File::access(MAKE_PATH_SAFE(strImageName))) + return false; + const String strImageNameP(Util::getFileFullName(strImageName)+"_P.txt"); + std::ifstream iFileP(MAKE_PATH_SAFE(strImageNameP)); + if (!iFileP.is_open()) + return false; + String strP; int numLines(0); + while (iFileP.good()) { + String line; + std::getline(iFileP, line); + if (strImageName.empty()) + break; + if (strP.empty()) + strP = line; + else + strP += _T(' ') + line; + ++numLines; + } + if (numLines != 3) + return false; + PMatrix P; + size_t argc; + CAutoPtrArr argv(Util::CommandLineToArgvA(strP, argc)); + if (argc != 12) + return false; + _ImageListParseP(argv, P); + KMatrix K; RMatrix R; CMatrix C; + MVS::DecomposeProjectionMatrix(P, K, R, C); + // set image + MVS::Image& image = scene.images.AddEmpty(); + image.name = strImageName; + Util::ensureUnifySlash(image.name); + image.name = MAKE_PATH_FULL(WORKING_FOLDER_FULL, image.name); + if (!image.ReloadImage(0, false)) { + LOG("error: can not read image %s", image.name.c_str()); + return false; + } + // set camera + image.platformID = scene.platforms.GetSize(); + MVS::Platform& platform = scene.platforms.AddEmpty(); + MVS::Platform::Camera& camera = platform.cameras.AddEmpty(); + image.cameraID = 0; + camera.K = K; + camera.R = RMatrix::IDENTITY; + camera.C = CMatrix::ZERO; + // normalize camera intrinsics + camera.K = camera.GetScaledK(REAL(1)/MVS::Camera::GetNormalizationScale(image.width, image.height)); + // set pose + image.poseID = platform.poses.GetSize(); + MVS::Platform::Pose& pose = platform.poses.AddEmpty(); + pose.R = R; + pose.C = C; + image.UpdateCamera(scene.platforms); + ++scene.nCalibratedImages; + } + + VERBOSE("Input data imported: %u images (%s)", scene.images.size(), TD_TIMER_GET_FMT().c_str()); + + // write OpenMVS input data + return scene.SaveInterface(MAKE_PATH_SAFE(OPT::strOutputFileName)); +} + + +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application application; + if (!application.Initialize(argc, argv)) + return EXIT_FAILURE; + + if (OPT::IsFromOpenMVS) { + ExportSceneVSFM(); + } else { + const String strInputFileNameExt(Util::getFileExt(OPT::strInputFileName).ToLower()); + if (strInputFileNameExt == VSFM_EXT || strInputFileNameExt == BUNDLE_EXT) { + if (!ImportSceneVSFM()) + return EXIT_FAILURE; + } else + if (strInputFileNameExt == CMPMVS_EXT) { + if (!ImportSceneCMPMVS()) + return EXIT_FAILURE; + } + } + + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/InterfaceVisualSFM/Util.h b/apps/InterfaceVisualSFM/Util.h new file mode 100644 index 0000000..abf7e1d --- /dev/null +++ b/apps/InterfaceVisualSFM/Util.h @@ -0,0 +1,756 @@ +//////////////////////////////////////////////////////////////////////////// +// File: util.h +// Author: Changchang Wu (ccwu@cs.washington.edu) +// Description : some utility functions for reading/writing SfM data +// +// Copyright (c) 2011 Changchang Wu (ccwu@cs.washington.edu) +// and the University of Washington at Seattle +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation; either +// Version 3 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include +#include "DataInterface.h" + +namespace PBA { + +//File loader supports .nvm format and bundler format +bool LoadModelFile(const char* name, std::vector& camera_data, std::vector& point_data, + std::vector& measurements, std::vector& ptidx, std::vector& camidx, + std::vector& names, std::vector& ptc); +void SaveNVM(const char* filename, std::vector& camera_data, std::vector& point_data, + std::vector& measurements, std::vector& ptidx, std::vector& camidx, + std::vector& names, std::vector& ptc); +void SaveBundlerModel(const char* filename, std::vector& camera_data, std::vector& point_data, + std::vector& measurements, std::vector& ptidx, std::vector& camidx); + +////////////////////////////////////////////////////////////////// +void AddNoise(std::vector& camera_data, std::vector& point_data, float percent); +void AddStableNoise(std::vector& camera_data, std::vector& point_data, + const std::vector& ptidx, const std::vector& camidx, float percent); +bool RemoveInvisiblePoints( std::vector& camera_data, std::vector& point_data, + std::vector& ptidx, std::vector& camidx, + std::vector& measurements, std::vector& names, std::vector& ptc); + +///////////////////////////////////////////////////////////////////////////// +bool LoadNVM(std::ifstream& in, std::vector& camera_data, std::vector& point_data, + std::vector& measurements, std::vector& ptidx, std::vector& camidx, + std::vector& names, std::vector& ptc) +{ + int rotation_parameter_num = 4; + bool format_r9t = false; + std::string token; + if(in.peek() == 'N') + { + in >> token; //file header + if(strstr(token.c_str(), "R9T")) + { + rotation_parameter_num = 9; //rotation as 3x3 matrix + format_r9t = true; + } + } + + double fxFixed, fyFixed, cxFixed, cyFixed, k1(0); + int ncam = 0, npoint = 0, nproj = 0; + in >> token; + if (token == "FixedK") { + // read fixed intrinsics + std::getline(in, token); + sscanf(token.c_str(), "%lf %lf %lf %lf %lf", &fxFixed, &cxFixed, &fyFixed, &cyFixed, &k1); + // read # of cameras + in >> ncam; + } else { + // read # of cameras + ncam = atoi(token.c_str()); + } + if(ncam <= 1) return false; + + //read the camera parameters + camera_data.resize(ncam); // allocate the camera data + names.resize(ncam); + for(int i = 0; i < ncam; ++i) + { + double f, q[9], c[3], d[2]; + in >> token >> f ; + for(int j = 0; j < rotation_parameter_num; ++j) in >> q[j]; + in >> c[0] >> c[1] >> c[2] >> d[0] >> d[1]; + + camera_data[i].SetFocalLength(f); + if(format_r9t) + { + camera_data[i].SetMatrixRotation(q); + camera_data[i].SetTranslation(c); + } + else + { + //older format for compatibility + camera_data[i].SetQuaternionRotation(q); //quaternion from the file + camera_data[i].SetCameraCenterAfterRotation(c); //camera center from the file + } + camera_data[i].SetNormalizedMeasurementDistortion(k1!=0 ? k1 : d[0]); + names[i] = token; + } + + ////////////////////////////////////// + in >> npoint; if(npoint <= 0) return false; + + //read image projections and 3D points. + point_data.resize(npoint); + for(int i = 0; i < npoint; ++i) + { + float pt[3]; int cc[3], npj; + in >> pt[0] >> pt[1] >> pt[2] + >> cc[0] >> cc[1] >> cc[2] >> npj; + for(int j = 0; j < npj; ++j) + { + int cidx, fidx; float imx, imy; + in >> cidx >> fidx >> imx >> imy; + + camidx.push_back(cidx); //camera index + ptidx.push_back(i); //point index + + //add a measurement to the vector + measurements.push_back(Point2D(imx, imy)); + nproj ++; + } + point_data[i].SetPoint(pt); + ptc.insert(ptc.end(), cc, cc + 3); + } + /////////////////////////////////////////////////////////////////////////////// + LOG_OUT() << ncam << " cameras; " << npoint << " 3D points; " << nproj << " projections\n"; + + return true; +} + + +void SaveNVM(const char* filename, std::vector& camera_data, std::vector& point_data, + std::vector& measurements, std::vector& ptidx, std::vector& camidx, + std::vector& names, std::vector& ptc) +{ + LOG_OUT() << "Saving model to " << filename << "...\n"; + std::ofstream out(filename); + + out << "NVM_V3_R9T\n" << camera_data.size() << '\n' << std::setprecision(12); + if(names.size() < camera_data.size()) names.resize(camera_data.size(),std::string("unknown")); + if(ptc.size() < 3 * point_data.size()) ptc.resize(point_data.size() * 3, 0); + + //////////////////////////////////// + for(size_t i = 0; i < camera_data.size(); ++i) + { + CameraT& cam = camera_data[i]; + out << names[i] << ' ' << cam.GetFocalLength() << ' '; + for(int j = 0; j < 9; ++j) out << cam.m[0][j] << ' '; + out << cam.t[0] << ' ' << cam.t[1] << ' ' << cam.t[2] << ' ' + << cam.GetNormalizedMeasurementDistortion() << " 0\n"; + } + + out << point_data.size() << '\n'; + + for(size_t i = 0, j = 0; i < point_data.size(); ++i) + { + Point3D& pt = point_data[i]; + int * pc = &ptc[i * 3]; + out << pt.xyz[0] << ' ' << pt.xyz[1] << ' ' << pt.xyz[2] << ' ' + << pc[0] << ' ' << pc[1] << ' ' << pc[2] << ' '; + + size_t je = j; + while(je < ptidx.size() && ptidx[je] == (int) i) je++; + + out << (je - j) << ' '; + + for(; j < je; ++j) out << camidx[j] << ' ' << " 0 " << measurements[j].x << ' ' << measurements[j].y << ' '; + + out << '\n'; + } +} + + +bool LoadBundlerOut(const char* name, std::ifstream& in, std::vector& camera_data, std::vector& point_data, + std::vector& measurements, std::vector& ptidx, std::vector& camidx, + std::vector& names, std::vector& ptc) +{ + int rotation_parameter_num = 9; + std::string token; + while(in.peek() == '#') std::getline(in, token); + + char listpath[1024], filepath[1024]; + strcpy(listpath, name); + char* ext = strstr(listpath, ".out"); + strcpy(ext, "-list.txt\0"); + + /////////////////////////////////// + std::ifstream listin(listpath); + if(!listin.is_open()) + { + listin.close(); listin.clear(); + strcpy(ext, ".txt\0"); + listin.open(listpath); + } + if(!listin.is_open()) + { + listin.close(); listin.clear(); + char * slash = strrchr(listpath, '/'); + if(slash == NULL) slash = strrchr(listpath, '\\'); + slash = slash ? slash + 1 : listpath; + strcpy(slash, "image_list.txt"); + listin.open(listpath); + } + if(listin) LOG_OUT() << "Using image list: " << listpath << '\n'; + + // read # of cameras + int ncam = 0, npoint = 0, nproj = 0; + in >> ncam >> npoint; + if(ncam <= 1 || npoint <= 1) return false; + LOG_OUT() << ncam << " cameras; " << npoint << " 3D points;\n"; + + //read the camera parameters + camera_data.resize(ncam); // allocate the camera data + names.resize(ncam); + + bool det_checked = false; + for(int i = 0; i < ncam; ++i) + { + float f, q[9], c[3], d[2]; + in >> f >> d[0] >> d[1]; + for(int j = 0; j < rotation_parameter_num; ++j) in >> q[j]; + in >> c[0] >> c[1] >> c[2]; + + camera_data[i].SetFocalLength(f); + camera_data[i].SetInvertedR9T(q, c); + camera_data[i].SetProjectionDistortion(d[0]); + + if(listin >> filepath && f != 0) + { + names[i] = filepath; + std::getline(listin, token); + + if(!det_checked) + { + float det = camera_data[i].GetRotationMatrixDeterminant(); + LOG_OUT() << "Check rotation matrix: " << det << '\n'; + det_checked = true; + } + }else + { + names[i] = "unknown"; + } + } + + + //read image projections and 3D points. + point_data.resize(npoint); + for(int i = 0; i < npoint; ++i) + { + float pt[3]; int cc[3], npj; + in >> pt[0] >> pt[1] >> pt[2] + >> cc[0] >> cc[1] >> cc[2] >> npj; + for(int j = 0; j < npj; ++j) + { + int cidx, fidx; float imx, imy; + in >> cidx >> fidx >> imx >> imy; + + camidx.push_back(cidx); //camera index + ptidx.push_back(i); //point index + + //add a measurement to the vector + measurements.push_back(Point2D(imx, -imy)); + nproj ++; + } + point_data[i].SetPoint(pt[0], pt[1], pt[2]); + ptc.insert(ptc.end(), cc, cc + 3); + } + /////////////////////////////////////////////////////////////////////////////// + LOG_OUT() << ncam << " cameras; " << npoint << " 3D points; " << nproj << " projections\n"; + return true; +} + +void SaveBundlerOut(const char* filename, std::vector& camera_data, std::vector& point_data, + std::vector& measurements, std::vector& ptidx, std::vector& camidx, + std::vector& names, std::vector& ptc) +{ + char listpath[1024]; strcpy(listpath, filename); + char* ext = strstr(listpath, ".out"); if(ext == NULL) return; + strcpy(ext, "-list.txt\0"); + + std::ofstream out(filename); + out << "# Bundle file v0.3\n"; + out << std::setprecision(12); //need enough precision + out << camera_data.size() << " " << point_data.size() << '\n'; + + //save camera data + for(size_t i = 0; i < camera_data.size(); ++i) + { + float q[9], c[3]; + CameraT& ci = camera_data[i]; + out << ci.GetFocalLength() << ' ' << ci.GetProjectionDistortion() << " 0\n"; + ci.GetInvertedR9T(q, c); + for(int j = 0; j < 9; ++j) out << q[j] << (((j % 3) == 2)? '\n' : ' '); + out << c[0] << ' ' << c[1] << ' ' << c[2] << '\n'; + } + /// + for(size_t i = 0, j = 0; i < point_data.size(); ++i) + { + int npj = 0, *ci = &ptc[i * 3]; Point3D& pt = point_data[i]; + while(j + npj < point_data.size() && ptidx[j + npj] == ptidx[j]) npj++; + /////////////////////////// + out << pt.xyz[0] << ' ' << pt.xyz[1] << ' ' << pt.xyz[2] << '\n'; + out << ci[0] << ' ' << ci[1] << ' ' << ci[2] << '\n'; + out << npj << ' '; + for(int k = 0; k < npj; ++k) out << camidx[j + k] << " 0 " + << measurements[j + k].x << ' ' << -measurements[j + k].y << '\n'; + out << '\n'; j += npj; + } + + std::ofstream listout(listpath); + for(size_t i = 0; i < names.size(); ++i) listout << names[i] << '\n'; +} + +template +bool LoadBundlerModel(std::ifstream& in, std::vector& camera_data, std::vector& point_data, + std::vector& measurements, std::vector& ptidx, std::vector& camidx) +{ + // read bundle data from a file + size_t ncam = 0, npt = 0, nproj = 0; + if(!(in >> ncam >> npt >> nproj)) return false; + /////////////////////////////////////////////////////////////////////////////// + LOG_OUT() << ncam << " cameras; " << npt << " 3D points; " << nproj << " projections\n"; + + camera_data.resize(ncam); + point_data.resize(npt); + measurements.resize(nproj); + camidx.resize(nproj); + ptidx.resize(nproj); + + for(size_t i = 0; i < nproj; ++i) + { + double x, y; int cidx, pidx; + in >> cidx >> pidx >> x >> y; + if(((size_t) pidx) == npt && camidx.size() > i) + { + camidx.resize(i); + ptidx.resize(i); + measurements.resize(i); + LOG_OUT() << "Truncate measurements to " << i << '\n'; + }else if(((size_t) pidx) >= npt) + { + continue; + }else + { + camidx[i] = cidx; ptidx[i] = pidx; + measurements[i].SetPoint2D(x, -y); + } + } + + for(size_t i = 0; i < ncam; ++i) + { + double p[9]; + for(int j = 0; j < 9; ++j) in >> p[j]; + CameraT& cam = camera_data[i]; + cam.SetFocalLength(p[6]); + cam.SetInvertedRT(p, p + 3); + cam.SetProjectionDistortion(p[7]); + } + + for(size_t i = 0; i < npt; ++i) + { + double pt[3]; + in >> pt[0] >> pt[1] >> pt[2]; + point_data[i].SetPoint(pt); + } + return true; +} + +void SaveBundlerModel(const char* filename, std::vector& camera_data, std::vector& point_data, + std::vector& measurements, std::vector& ptidx, std::vector& camidx) +{ + LOG_OUT() << "Saving model to " << filename << "...\n"; + std::ofstream out(filename); + out << std::setprecision(12); //need enough precision + out << camera_data.size() << ' ' << point_data.size() << ' ' << measurements.size() << '\n'; + for(size_t i = 0; i < measurements.size(); ++i) + { + out << camidx[i] << ' ' << ptidx[i] << ' ' << measurements[i].x << ' ' << -measurements[i].y << '\n'; + } + + for(size_t i = 0; i < camera_data.size(); ++i) + { + CameraT& cam = camera_data[i]; + double r[3], t[3]; cam.GetInvertedRT(r, t); + out << r[0] << ' ' << r[1] << ' ' << r[2] << ' ' + << t[0] << ' ' << t[1] << ' ' << t[2] << ' ' << cam.f + << ' ' << cam.GetProjectionDistortion() << " 0\n"; + } + + for(size_t i = 0; i < point_data.size(); ++i) + { + Point3D& pt = point_data[i]; + out << pt.xyz[0] << ' ' << pt.xyz[1] << ' ' << pt.xyz[2] << '\n'; + } +} + +bool LoadModelFile(const char* name, std::vector& camera_data, std::vector& point_data, + std::vector& measurements, std::vector& ptidx, std::vector& camidx, + std::vector& names, std::vector& ptc) +{ + if(name == NULL)return false; + std::ifstream in(name); + + LOG_OUT() << "Loading cameras/points: " << name <<"\n" ; + if(!in.is_open()) return false; + + if(strstr(name, ".nvm"))return LoadNVM(in, camera_data, point_data, measurements, ptidx, camidx, names, ptc); + else if(strstr(name, ".out")) return LoadBundlerOut(name, in, camera_data, point_data, measurements, ptidx, camidx, names, ptc); + else return LoadBundlerModel(in, camera_data, point_data, measurements, ptidx, camidx); +} + + +float random_ratio(float percent) +{ + return (rand() % 101 - 50) * 0.02f * percent + 1.0f; +} + +void AddNoise(std::vector& camera_data, std::vector& point_data, float percent) +{ + std::srand((unsigned int) time(NULL)); + for(size_t i = 0; i < camera_data.size(); ++i) + { + camera_data[i].f *= random_ratio(percent); + camera_data[i].t[0] *= random_ratio(percent); + camera_data[i].t[1] *= random_ratio(percent); + camera_data[i].t[2] *= random_ratio(percent); + double e[3]; + camera_data[i].GetRodriguesRotation(e); + e[0] *= random_ratio(percent); + e[1] *= random_ratio(percent); + e[2] *= random_ratio(percent); + camera_data[i].SetRodriguesRotation(e); + } + + for(size_t i = 0; i < point_data.size(); ++i) + { + point_data[i].xyz[0] *= random_ratio(percent); + point_data[i].xyz[1] *= random_ratio(percent); + point_data[i].xyz[2] *= random_ratio(percent); + } +} + +void AddStableNoise(std::vector& camera_data, std::vector& point_data, + const std::vector& ptidx, const std::vector& camidx, float percent) +{ + /// + std::srand((unsigned int) time(NULL)); + //do not modify the visibility status.. + std::vector zz0(ptidx.size()); + std::vector backup = camera_data; + std::vector vx(point_data.size()), vy(point_data.size()), vz(point_data.size()); + for(size_t i = 0; i < point_data.size(); ++i) + { + Point3D& pt = point_data[i]; + vx[i] = pt.xyz[0]; + vy[i] = pt.xyz[1]; + vz[i] = pt.xyz[2]; + } + + //find out the median location of all the 3D points. + size_t median_idx = point_data.size() / 2; + + std::nth_element(vx.begin(), vx.begin() + median_idx, vx.end()); + std::nth_element(vy.begin(), vy.begin() + median_idx, vy.end()); + std::nth_element(vz.begin(), vz.begin() + median_idx, vz.end()); + float cx = vx[median_idx], cy = vy[median_idx], cz = vz[median_idx]; + + for(size_t i = 0; i < ptidx.size(); ++i) + { + CameraT& cam = camera_data[camidx[i]]; + Point3D& pt = point_data[ptidx[i]]; + zz0[i] = cam.m[2][0] * pt.xyz[0] + cam.m[2][1] * pt.xyz[1] + cam.m[2][2] * pt.xyz[2] + cam.t[2]; + } + + std::vector z2 = zz0; median_idx = ptidx.size() / 2; + std::nth_element(z2.begin(), z2.begin() + median_idx, z2.end()); + float mz = z2[median_idx]; // median depth + float dist_noise_base = mz * 0.2f; + + ///////////////////////////////////////////////// + //modify points first.. + for(size_t i = 0; i < point_data.size(); ++i) + { + Point3D& pt = point_data[i]; + pt.xyz[0] = pt.xyz[0] - cx + dist_noise_base * random_ratio(percent); + pt.xyz[1] = pt.xyz[1] - cy + dist_noise_base * random_ratio(percent); + pt.xyz[2] = pt.xyz[2] - cz + dist_noise_base * random_ratio(percent); + } + + std::vector need_modification(camera_data.size(), true); + int invalid_count = 0, modify_iteration = 1; + + do + { + if(invalid_count) LOG_OUT() << "NOTE" << std::setw(2) << modify_iteration + << ": modify " << invalid_count << " camera to fix visibility\n"; + + ////////////////////////////////////////////////////// + for(size_t i = 0; i < camera_data.size(); ++i) + { + if(!need_modification[i])continue; + CameraT & cam = camera_data[i]; + double e[3], c[3]; cam = backup[i]; + cam.f *= random_ratio(percent); + + /////////////////////////////////////////////////////////// + cam.GetCameraCenter(c); + c[0] = c[0] - cx + dist_noise_base * random_ratio(percent); + c[1] = c[1] - cy + dist_noise_base * random_ratio(percent); + c[2] = c[2] - cz + dist_noise_base * random_ratio(percent); + + /////////////////////////////////////////////////////////// + cam.GetRodriguesRotation(e); + e[0] *= random_ratio(percent); + e[1] *= random_ratio(percent); + e[2] *= random_ratio(percent); + + /////////////////////////////////////////////////////////// + cam.SetRodriguesRotation(e); + cam.SetCameraCenterAfterRotation(c); + } + std::vector invalidc(camera_data.size(), false); + + invalid_count = 0; + for(size_t i = 0; i < ptidx.size(); ++i) + { + int cid = camidx[i]; + if(need_modification[cid] ==false) continue; + if(invalidc[cid])continue; + CameraT& cam = camera_data[cid]; + Point3D& pt = point_data[ptidx[i]]; + float z = cam.m[2][0] * pt.xyz[0] + cam.m[2][1] * pt.xyz[1] + cam.m[2][2] * pt.xyz[2] + cam.t[2]; + if (z * zz0[i] > 0)continue; + if (zz0[i] == 0 && z > 0) continue; + invalid_count++; + invalidc[cid] = true; + } + + need_modification = invalidc; + modify_iteration++; + + }while(invalid_count && modify_iteration < 20); + +} + +void ExamineVisiblity(const char* input_filename ) +{ + + ////////////// + std::vector camera_data; + std::vector point_data; + std::vector ptidx, camidx; + std::vector measurements; + std::ifstream in (input_filename); + LoadBundlerModel(in, camera_data, point_data, measurements, ptidx, camidx); + + //////////////// + int count = 0; double d1 = 100, d2 = 100; + LOG_OUT() << "checking visibility...\n"; + std::vector zz(ptidx.size()); + for(size_t i = 0; i < ptidx.size(); ++i) + { + CameraD& cam = camera_data[camidx[i]]; + Point3B& pt = point_data[ptidx[i]]; + double dz = cam.m[2][0] * pt.xyz[0] + cam.m[2][1] * pt.xyz[1] + cam.m[2][2] * pt.xyz[2] + cam.t[2]; + //double dx = cam.m[0][0] * pt.xyz[0] + cam.m[0][1] * pt.xyz[1] + cam.m[0][2] * pt.xyz[2] + cam.t[0]; + //double dy = cam.m[1][0] * pt.xyz[0] + cam.m[1][1] * pt.xyz[1] + cam.m[1][2] * pt.xyz[2] + cam.t[1]; + + //////////////////////////////////////// + float c[3]; cam.GetCameraCenter(c); + + CameraT camt; camt.SetCameraT(cam); + Point3D ptt; ptt.SetPoint(pt.xyz); + double fz = camt.m[2][0] * ptt.xyz[0] + camt.m[2][1] * ptt.xyz[1] + camt.m[2][2] * ptt.xyz[2] + camt.t[2]; + double fz2 = camt.m[2][0] * (ptt.xyz[0] - c[0]) + camt.m[2][1] * (ptt.xyz[1] - c[1]) + + camt.m[2][2] * (ptt.xyz[2] - c[2]); + + + //if(dz == 0 && fz == 0) continue; + + if(dz * fz <= 0 || fz == 0) + { + LOG_OUT() << "cam " << camidx[i] //<& camera_data, std::vector& point_data, + std::vector& ptidx, std::vector& camidx, + std::vector& measurements, std::vector& names, std::vector& ptc) +{ + std::vector zz(ptidx.size()); + for(size_t i = 0; i < ptidx.size(); ++i) + { + CameraT& cam = camera_data[camidx[i]]; + Point3D& pt = point_data[ptidx[i]]; + zz[i] = cam.m[2][0] * pt.xyz[0] + cam.m[2][1] * pt.xyz[1] + cam.m[2][2] * pt.xyz[2] + cam.t[2]; + } + size_t median_idx = ptidx.size() / 2; + std::nth_element(zz.begin(), zz.begin() + median_idx, zz.end()); + float dist_threshold = zz[median_idx] * 0.001f; + + //keep removing 3D points. until all of them are infront of the cameras.. + std::vector pmask(point_data.size(), true); + int points_removed = 0; + for(size_t i = 0; i < ptidx.size(); ++i) + { + int cid = camidx[i], pid = ptidx[i]; + if(!pmask[pid])continue; + CameraT& cam = camera_data[cid]; + Point3D& pt = point_data[pid]; + bool visible = (cam.m[2][0] * pt.xyz[0] + cam.m[2][1] * pt.xyz[1] + cam.m[2][2] * pt.xyz[2] + cam.t[2] > dist_threshold); + pmask[pid] = visible; //this point should be removed + if(!visible) points_removed++; + } + if(points_removed == 0) return false; + std::vector cv(camera_data.size(), 0); + //should any cameras be removed ? + int min_observation = 20; //cameras should see at least 20 points + + do + { + //count visible points for each camera + std::fill(cv.begin(), cv.end(), 0); + for(size_t i = 0; i < ptidx.size(); ++i) + { + int cid = camidx[i], pid = ptidx[i]; + if(pmask[pid]) cv[cid]++; + } + + //check if any more points should be removed + std::vector pv(point_data.size(), 0); + for(size_t i = 0; i < ptidx.size(); ++i) + { + int cid = camidx[i], pid = ptidx[i]; + if(!pmask[pid]) continue; //point already removed + if(cv[cid] < min_observation) //this camera shall be removed. + { + /// + }else + { + pv[pid]++; + } + } + + points_removed = 0; + for(size_t i = 0; i < point_data.size(); ++i) + { + if(pmask[i] == false) continue; + if(pv[i] >= 2) continue; + pmask[i] = false; + points_removed++; + } + }while(points_removed > 0); + + //////////////////////////////////// + std::vector cmask(camera_data.size(), true); + for(size_t i = 0; i < camera_data.size(); ++i) cmask[i] = cv[i] >= min_observation; + //////////////////////////////////////////////////////// + + std::vector cidx(camera_data.size()); + std::vector pidx(point_data.size()); + + + + + ///modified model. + std::vector camera_data2; + std::vector point_data2; + std::vector ptidx2; + std::vector camidx2; + std::vector measurements2; + std::vector names2; + std::vector ptc2; + + + // + if(names.size() < camera_data.size()) names.resize(camera_data.size(),std::string("unknown")); + if(ptc.size() < 3 * point_data.size()) ptc.resize(point_data.size() * 3, 0); + + ////////////////////////////// + int new_camera_count = 0, new_point_count = 0; + for(size_t i = 0; i < camera_data.size(); ++i) + { + if(!cmask[i])continue; + camera_data2.push_back(camera_data[i]); + names2.push_back(names[i]); + cidx[i] = new_camera_count++; + } + + for(size_t i = 0; i < point_data.size(); ++i) + { + if(!pmask[i])continue; + point_data2.push_back(point_data[i]); + ptc.push_back(ptc[i]); + pidx[i] = new_point_count++; + } + + int new_observation_count = 0; + for(size_t i = 0; i < ptidx.size(); ++i) + { + int pid = ptidx[i], cid = camidx[i]; + if(!pmask[pid] || ! cmask[cid]) continue; + ptidx2.push_back(pidx[pid]); + camidx2.push_back(cidx[cid]); + measurements2.push_back(measurements[i]); + new_observation_count++; + } + + LOG_OUT() << "NOTE: removing " << (camera_data.size() - new_camera_count) << " cameras; "<< (point_data.size() - new_point_count) + << " 3D Points; " << (measurements.size() - new_observation_count) << " Observations;\n"; + + camera_data2.swap(camera_data); names2.swap(names); + point_data2.swap(point_data); ptc2.swap(ptc); + ptidx2.swap(ptidx); camidx2.swap(camidx); + measurements2.swap(measurements); + + return true; +} + +void SaveModelFile(const char* outpath, std::vector& camera_data, std::vector& point_data, + std::vector& measurements, std::vector& ptidx, std::vector& camidx, + std::vector& names, std::vector& ptc) +{ + if(outpath == NULL) return; + if(strstr(outpath, ".nvm")) + SaveNVM(outpath, camera_data, point_data, measurements, ptidx, camidx, names, ptc); + else if(strstr(outpath, ".out")) + SaveBundlerOut(outpath, camera_data, point_data, measurements, ptidx, camidx, names, ptc); + else + SaveBundlerModel(outpath, camera_data, point_data, measurements, ptidx, camidx); +} + +} // namespace PBA diff --git a/apps/ReconstructMesh/CMakeLists.txt b/apps/ReconstructMesh/CMakeLists.txt new file mode 100644 index 0000000..4b6aac7 --- /dev/null +++ b/apps/ReconstructMesh/CMakeLists.txt @@ -0,0 +1,13 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(ReconstructMesh "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS ReconstructMesh + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/ReconstructMesh/ReconstructMesh.cpp b/apps/ReconstructMesh/ReconstructMesh.cpp new file mode 100644 index 0000000..f7c0f6d --- /dev/null +++ b/apps/ReconstructMesh/ReconstructMesh.cpp @@ -0,0 +1,498 @@ +/* + * ReconstructMesh.cpp + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#include + +using namespace MVS; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("ReconstructMesh") + +// uncomment to enable multi-threading based on OpenMP +#ifdef _USE_OPENMP +#define RECMESH_USE_OPENMP +#endif + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { +String strInputFileName; +String strPointCloudFileName; +String strOutputFileName; +String strMeshFileName; +String strImportROIFileName; +String strImagePointsFileName; +bool bMeshExport; +float fDistInsert; +bool bUseOnlyROI; +bool bUseConstantWeight; +bool bUseFreeSpaceSupport; +float fThicknessFactor; +float fQualityFactor; +float fDecimateMesh; +unsigned nTargetFaceNum; +float fRemoveSpurious; +bool bRemoveSpikes; +unsigned nCloseHoles; +unsigned nSmoothMesh; +float fEdgeLength; +bool bCrop2ROI; +float fBorderROI; +float fSplitMaxArea; +unsigned nArchiveType; +int nProcessPriority; +unsigned nMaxThreads; +String strExportType; +String strConfigFileName; +boost::program_options::variables_map vm; +} // namespace OPT + +class Application { +public: + Application() {} + ~Application() { Finalize(); } + + bool Initialize(size_t argc, LPCTSTR* argv); + void Finalize(); +}; // Application + +// initialize and parse the command line parameters +bool Application::Initialize(size_t argc, LPCTSTR* argv) +{ + // initialize log and console + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "produce this help message") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("export-type", boost::program_options::value(&OPT::strExportType)->default_value(_T("ply")), "file type used to export the 3D scene (ply or obj)") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: -1-interface, 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + #ifdef _USE_CUDA + ("cuda-device", boost::program_options::value(&CUDA::desiredDeviceID)->default_value(-1), "CUDA device number to be used to reconstruct the mesh (-2 - CPU processing, -1 - best GPU, >=0 - device index)") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description config_main("Reconstruct options"); + config_main.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input filename containing camera poses and image list") + ("pointcloud-file,p", boost::program_options::value(&OPT::strPointCloudFileName), "dense point-cloud with views file name to reconstruct (overwrite existing point-cloud)") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the mesh") + ("min-point-distance,d", boost::program_options::value(&OPT::fDistInsert)->default_value(2.5f), "minimum distance in pixels between the projection of two 3D points to consider them different while triangulating (0 - disabled)") + ("integrate-only-roi", boost::program_options::value(&OPT::bUseOnlyROI)->default_value(false), "use only the points inside the ROI") + ("constant-weight", boost::program_options::value(&OPT::bUseConstantWeight)->default_value(true), "considers all view weights 1 instead of the available weight") + ("free-space-support,f", boost::program_options::value(&OPT::bUseFreeSpaceSupport)->default_value(false), "exploits the free-space support in order to reconstruct weakly-represented surfaces") + ("thickness-factor", boost::program_options::value(&OPT::fThicknessFactor)->default_value(1.f), "multiplier adjusting the minimum thickness considered during visibility weighting") + ("quality-factor", boost::program_options::value(&OPT::fQualityFactor)->default_value(1.f), "multiplier adjusting the quality weight considered during graph-cut") + ; + boost::program_options::options_description config_clean("Clean options"); + config_clean.add_options() + ("decimate", boost::program_options::value(&OPT::fDecimateMesh)->default_value(1.f), "decimation factor in range (0..1] to be applied to the reconstructed surface (1 - disabled)") + ("target-face-num", boost::program_options::value(&OPT::nTargetFaceNum)->default_value(0), "target number of faces to be applied to the reconstructed surface. (0 - disabled)") + ("remove-spurious", boost::program_options::value(&OPT::fRemoveSpurious)->default_value(20.f), "spurious factor for removing faces with too long edges or isolated components (0 - disabled)") + ("remove-spikes", boost::program_options::value(&OPT::bRemoveSpikes)->default_value(true), "flag controlling the removal of spike faces") + ("close-holes", boost::program_options::value(&OPT::nCloseHoles)->default_value(30), "try to close small holes in the reconstructed surface (0 - disabled)") + ("smooth", boost::program_options::value(&OPT::nSmoothMesh)->default_value(2), "number of iterations to smooth the reconstructed surface (0 - disabled)") + ("edge-length", boost::program_options::value(&OPT::fEdgeLength)->default_value(0.f), "remesh such that the average edge length is this size (0 - disabled)") + ("roi-border", boost::program_options::value(&OPT::fBorderROI)->default_value(0), "add a border to the region-of-interest when cropping the scene (0 - disabled, >0 - percentage, <0 - absolute)") + ("crop-to-roi", boost::program_options::value(&OPT::bCrop2ROI)->default_value(true), "crop scene using the region-of-interest") + ; + + // hidden options, allowed both on command line and + // in config file, but will not be shown to the user + boost::program_options::options_description hidden("Hidden options"); + hidden.add_options() + ("mesh-file", boost::program_options::value(&OPT::strMeshFileName), "mesh file name to clean (skips the reconstruction step)") + ("mesh-export", boost::program_options::value(&OPT::bMeshExport)->default_value(false), "just export the mesh contained in loaded project") + ("split-max-area", boost::program_options::value(&OPT::fSplitMaxArea)->default_value(0.f), "maximum surface area that a sub-mesh can contain (0 - disabled)") + ("import-roi-file", boost::program_options::value(&OPT::strImportROIFileName), "ROI file name to be imported into the scene") + ("image-points-file", boost::program_options::value(&OPT::strImagePointsFileName), "input filename containing the list of points from an image to project on the mesh (optional)") + ; + + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(config_main).add(config_clean).add(hidden); + + boost::program_options::options_description config_file_options; + config_file_options.add(config_main).add(config_clean).add(hidden); + + boost::program_options::positional_options_description p; + p.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(p).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + INIT_WORKING_FOLDER; + // parse configuration file + std::ifstream ifs(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (ifs) { + boost::program_options::store(parse_config_file(ifs, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // initialize the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-")+Util::getUniqueName(0)+_T(".log")).c_str()); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // validate input + Util::ensureValidPath(OPT::strInputFileName); + if (OPT::vm.count("help") || OPT::strInputFileName.empty()) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(config_main).add(config_clean); + GET_LOG() << visible; + } + if (OPT::strInputFileName.empty()) + return false; + OPT::strExportType = OPT::strExportType.ToLower() == _T("obj") ? _T(".obj") : _T(".ply"); + + // initialize optional options + Util::ensureValidPath(OPT::strPointCloudFileName); + Util::ensureValidPath(OPT::strOutputFileName); + Util::ensureValidPath(OPT::strImportROIFileName); + Util::ensureValidPath(OPT::strImagePointsFileName); + Util::ensureValidPath(OPT::strMeshFileName); + if (OPT::strPointCloudFileName.empty() && (ARCHIVE_TYPE)OPT::nArchiveType == ARCHIVE_MVS) + OPT::strPointCloudFileName = Util::getFileFullName(OPT::strInputFileName) + _T(".ply"); + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = Util::getFileFullName(OPT::strInputFileName) + _T("_mesh.mvs"); + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::Finalize() +{ + MVS::Finalize(); + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +} // unnamed namespace + + +// export 3D coordinates corresponding to 2D coordinates provided by inputFileName: +// parse image point list; first line is the name of the image to project, +// each consequent line store the xy coordinates to project: +// +// +// +// ... +// +// for example: +// N01.JPG 3 +// 3090 2680 +// 3600 2100 +// 3640 2190 +bool Export3DProjections(Scene& scene, const String& inputFileName) { + SML smlPointList(_T("ImagePoints")); + smlPointList.Load(inputFileName); + ASSERT(smlPointList.GetArrChildren().size() <= 1); + IDX idx(0); + + // read image name + size_t argc; + CAutoPtrArr argv; + while (true) { + argv = Util::CommandLineToArgvA(smlPointList.GetValue(idx).val, argc); + if (argc > 0 && argv[0][0] != _T('#')) + break; + if (++idx == smlPointList.size()) + return false; + } + if (argc < 2) + return false; + String imgName(argv[0]); + IIndex imgID(NO_ID); + for (const Image& imageData : scene.images) { + if (!imageData.IsValid()) + continue; + if (imageData.name.substr(imageData.name.size() - imgName.size()) == imgName) { + imgID = imageData.ID; + break; + } + } + if (imgID == NO_ID) { + VERBOSE("Unable to find image named: %s", imgName.c_str()); + return false; + } + + // read image points + std::vector imagePoints; + while (++idx != smlPointList.size()) { + // parse image element + const String& line(smlPointList.GetValue(idx).val); + argv = Util::CommandLineToArgvA(line, argc); + if (argc > 0 && argv[0][0] == _T('#')) + continue; + if (argc < 2) { + VERBOSE("Invalid image coordinates: %s", line.c_str()); + continue; + } + const Point2f pt( + String::FromString(argv[0], -1), + String::FromString(argv[1], -1)); + if (pt.x > 0 && pt.y > 0) + imagePoints.emplace_back(pt); + } + if (imagePoints.empty()) { + VERBOSE("Unable to read image points from: %s", imgName.c_str()); + return false; + } + + // prepare output file + String outFileName(Util::insertBeforeFileExt(inputFileName, "_3D")); + File oStream(outFileName, File::WRITE, File::CREATE | File::TRUNCATE); + if (!oStream.isOpen()) { + VERBOSE("Unable to open output file: %s", outFileName.c_str()); + return false; + } + + // print image name + oStream.print("%s %u\n", imgName.c_str(), imagePoints.size()); + + // init mesh octree + const Mesh::Octree octree(scene.mesh.vertices, [](Mesh::Octree::IDX_TYPE size, Mesh::Octree::Type /*radius*/) { + return size > 256; + }); + scene.mesh.ListIncidenteFaces(); + + // save 3D coord in the output file + const Image& imgToExport = scene.images[imgID]; + for (const Point2f& pt : imagePoints) { + // define ray from camera center to each x,y image coord + const Ray3 ray(imgToExport.camera.C, normalized(imgToExport.camera.RayPoint(pt))); + // find ray intersection with the mesh + const IntersectRayMesh intRay(octree, ray, scene.mesh); + if (intRay.pick.IsValid()) { + const Point3d ptHit(ray.GetPoint(intRay.pick.dist)); + oStream.print("%.7f %.7f %.7f\n", ptHit.x, ptHit.y, ptHit.z); + } else + oStream.print("NA\n"); + } + return true; +} + +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application application; + if (!application.Initialize(argc, argv)) + return EXIT_FAILURE; + + Scene scene(OPT::nMaxThreads); + // load project + const Scene::SCENE_TYPE sceneType(scene.Load(MAKE_PATH_SAFE(OPT::strInputFileName), + OPT::fSplitMaxArea > 0 || OPT::fDecimateMesh < 1 || OPT::nTargetFaceNum > 0 || !OPT::strImportROIFileName.empty())); + if (sceneType == Scene::SCENE_NA) + return EXIT_FAILURE; + if (!OPT::strPointCloudFileName.empty() && (File::isFile(MAKE_PATH_SAFE(OPT::strPointCloudFileName)) ? + !scene.pointcloud.Load(MAKE_PATH_SAFE(OPT::strPointCloudFileName)) : + !scene.pointcloud.IsValid())) { + VERBOSE("error: cannot load point-cloud file"); + return EXIT_FAILURE; + } + if (!OPT::strMeshFileName.empty() && !scene.mesh.Load(MAKE_PATH_SAFE(OPT::strMeshFileName))) { + VERBOSE("error: cannot load mesh file"); + return EXIT_FAILURE; + } + const String baseFileName(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName))); + if (OPT::fSplitMaxArea > 0) { + // split mesh using max-area constraint + Mesh::FacesChunkArr chunks; + if (scene.mesh.Split(chunks, OPT::fSplitMaxArea)) + scene.mesh.Save(chunks, baseFileName); + return EXIT_SUCCESS; + } + + if (!OPT::strImportROIFileName.empty()) { + std::ifstream fs(MAKE_PATH_SAFE(OPT::strImportROIFileName)); + if (!fs) + return EXIT_FAILURE; + fs >> scene.obb; + if (OPT::bCrop2ROI && !scene.mesh.IsEmpty() && !scene.IsValid()) { + TD_TIMER_START(); + const size_t numVertices = scene.mesh.vertices.size(); + const size_t numFaces = scene.mesh.faces.size(); + scene.mesh.RemoveFacesOutside(scene.obb); + VERBOSE("Mesh trimmed to ROI: %u vertices and %u faces removed (%s)", + numVertices-scene.mesh.vertices.size(), numFaces-scene.mesh.faces.size(), TD_TIMER_GET_FMT().c_str()); + scene.mesh.Save(baseFileName+OPT::strExportType); + return EXIT_SUCCESS; + } + } + + if (!OPT::strImagePointsFileName.empty() && !scene.mesh.IsEmpty()) { + Export3DProjections(scene, MAKE_PATH_SAFE(OPT::strImagePointsFileName)); + return EXIT_SUCCESS; + } + + if (OPT::bMeshExport) { + // check there is a mesh to export + if (scene.mesh.IsEmpty()) + return EXIT_FAILURE; + // save mesh + const String fileName(MAKE_PATH_SAFE(OPT::strOutputFileName)); + scene.mesh.Save(fileName); + #if TD_VERBOSE != TD_VERBOSE_OFF + if (VERBOSITY_LEVEL > 2) + scene.ExportCamerasMLP(baseFileName+_T(".mlp"), fileName); + #endif + } else { + const OBB3f initialOBB(scene.obb); + if (OPT::fBorderROI > 0) + scene.obb.EnlargePercent(OPT::fBorderROI); + else if (OPT::fBorderROI < 0) + scene.obb.Enlarge(-OPT::fBorderROI); + if (OPT::strMeshFileName.empty() && scene.mesh.IsEmpty()) { + // reset image resolution to the original size and + // make sure the image neighbors are initialized before deleting the point-cloud + #ifdef RECMESH_USE_OPENMP + bool bAbort(false); + #pragma omp parallel for + for (int_t idx=0; idx<(int_t)scene.images.size(); ++idx) { + #pragma omp flush (bAbort) + if (bAbort) + continue; + const uint32_t idxImage((uint32_t)idx); + #else + FOREACH(idxImage, scene.images) { + #endif + Image& imageData = scene.images[idxImage]; + if (!imageData.IsValid()) + continue; + // reset image resolution + if (!imageData.ReloadImage(0, false)) { + #ifdef RECMESH_USE_OPENMP + bAbort = true; + #pragma omp flush (bAbort) + continue; + #else + return EXIT_FAILURE; + #endif + } + imageData.UpdateCamera(scene.platforms); + // select neighbor views + if (imageData.neighbors.empty()) { + IndexArr points; + scene.SelectNeighborViews(idxImage, points); + } + } + #ifdef RECMESH_USE_OPENMP + if (bAbort) + return EXIT_FAILURE; + #endif + // reconstruct a coarse mesh from the given point-cloud + TD_TIMER_START(); + if (OPT::bUseConstantWeight) + scene.pointcloud.pointWeights.Release(); + if (!scene.ReconstructMesh(OPT::fDistInsert, OPT::bUseFreeSpaceSupport, OPT::bUseOnlyROI, 4, OPT::fThicknessFactor, OPT::fQualityFactor)) + return EXIT_FAILURE; + VERBOSE("Mesh reconstruction completed: %u vertices, %u faces (%s)", scene.mesh.vertices.GetSize(), scene.mesh.faces.GetSize(), TD_TIMER_GET_FMT().c_str()); + #if TD_VERBOSE != TD_VERBOSE_OFF + if (VERBOSITY_LEVEL > 2) { + // dump raw mesh + scene.mesh.Save(baseFileName+_T("_raw")+OPT::strExportType); + } + #endif + } else if (!OPT::strMeshFileName.empty()) { + // load existing mesh to clean + scene.mesh.Load(MAKE_PATH_SAFE(OPT::strMeshFileName)); + } + + // clean the mesh + if (OPT::bCrop2ROI && scene.IsBounded()) { + TD_TIMER_START(); + const size_t numVertices = scene.mesh.vertices.size(); + const size_t numFaces = scene.mesh.faces.size(); + scene.mesh.RemoveFacesOutside(scene.obb); + VERBOSE("Mesh trimmed to ROI: %u vertices and %u faces removed (%s)", + numVertices-scene.mesh.vertices.size(), numFaces-scene.mesh.faces.size(), TD_TIMER_GET_FMT().c_str()); + } + const float fDecimate(OPT::nTargetFaceNum ? static_cast(OPT::nTargetFaceNum) / scene.mesh.faces.size() : OPT::fDecimateMesh); + scene.mesh.Clean(fDecimate, OPT::fRemoveSpurious, OPT::bRemoveSpikes, OPT::nCloseHoles, OPT::nSmoothMesh, OPT::fEdgeLength, false); + scene.mesh.Clean(1.f, 0.f, OPT::bRemoveSpikes, OPT::nCloseHoles, 0u, 0.f, false); // extra cleaning trying to close more holes + scene.mesh.Clean(1.f, 0.f, false, 0u, 0u, 0.f, true); // extra cleaning to remove non-manifold problems created by closing holes + scene.obb = initialOBB; + + // save the final mesh + scene.mesh.Save(baseFileName+OPT::strExportType); + #if TD_VERBOSE != TD_VERBOSE_OFF + if (VERBOSITY_LEVEL > 2) + scene.ExportCamerasMLP(baseFileName+_T(".mlp"), baseFileName+OPT::strExportType); + #endif + if ((ARCHIVE_TYPE)OPT::nArchiveType != ARCHIVE_MVS || sceneType != Scene::SCENE_INTERFACE) + scene.Save(baseFileName+_T(".mvs"), (ARCHIVE_TYPE)OPT::nArchiveType); + } + + if (!OPT::strImagePointsFileName.empty()) { + Export3DProjections(scene, MAKE_PATH_SAFE(OPT::strImagePointsFileName)); + return EXIT_SUCCESS; + } + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/RefineMesh/CMakeLists.txt b/apps/RefineMesh/CMakeLists.txt new file mode 100644 index 0000000..26a6f84 --- /dev/null +++ b/apps/RefineMesh/CMakeLists.txt @@ -0,0 +1,13 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(RefineMesh "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS RefineMesh + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/RefineMesh/RefineMesh.cpp b/apps/RefineMesh/RefineMesh.cpp new file mode 100644 index 0000000..3aa6230 --- /dev/null +++ b/apps/RefineMesh/RefineMesh.cpp @@ -0,0 +1,266 @@ +/* + * RefineMesh.cpp + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#include + +using namespace MVS; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("RefineMesh") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { +String strInputFileName; +String strMeshFileName; +String strOutputFileName; +unsigned nResolutionLevel; +unsigned nMinResolution; +unsigned nMaxViews; +float fDecimateMesh; +unsigned nCloseHoles; +unsigned nEnsureEdgeSize; +unsigned nScales; +float fScaleStep; +unsigned nReduceMemory; +unsigned nAlternatePair; +float fRegularityWeight; +float fRatioRigidityElasticity; +unsigned nMaxFaceArea; +float fPlanarVertexRatio; +float fGradientStep; +unsigned nArchiveType; +int nProcessPriority; +unsigned nMaxThreads; +String strExportType; +String strConfigFileName; +boost::program_options::variables_map vm; +} // namespace OPT + +class Application { +public: + Application() {} + ~Application() { Finalize(); } + + bool Initialize(size_t argc, LPCTSTR* argv); + void Finalize(); +}; // Application + +// initialize and parse the command line parameters +bool Application::Initialize(size_t argc, LPCTSTR* argv) +{ + // initialize log and console + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "produce this help message") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("export-type", boost::program_options::value(&OPT::strExportType)->default_value(_T("ply")), "file type used to export the 3D scene (ply or obj)") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: -1-interface, 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + #ifdef _USE_CUDA + ("cuda-device", boost::program_options::value(&CUDA::desiredDeviceID)->default_value(-2), "CUDA device number to be used for mesh refinement (-2 - CPU processing, -1 - best GPU, >=0 - device index)") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description config("Refine options"); + config.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input filename containing camera poses and image list") + ("mesh-file,m", boost::program_options::value(&OPT::strMeshFileName), "mesh file name to refine (overwrite existing mesh)") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the mesh") + ("resolution-level", boost::program_options::value(&OPT::nResolutionLevel)->default_value(0), "how many times to scale down the images before mesh refinement") + ("min-resolution", boost::program_options::value(&OPT::nMinResolution)->default_value(640), "do not scale images lower than this resolution") + ("max-views", boost::program_options::value(&OPT::nMaxViews)->default_value(8), "maximum number of neighbor images used to refine the mesh") + ("decimate", boost::program_options::value(&OPT::fDecimateMesh)->default_value(0.f), "decimation factor in range [0..1] to be applied to the input surface before refinement (0 - auto, 1 - disabled)") + ("close-holes", boost::program_options::value(&OPT::nCloseHoles)->default_value(30), "try to close small holes in the input surface (0 - disabled)") + ("ensure-edge-size", boost::program_options::value(&OPT::nEnsureEdgeSize)->default_value(1), "ensure edge size and improve vertex valence of the input surface (0 - disabled, 1 - auto, 2 - force)") + ("max-face-area", boost::program_options::value(&OPT::nMaxFaceArea)->default_value(32), "maximum face area projected in any pair of images that is not subdivided (0 - disabled)") + ("scales", boost::program_options::value(&OPT::nScales)->default_value(2), "how many iterations to run mesh optimization on multi-scale images") + ("scale-step", boost::program_options::value(&OPT::fScaleStep)->default_value(0.5f), "image scale factor used at each mesh optimization step") + ("alternate-pair", boost::program_options::value(&OPT::nAlternatePair)->default_value(0), "refine mesh using an image pair alternatively as reference (0 - both, 1 - alternate, 2 - only left, 3 - only right)") + ("regularity-weight", boost::program_options::value(&OPT::fRegularityWeight)->default_value(0.2f), "scalar regularity weight to balance between photo-consistency and regularization terms during mesh optimization") + ("rigidity-elasticity-ratio", boost::program_options::value(&OPT::fRatioRigidityElasticity)->default_value(0.9f), "scalar ratio used to compute the regularity gradient as a combination of rigidity and elasticity") + ("gradient-step", boost::program_options::value(&OPT::fGradientStep)->default_value(45.05f), "gradient step to be used instead (0 - auto)") + ("planar-vertex-ratio", boost::program_options::value(&OPT::fPlanarVertexRatio)->default_value(0.f), "threshold used to remove vertices on planar patches (0 - disabled)") + ("reduce-memory", boost::program_options::value(&OPT::nReduceMemory)->default_value(1), "recompute some data in order to reduce memory requirements") + ; + + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(config); + + boost::program_options::options_description config_file_options; + config_file_options.add(config); + + boost::program_options::positional_options_description p; + p.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(p).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + INIT_WORKING_FOLDER; + // parse configuration file + std::ifstream ifs(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (ifs) { + boost::program_options::store(parse_config_file(ifs, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // initialize the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-")+Util::getUniqueName(0)+_T(".log")).c_str()); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // validate input + Util::ensureValidPath(OPT::strInputFileName); + if (OPT::vm.count("help") || OPT::strInputFileName.empty()) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(config); + GET_LOG() << visible; + } + if (OPT::strInputFileName.empty()) + return false; + OPT::strExportType = OPT::strExportType.ToLower() == _T("obj") ? _T(".obj") : _T(".ply"); + + // initialize optional options + Util::ensureValidPath(OPT::strMeshFileName); + Util::ensureValidPath(OPT::strOutputFileName); + if (OPT::strMeshFileName.empty() && (ARCHIVE_TYPE)OPT::nArchiveType == ARCHIVE_MVS) + OPT::strMeshFileName = Util::getFileFullName(OPT::strInputFileName) + _T(".ply"); + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = Util::getFileFullName(OPT::strInputFileName) + _T("_refine.mvs"); + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::Finalize() +{ + MVS::Finalize(); + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +} // unnamed namespace + +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application application; + if (!application.Initialize(argc, argv)) + return EXIT_FAILURE; + + Scene scene(OPT::nMaxThreads); + // load and refine the coarse mesh + const Scene::SCENE_TYPE sceneType(scene.Load(MAKE_PATH_SAFE(OPT::strInputFileName))); + if (sceneType == Scene::SCENE_NA) + return EXIT_FAILURE; + if (!OPT::strMeshFileName.empty() && !scene.mesh.Load(MAKE_PATH_SAFE(OPT::strMeshFileName))) { + VERBOSE("error: cannot load mesh file"); + return EXIT_FAILURE; + } + if (scene.mesh.IsEmpty()) { + VERBOSE("error: empty initial mesh"); + return EXIT_FAILURE; + } + TD_TIMER_START(); + #ifdef _USE_CUDA + if (CUDA::desiredDeviceID < -1 || + !scene.RefineMeshCUDA(OPT::nResolutionLevel, OPT::nMinResolution, OPT::nMaxViews, + OPT::fDecimateMesh, OPT::nCloseHoles, OPT::nEnsureEdgeSize, + OPT::nMaxFaceArea, + OPT::nScales, OPT::fScaleStep, + OPT::nAlternatePair>10 ? OPT::nAlternatePair%10 : 0, + OPT::fRegularityWeight, + OPT::fRatioRigidityElasticity, + OPT::fGradientStep)) + #endif + if (!scene.RefineMesh(OPT::nResolutionLevel, OPT::nMinResolution, OPT::nMaxViews, + OPT::fDecimateMesh, OPT::nCloseHoles, OPT::nEnsureEdgeSize, + OPT::nMaxFaceArea, + OPT::nScales, OPT::fScaleStep, + OPT::nAlternatePair, + OPT::fRegularityWeight, + OPT::fRatioRigidityElasticity, + OPT::fGradientStep, + OPT::fPlanarVertexRatio, + OPT::nReduceMemory)) + return EXIT_FAILURE; + VERBOSE("Mesh refinement completed: %u vertices, %u faces (%s)", scene.mesh.vertices.GetSize(), scene.mesh.faces.GetSize(), TD_TIMER_GET_FMT().c_str()); + + // save the final mesh + const String baseFileName(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName))); + scene.mesh.Save(baseFileName+OPT::strExportType); + #if TD_VERBOSE != TD_VERBOSE_OFF + if (VERBOSITY_LEVEL > 2) + scene.ExportCamerasMLP(baseFileName+_T(".mlp"), baseFileName+OPT::strExportType); + #endif + if ((ARCHIVE_TYPE)OPT::nArchiveType != ARCHIVE_MVS || sceneType != Scene::SCENE_INTERFACE) + scene.Save(baseFileName+_T(".mvs"), (ARCHIVE_TYPE)OPT::nArchiveType); + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/Tests/CMakeLists.txt b/apps/Tests/CMakeLists.txt new file mode 100644 index 0000000..192178b --- /dev/null +++ b/apps/Tests/CMakeLists.txt @@ -0,0 +1,15 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +ADD_DEFINITIONS(-D_DATA_PATH="${CMAKE_CURRENT_SOURCE_DIR}/data/") + +cxx_executable_with_flags(Tests "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS Tests + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/Tests/Tests.cpp b/apps/Tests/Tests.cpp new file mode 100644 index 0000000..731c04d --- /dev/null +++ b/apps/Tests/Tests.cpp @@ -0,0 +1,136 @@ +/* + * Tests.cpp + * + * Copyright (c) 2014-2021 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" + +using namespace MVS; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("Tests") + + +// S T R U C T S /////////////////////////////////////////////////// + +// test various algorithms independently +bool UnitTests() +{ + TD_TIMER_START(); + if (!SEACAVE::cListTest(100)) { + VERBOSE("ERROR: cListTest failed!"); + return false; + } + if (!SEACAVE::OctreeTest(100)) { + VERBOSE("ERROR: OctreeTest failed!"); + return false; + } + if (!SEACAVE::OctreeTest(100)) { + VERBOSE("ERROR: OctreeTest failed!"); + return false; + } + if (!SEACAVE::TestRayTriangleIntersection(1000)) { + VERBOSE("ERROR: TestRayTriangleIntersection failed!"); + return false; + } + if (!SEACAVE::TestRayTriangleIntersection(1000)) { + VERBOSE("ERROR: TestRayTriangleIntersection failed!"); + return false; + } + VERBOSE("All unit tests passed (%s)", TD_TIMER_GET_FMT().c_str()); + return true; +} + + +// test MVS stages on a small sample dataset +bool PipelineTest(bool verbose=false) +{ + TD_TIMER_START(); + Scene scene; + if (!scene.Load(MAKE_PATH("scene.mvs"))) { + VERBOSE("ERROR: TestDataset failed loading the scene!"); + return false; + } + OPTDENSE::init(); + OPTDENSE::bRemoveDmaps = true; + if (!scene.DenseReconstruction() || scene.pointcloud.GetSize() < 200000u) { + VERBOSE("ERROR: TestDataset failed estimating dense point cloud!"); + return false; + } + if (verbose) + scene.pointcloud.Save(MAKE_PATH("scene_dense.ply")); + if (!scene.ReconstructMesh() || scene.mesh.faces.size() < 75000u) { + VERBOSE("ERROR: TestDataset failed reconstructing the mesh!"); + return false; + } + if (verbose) + scene.mesh.Save(MAKE_PATH("scene_dense_mesh.ply")); + constexpr float decimate = 0.5f; + scene.mesh.Clean(decimate); + if (!ISINSIDE(scene.mesh.faces.size(), 35000u, 45000u)) { + VERBOSE("ERROR: TestDataset failed cleaning the mesh!"); + return false; + } + #ifdef _USE_OPENMP + TestMeshProjectionMT(scene.mesh, scene.images[1]); + #endif + if (!scene.TextureMesh(0, 0) || !scene.mesh.HasTexture()) { + VERBOSE("ERROR: TestDataset failed texturing the mesh!"); + return false; + } + if (verbose) + scene.mesh.Save(MAKE_PATH("scene_dense_mesh_texture.ply")); + VERBOSE("All pipeline stages passed (%s)", TD_TIMER_GET_FMT().c_str()); + return true; +} + +// test OpenMVS functionality +int main(int argc, LPCTSTR* argv) +{ + OPEN_LOG(); + OPEN_LOGCONSOLE(); + MVS::Initialize(APPNAME); + WORKING_FOLDER = _DATA_PATH; + INIT_WORKING_FOLDER; + if (argc < 2 || std::atoi(argv[1]) == 0) { + if (!UnitTests()) + return EXIT_FAILURE; + } else { + if (!PipelineTest()) + return EXIT_FAILURE; + } + MVS::Finalize(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/Tests/data/images/00000.jpg b/apps/Tests/data/images/00000.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2ec67cb9a6946ba1e4b5a868a312e28c8ea8db70 GIT binary patch literal 286347 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�^3D!D96PyD)RU6fkV|DQ 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

dVNJ@oou&^6_@N)*r!# zzE@`Ea4$3VkwA}uHrdZ>?+`Y}-2fqjO9@Z>z_6%LxwZ)Jf?tkqqE>q*hNG+6q`JIm z`zB7wjXLU!`h#Y#z0=%N7=5WE+Z9SvNw;Zi3YyTWED?UbxISO8^vHS4oK=#|uZ*q0 zu{aKKD<-M?42hK*3?-O_ygD2GJ*%hVV> zq-^PN*Fy09w^6wLn$5UXB|2g>D8a}4o#5s&4y_)@W_vn0CAgf`X1J#PE?A zqf0ay!AEc@N`Mk!uYZV980mk$FC_R*N3iZ2T~Lb2z4vh3g9sbc(K$6_An?>s&sb@M z2g-f#xp~Xp=w49X?&_NUN1#Ojq`pUWxSoa`E!jU}>Sot##w~TC)rI_qm_$>%BDdOt ze?sNgZ%v>cw?vfH7W^E?Ro=8xWn2)gea$|c{^N&@rLBDzRJ**Xc4Y``g8Dc_i4K~p zn@k9{{|r@MwE>HP3njxKyKp1yO=x=iQspNS>X(O6jOiP&5l}_bwXqQd4%5v4fw7eX zHNM=k+0|2qr@NkOUL>M9o=Po_`&F9Sa|vY{gE#`T_RXA9WI^9n)NKaz$ucbT{3(fZ zUX#Y?#o%Y0>zv$Y}3WhQ=cTc-H=-~w8oK2Y8<+PF4$lYPz^&T z-|Ub%OuGK-uX^RhS3A0})A<%hyK1V(Jb&>~$cUv!iHNuV^36QQtI=BYU$zt2vB)sT zA+a3S)5M)BvdZN~Tp7GQlynF=d6vmEl+}3sheBQbP1c-9hk^!VJjr}>a zH=ed2Sy`|d!RXwZag0nT_Y_!PP3X0SnvL7np8VP4z2;u&arAa#ph!>rq)wO|Q;0!v zXqjk#krkPltFZ*4msuPbQi5k25qpVNBE>>b{H4aj5sKE z0lf`ZIm;s3<}vXp(iF;(J$D#)b)~C(lXZBq&F6er53xkBL3xF_*F5W>k-AuE(b8l+ zGDudse79``|K@jb7~+7djk~1@!u3AIY2hOBu2!4Z{&EDIHn2p`T_1)Z8Y55gL0pT_ z+8D}EY5?;Y_Hg9am*Wj@;l1Jjg%^>^KuBv?dp`FB*9~)e;J66d3_LB$a-0jJ zF_^0LgZGAZIs9~=tLBdy7s7$JMQIb2az(At<=@K(G3%1GgN-2}F7zLiG)(@Qz7_;Z z<^Nvz#V_a+_Wo)89!P9`=?1}rw6~mo`1Zu;P}KvLNa#1*44i=LNvHE=(RR`ZoMhpe_j0Qwc5Ui=jdqe9vu zAPzC?$w7AjT8M088`5Mh0!y}EJ&rp+sc0>kfBirsAkAS8vT#&HnBHzgmdp|r7!cn| zAP_2&%VQOvt`VtO2<@}NE0Ao}$(Bqo{}5JAsC!j~kh+z3Hl{@5Q!2$TS3m4}IF1C;F5i zhb<18K?Vwh9qgDwbcB+ip*Ej8OQT46PI}Ddo%=^f9@>Bd+k=Oeu4Oyfiw8?XVRIXH znJyXWKOSAQX>9ZxodH%C0?!kMv-Di5`^Py=l-e!To?a))kQrpV)l0W;C0EZ;R9Wgx z>DJz^;t&v{*vx7U-^K*-jqWoJ*s(s>LDxbV2ME>xv?}*IVH3k!nD1Yo5-~gH$jzCtYbE zEi<7RFTRS7;6b6h@NzgMcKZNBB)LUQG>BisBRJ*Vddrv&HA!%O9<6qaCatjDhmatf zn!H#3YU&#@odeEI?#@9DkNJmLl?O=Hf*-jwpc|>ocQFcUipeQ=^UvItP*nkW6or;; z*{We}RzI8{PGINon9-p^@+u}eTPLaj7j?6?55JR)-2M}A;;;^*jMq~jRJ0V@^`ojm zVoSrkQKYh{yKOt(sMS+Pgts|OB$U{oLI0wkE<}84co2S`I1CD|mE*$vGtHvbsZUa} zCisH|!2+&rI_%aX7usqQⅅ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

9KPI z6lrWT>%LW)Xw+3?RD(8gNeWfMin&kewXd%OS?-fWUCp92oXqx`a|a0fmUaq z!nwSAiaQf0Dh?Bkk!xoPJ2rLJ{Xkl{_^jn~3o6Rw0Iezg#PUZwi`PjR9q=y&E7yS( z5#|>~oDA~-Jl5;!4sITHmHW{ESN|b9Z@JzCRM#B81AE>}ewk?oM!sLORqyZBF6cOC zcT9%j+r;ZDb!TM;ANdY%)SqLd8Y5Hf|$_QKI_G0HLS^X2HBaiB>+^qw2;ngES zA*pxzb0IVx!fRkUuu=R7Hxy%x^8w@lWG901aV!uQDh>?PJcY7Y44vU2R)*H2@6 zM7JFGnNlz+?&UKqHHC#ZXD62BHUP$cyn`V4AdX$cORUBJ57*@?{qNWO6vD|G5I-#O zk;BudPg-Q8swWJ?@{q5q*Bh*E7P0-wCbhvr8*HVH3v?7ZV#%RD{W9zEnsS2+jTEfL zO2d;;Q>)!|xT~^^yH@(@K>tNS%P;i+2HHfU$;Mvgkj6oqqr%GQF&dF5d2W^BdWF!z z*h#R{kE@>5)_hZNDg;PZw&tKc1G(g7?>%3TXfBr9&fE&n#lahA*xC}Huxs6$W0_*Z zPBr2z;36dC-PkbkwnG1gmov8Mq{@x|FA+ts`*om5j0hd!XX9_o7E(OLgrc`g)muKu zS2Ii(TKp#TGaXee_0#{to&}ETq+r~zqH<=F6+ zQ=$(`_`S2XdfDZ}Gau_gdIT()kmrb!u(CGw2A1&JAO%RXL>Y_;26)LvOVcb?8tqXlO}wSKCXe_GRlPWUXmK-_-ls9%##vi)ut) zX=$z8=!tlC$!9lIPK zI9O(gOVgb|8uK!Hr+%f}rOBLqP#&LszMF-IedcFKtl^j3a$s_R77ISj@`)Vha9 zBS^>+&7sQ!J;Y9n8k_F^J)p|=AtlN|2WQv<1jS9vP!mDW3tS1Y4?7~-=G*gF|?e5g7a?NGkS`OKz;vRumfT8$qKKE${$mgjo;ne-CGa%a-HN)7b;k) z;PC7KVm~o+N^WAd$qK)Ta}f(oP-iK{>YaL8f?M_CL3CKCx4Yb2nwGOI`T)^5y+d@u zr)IZ@8flL6;tAZsjtIt2b#7683r@Cn<&7H8<|zalcPa45 zkeL-K=!^Zd;k~QR4d6Xs2d>b$z9zaJrEQTFhX++$cnN-2pu*k;b^_um)+`bidtc$& z1r{*oPCLIIM4(c&%cTZaik!KeY2rik?|h6Ali47MXp$fE{UD3 z5XT#rtx1Y{DF|f{aA3i&pOb+#9!KRiP#QOVZR(!Dm z9lNXBqKOw`41{B$ANY&4BVW5Z!yhhxq%wjW?{ylu^eL5{MD;CNO*47dYUlEMF25bh z?_2K|(&59aT{qS*W9}Tz_;Vm6?Z(%?@0~aKw}c`G?cGP!RA3Xj z`~!N+XriA`2qr)tl}RumQ}gSsMsDT9?-l<{q`T4e>lG&8?O8>hO0Aqpis;Ny zov|4R!iRT*&lfXH!k?TVx4s|UwFvI4?LBS6aNe2Dn9BdQgkS7MC;rmpdg}DX-A|G% z*!cF$GT2`^X7w+I{BrL$F$SraON`R$i6##}iPfi0FOx}d{BCDG0)tRfhj&_UZBSJqZB%cw7}1F#6|q@}CE5Wou!?_}aQDe zCi~7Ay?fC2;~e^1v_V%}yB0oBwwIr351=HayGBC$!;J~C-`2evi0wvP+^LGpipKnf z=^YH~32APSclhnq$876$0@`b|PIxZE+~#C$=7(9)YfhF)I|bSG%1fRFz5QLEkYw%5c=%nHzQWql{E<|2>7#UJ6phVrq_};Hz0*c_Jt>4qzuV zV6RFxOE+r8p-%6ieywQ>EK$mCoO_LDHA+uOZn&CxEeFP#H#Hi%7yZe2{n)AxIFE&+ zl*^@sSz9l`BTsoxu(kyusaAM`n{*+Hn;zro+QI zG$7~<&}i%l;XxPM3!g+o47$Vh3|E`UYB)y@?(~=@oljjia}`%Lm4$|mR5iUsc`;88 znQ5-V1T7k3l5ObePm}t(7xtx3vE#FAQ|L}UQH&c<%Kjj8|JvK;%Wfbx>~F+c6iWh1 z5DQ#5w{QtDvh_~b-XSsXRV3OB9jAk^3w9_Cw|>aM#`jHj9aQ+6ErUNC@5+OVrc`~~ z5)`yH4SsXFD?9RdZ)fOPj}srQi<**V0E6O>f81sF6+SWP@(5r|qMVb1>ItqS+Lz6H z4tgjWT+L1;ufq1PX8_#g5qqyqv|)X@aT=vo`41?G^YsJ&vM@e>?G(b$v&U2ajPC`< zM`1s}07L*Io>T@4noTG?ShAxK5!;^M7z#PC{;j##RFd0Ub?4Nx&ujRF4=Nm&Go@Mo zZJ8*AAGX^nJMYPF(U7Z(1sR*#k@0%xsIrBEYU~4VU7+UzN4-PyRPU=Vvzy0r+GyI2 zD?UxR?d&UpMUthmwN4KL!stZ45@R^3+S8kkTnbpXVjh#xGxFG4f&`inPq5ov9|RO# zlq-|g$0{odZ>^|zaY^4R^ii~u!3ERriIS#7oTPz8Z?WmGk;vZR8qP9e%F@|fWcdq& zvwf!mV2dUQM8!M0ql)Bmt-fjbVHpr$>TDPM zA&2DO#fXd0QER9T_SC-n!PL8<_c{v}BUfZ=o?AmtQX0LjKEHYT;9}VIscpYT!(Z6m6Nc#R zqP_#Kd=ykK-jhjlw>uTZ8eY3cON#Xp_+I{boHBH+@C1}6?k+~h_UwKBq&(n$C=Lds ziOu4oq-?D~E&Lfy z*CDZc$G@+Ks%BjJD!wNMy`^isWMW-xdS`^3a7@IG{txx!x$){xVhYW46SShcgkY zZbO#rhUm5PfBGd$KT@;3-QG60Y2SPo@v5yl7!lCm(jEkz5&dm+(X(YcxkvSI>VVMZ zPRrJ7A`-j}w!NrB+c?uGLu~z6?1qaLPg~kw|Frg#y;emaDsDG(?2a?{0t`;z$e8mr zyOqMKGW>Omx}}`CZ+8SfapRivG({>-S# z-xw@e=|h}VjM!xP0bks%dI)Zl-r^bim^$|I&Bb2BRKJJ4C&Kez)pBYwYi#|(e0}cb zyvZfxe&-q0KV3tv8>|$*dw3do0yf|=?`-t%JM>h_+S1R6rCQCmYfoEZMqr)xEwWB~ zN<+w{`RD`h(UEAiUTK(JI&Ei8fl;l_Or`sl;;KJFe^5*tpA|%3?%Ch)_Ogw>rNQK< z7yFz+-FUyy=KUkA2YYkB_rB@1i-FVbNj+6isi1IseG%e7jQ1fPwKzB=UE&+~O-Z*n zy(WHP_T%KQrz5d!NqS8+fb8yuDajlv<@yt&D`q&T8J=oqAvbAH1y`&r(Fu+jx*lUB zhrIe1ovc}OU3qQT{AbjT0~;m3GF1=l(KtRt^yB+{PqWckzf|6+Iq+>jCvYA$Bek%?4-p!*1hfK#ggOLW6Lydn z8}w?l{A;A`=$n?DPVLGnI%dbyWYLF-M!qGXhft9}I{zy8xdUl%*^}hy*1&A#*2fY? z2&mwqdGpyxli{WNajNDhlu2{-NRN$~GOvZO^E6xiU*ErF?m3^x5AcWa5pWgMqWo%y zqwdnkm)L{#OG;)sLA+&U&u)-b^Q9@$#z6&$5eGxeMR7{^eRbmHHW^F*ZP||$&qesr zf-gqpp;llUl@6LJZq^fRcWd%-8t3k0 zNaIv{y_S}tc4L;G@v)MUm0HNv6l*U2>hei{C3^4CqmBQ1Pd?WBc<@dGO>F(Hdr#s_ z_tDqqw)YzxD=nya@XN?MiKFcagY-DRYafoqz|R0pE7@;oGA%Y<3yWe4rJnLvo#d9i zaVe+DTQ8j=eN{)n^m0D)9cg6KAd5;G!TU6V-zapialU@@_c5sr$XPzm=CAdGf*O14 zK9f+(*}{i4F=z~8`{yqYq-e!{Po1%DEQI18o@wZ7Gjf#i@Z1%WhUT~}=4sH_*s2L1 z%sn6$mLflLyrPNQ#;x*_o11aWdB)rzLt?yj@1?4(^hrHTe)xNnM?G(TnKTbO-C`~3 z?d?!=Hzc(boNR1o#@^of<6CCM>(5ylo09!|(LWfEDuchDwF~&BH6ON)MJ6y$FgBiT z?hIm=W7j+49rSbl0Pf3mpiMHZ^d$=wXE6w~7o7zoy8$e0-CXQUi_NPL1ZNg?JhtEu zPB_)fj8|SN0Bv}6T5wgn^S9&9>%#cxlN8UC(gu+bdtT-0e1SoGLNd<7e!< z@5Rxg_}c_#5!~}%*44glpEO%e_Bopmqu_A?DxfDryZ;p^lm{Gs!=ZtXGUCL}zw4Ju zGgdbb4x}EJauq>*1!$^L>69(HJ>+*GaL(mO@bJ^jw==V11R7<8Z}0H0Nd&uj#(u4o zre~j7ROX(I=S&rxnXDvD?41qYqi~l043d=)%vSF^Sfuiq4uxH+YJgAf4X(odCBnG# z{|%n~=_QAGmlNXGN+a#~RBQ0)nQ}tkDfi3rIusH!u+#`)#;#)cmjqLY+_k5!H&KQlk&68C z73z0f@KpQ$z{+2*%QX_|UcN8NwGQ;pv{;zy@Al9z(9T18l*coC^SI&3LH7KQp$1F) z@%Q2T;^v*Y91q|la({=?B%4$&}IkxwDcL^idm2$y~>fC%) zcbUR)=;c7YUU2dGKV+-vO9x4@-Ur6EG1Bd=-Hxiwu3D4Ph6uGKXBn z*)C|6e)l)eW^8Qeg*V=8bb^DOJB&QVaWKFA|CspfP2HJ13-%WyU~s|@?0^RFadC)R zdm2ggK})hc2Fg`9N>o))Nq7Ppy}S$ONNPh>Pd8oUdL#LDehOS+JzTW9&m+w=75?l` zIZ!zC2K(9Nj25BR)bg((WJVak@+f*)0PF&Dqdr)@uu1(3p#Kt)axLir>nu9Z} zgdjwqnXBrt$g@=aE6~%vHubl~1h4F^K#>xcTn83eC^IDNgO6miX^S!l#qEhXN&@$rfE;Zgw{~?>q`t4O9tuB>WCb1FbkBQNE4Y0__3I$$#AWd8ILOp=1dL zhmEt7Eel~(bW*`7?45-}u;jSEempC5#&AJAM_GI~$ckh~cu_QHxrx+AZ73<|yb&7KKKxg>$#lt+Y=J{iv zPh4v_d#f+&H1a^iJ@!p($l)f0EvLgtBP)w|=jlmvig6IFNnGj;Q6Gw=j|7ue}AbB|HXiDcgJH3*aY$@4>Eng2zZpLe72s4u#c( zGM>H0EdFanPk9h$P#yShq5q4*8&jVlPV6fXtKJX#k?&fITRs6(5-+0M0i0La?))cq zaVO3lq5~PApmg;m7Hs|P;26L>Pok;>n^pfhlXS0O^VF|D+1;C(!*ZjV{q8BmSpudO zJ`!?f7RT=9)o;XE>ZHZ3jP-t?i9a~LK@{8cY7aB<-?B4%s~P<7@wJ27nSbB9UYn{;fzZ5$S_%T zxHGj*VzX;g^g&!~>E21XIyNUnUh;AvC4Hn5xX%P3q@xR8-=B(?KMPP_jkTSzSC`YX=7-*=kRRxHSU8bUk|Op)+Pg4x$C3U%zHrydz3wd}s} zsy+Pe8$sU_`j}x5?oe$=AW$5oa4?^zy3MJ3aLZdk|KaY`0^prU$&rv0O1`7OuRBNT zSq#PYXKLj;oRM|6_nN@v86D4K$#WKJ!9ZS@c4?-BMp;kz$6;Sj6##c{<(QM^MYQCN zn=+Px?Mdz{pr`tqw}17Y8J(Zs=BK_n$@?~>F=B=9GxBR zT|uTB+w`L(CRb~6ClB5zvpazF>3IDYDnBp>p&I$5-2ckMMnY7@(bj(rwT|De?t`3l zpV6t{jv}eNm{?l&l3-qSzrvpZoRc-VHf%g$8an3S&P9+%u*6RN{SlglU?WEPHE`0+ z)N*7Ve5U$S-pDKKUkDS5D~Cg+e|c7R6G!_|6;Q3*6Vd?(BVbXKF|RBu3Ig*75|$n| zxU+PRoqF{w7iTx5z}zUbCdM7G)qEYru&+06% z7v@lj)G>~C05_S0E?eoI&MP05P{_HX1skQ`KDZGvsaLpM{dS z!jh*4QZ>1zW5#^(I~J2d9Wu-spV3MvIDJ470W^qOoxo=<2DfKSPo8m4;S*hh5(rfu zOOzc3qzoRIIgGlFj`;m;FxrCcUJgI%WJnsz4r8>RPkSJSxTG{1FNtJ5>;nF#0hK`? zB6Z&ZQ!Qh(yAEkp;{JB1-;ZEeU>nM;D+=x-h@j>`&>pf~Rww4otxl-z4|LxQ!B5nc z9*pbH{BH}L-jR&H`qbh?2g_(Ga9y^AN}C4^H_RAmaD@g*D-%(2selLN4?) zBn!t-_wR{(uC8Ws^KI^f`F{W6U3Jc0fj?Lg2X9aF@+N7I-B~7xwVfIh|67g}>3W3l z4j4rA8b(BdQ=X>jW+crao(dGy=m3@==O_e-Uvw9s>lUn(&^I}QMsJIaPD4Y!hHf=f z(9eW8W8$nRe-&LSxLr}@=a=Urz`53jK)=jv?&lc|nHJ6tNtrrSx0aK%g#GTib88j! zGwD1l_h;^Vuf8uuB>WzBnXYez1rP3EhFr^Ys3PbFwUvi+Pg%)^nSpCbCs!_n)0SNQ zxv9yF)Jr7~^LO2U)_?puNjrb$7(f=Mcm-At?kzbgph^kKzxG^g$X(kU>z?NXdri58 zdKhylA2H^7-gK#tJKAJq_^2suVi8_mn{RroY!2b#^Yq1B9#d$&9DrPfpc94^n^l_s z3we|FRFb;FU^d?(6GJKI#xn||SYq-)=~`7&{jAVFUq=v(18er; zI_(soA;mXw>}LMdM@GJt^Fv?BY%i4kUh4vS{5{>DfnF-mrU-SPQt~%dxfaXRz_?KO z1z9VFfOs|Tr8#~j`W8qz0&gd3pXq1nUSvFATA~Uw?#9B?9n3j)Hlp?MjHK^UI3iwp zz+aox*-3fSBSu)+nwsuu0^nVxa^ zDYYlP(e|%wlDW2yJNh0%dv_Ygjgx;m*j4RHZnH9y(Zq`YOV>5!$+jNOAZX1}SPQ*#|tXoOUqfRtPrrAr-T>n6&z3HzO{o z#jF}>`?y(qvpnLZH>mU|_bx`G!y2_b9*y)&9Zh=xj@&#zz7gOj+bQVJ1&tEhSX-b0j`_2_6ZQpAUOr!=SL>%=Uo1d7IPcy$&?xHjJG=c1!8#WNYEU?Q=Eji8Z$>gdXdtaMZYW zD2743NIH2cy4VT|Iabu1Y#4{$Opul>P2IzVik0x%;{dx&S_==t#^V9EhXpESF(wG8 zrHy|<;|$+3E~!rjN!*Gw6Dyqun=+V~W41S6+}rivmP_+E6ZzCi0q;A|y0c>hz*&d_ zkI51n+cx#?eB6povzes+=)h${@Zf>t83kC(y;&~YhrROB;i(L;Oze3 z;8BZ}v=i{%kODk>jbZGk!#FWa?fj|_7l)J5c5|Y9B{eGvD_6weVgl! zu`s4LHa2mMlkaB*Xy4(+^f0vQv3p=@j3s2Pdp*#o_IqDOxo!+y$_~y3M*&-HJd?myON{ zD;%^3TCQTsrVH^|I5he;{b9-8H*R%Ss15trgy4R)5VoN>CCA6-sODWqBdLEHQBfLq?CbmtxQY6 zs;AgFcKtd3l;+FaVC%C0x7OuFN*#hC`KWQ@9a+byuC9pwQ31AxCiAK%r+bOL$7w|Y z0gwfL0j)pcMv_*?Vj7F)QZ{iS^@su|!#^~|ZLdW9FE|M7Z!>d^HaXT(#A~dv_dEHv zt3gsapRv4IQvn6e1oDgL%K7a*h(CWs8GkEP9SdT=Mzl?)KyNfR7RSy^<@0w8m<5ytbZABgmIP~)v@Hx++dU~W+ibwB@1dvw@t53 z=LasWhn__WeuRX2{pNz5=l@77`}^$qZHXGoS{aJ-Vl&S!ZNI((@wl3CwgQq^2QSiB z2}W{YDDB&sx2(YSv(_b-=7%GH1XohH6^)H47jH+jT#Mf3se!WrZ}}bhG=}Q~bG!A9 ze5p@~(m~=Ap{spG!&aOj_3o+5I@`($Ob*SRBU4!s_=lP$JLUcvl5n}k7(s6QUeJ%J zk_t4%-qacT|5~4O4H%KV6vs&V9jW)jWyzyqA?()!ul`hwg{9XX_Vmf~thmbcyu}Ro zR~nO^h4QZ#Y2)5V!^1Ty$MUJUELiJ;(Ov!fmdQP(`+x^2fq~P7U6qxix~YqPLvaU> zB@?YVp~$>rnvYSMtB}JnQsI2>ScSQ7=%J;eZ4R-Hk*S}@wH4dT71uJ@_rK2ewgvi)n>J%Hv5Bwf#!cF)(?_&l9#Md*@gIV|p zPqF6x3lGO;ei;pM%*J^e(Xm4E$V=XzMc%v>_iBf7%r6CA&pHosC$MeU@5j!LiDJdtC!N+O8?+ zIPczf&0)^?u0+s?Sq)ke*ieVNx{4$Ya?VOUngsrHTWvxgBty)FSgUYWjyq30blGO$ zW4RLKq+3~})%HgY+$q~9($Mx(4_CgsLxpA(f9_fT2KLVTQJHpEoyp1VJ2tI<%T<+d>Zi zU3$uLs8|pi)n$Fdp{NpM>m8<3^ThCnQ~V{8TbC?y?C_u&rIY@So-rqPie5Q|iCje- zI$`uuM6Ucg>6w1>remqTIxgC#S%-od5WT*U$K)`BP% z21t(C20VBLMpYGt%pod%KXA7_AZFS@)q88FxW`d-Mth7v#1_B@DRS`!)O+6-oLNI2 zTgWROCs_HhhAcl~90rriAk&48b409z79bJzJzYOJlX^yO>8N6*oZtiyMg5tn$65g& zxv}41%T-=BH{g<(SN6 zdr!|n%h-4F4IXJ}%{h~P=f5p0GqV2lmH=X8six)Xz?3YjfTpGGQV>1CIy|jy06f8t zT$StJ2P0!F^&%Ul_F)GjnPXf3uftduw9#rO7+CBnzP&VpbWm~oehM4X#|+EL*HQB- z){c*^^a`PfqWnp_tPU*X4jPSe96ZnWMn4bTtD=&}7GJYkyjXGV?57RwT*OFK0sSI= zn7DFFp1@|MmmC8D;T>e)0@D;Ah(r0cRX9U={9SnroVS#t1{f}a`0{U!vQDtxXj;Fl zN+=x9u$0uzmK@+w`?XM;zsQVoUVBzT!V#!Ss(k}pr zDx~9%Gbjta71bD!L*@2TXxz6#J20*(u+ z^7S)pujA3+yo+N{9nqD9h+j&D2+mRaJwRV+jx@Lw*3RZRadMBl@N(5Z+5QXp$GOmGwIu_6&W? z{pp5|fejrkJPzdOY_jza;v?ZXsdf!{o#5tKCP=rlM@dqQJD5gq1;&_th<&4@?T#Yt z!KtCrD?BTb$849xe@J<_o!%gI_MyZdMcgVX6+6>^A7wxO{d( zhSWjZ_|q7&xmue$V3E(e9qfK)#5t-*uE2B#Ugk~;*w!%9x3Z<)I%Gh;!13neFx@A7 zxXYdwvz0b-Yw>qPw_ikUG|whsI5>t_plY}Xp@+vVEc-7gmY573n3-&O^7%x{`A*ip zO~+Z7rJSF$ks;4;ZO9O%zv_`UIlbfg_CNtPLXr~Dv2Wb-W#BNZAN6vql9)#R-kMfN zZc(m5?fcM_vQ@qlWcV4$;AwVnaewyOMovfTQC$nMMH&-S_)WE|0Zs4qRltcQ;?yXD z`76jJUK%xNFF69_7!}n{3pYvf9sh0dmQQ8cS$s1pj+yA`jqrXDWH&N_Y1hAp*TJcQ zED2#m+v0sNg|o<)8uA1cE!q z4#-L1Y`r~597wW)tNtEkckN|L(GSTA20;Hm?xXlM*sU>}gZ;YkqD6xwle~C4KcIj!UW009rT9h-xaGqa5xHgOxrvjTh7 zhuYNT&iN!Xhn4vS>LqN zsI=_F+G!ixw8`hy)2qFuGRZ_n_jtx%D2C(Ql-7N`$PySwRVi!$fjpNp&b9@y@TvL= zM;ZTV#Vk~i7KJ-=sOS=qAR5&XE0dnA8!|v~x0&6!S|@e;xw#T|a1S~>E?%7fMLeo9 zF4me?7lpdQ`OA=)cH4!!uKgW#gu@{jsN`(5 zzhxxj7q0OMdbOBqyYu?USCtQL&f){I(2LF<>fZ}awmgaTAyDFwUcL}=qG7gY=pT2k zcm^GxuFoAL6@}Wc;`=Ff>NVJ{0YD51?6fY;MbL#(*kTk8pKWIH38bJf3Z8py`k&8-GtSV#_%rgU*ZgDvng(_=>R z@r)KXrXfS7nP*Pam;}Fb3#xlZTf54{%J9qiDEM|Eh#sa2$1knnOdcPBVZ?^432FFOQ+@;)87?aMApHnfCAHbg}ub_egk$thwV8?)s5+-l^73+ z?}!gkywggK+L9kwjqkxG1ThC<^$(2d8hG%i#&RVTQw3~1JV*{Cbr*T7szmswB|Sa) z$-@T~{4<#o71BTE{j~BR)zD~T#N;Pu3MY@rd-CnOmT|Ikb8V_N`p3J;&0{isu(`E1 zmtU%MD;9GP=17D1LNv!aPNCvyjK%lTW;4tMx%DeX+MEq2r4Gnk^2~TMggdvZk!rn$ zgjE+#PZ!c`@xKfsX^%nyRA*8Go3OrdE)K7p=|$8ldo_m2)}+N2pT9grHA7pDK(zk2 zV-_655KuO(IU`OP?)R5wcSAyd)jMt3QhfO{B8OFB%>!rZ}zw`xw(@%{Tj~t8Os?@$qP%R`|305 zTOQ3gUe7hV79zE^l>hi+=F{zt_JNw^Cv&fj%1vxg<~QaesB2ZB-?cuCFb2o^<_*bx zg3ae^klM)ctqKGEeR+O@poAjW+AM#wbV28f-x7}Vp4_82m83D-0S&# z`0c!PpDG^=OXI8^GzgY^fe**2@iM}&)NEe^O_=D)JP z1i^Z`SCu>6mf(#{PLe#Rhy3ejj~{!@A$aZSb?&kVK1T5pKqHrK4q`?V65NB9T}QM7 z=0jy8-6gy8x*bept7$lI8Q6Q$!D7TJM6r?y1S-A6T@W`E`3NxNILOtjf`#yuqzNQ+ zgPg7aC$A#3p695q)F)ZVq-VONwjJJtpEFx`2JL-ghro5UthSSRl6N$Ry=IfP+{vr+ zl>`>8t6l}wb;iCZX);*FA3M?#deud+{j)G3Q?t*{GsVJ-+4z>pmgy6g`oY(Xr9Ipe zK-c0-fsB}{5#UZJhHcFsJ0@7R03np-acIvgE!?`xG|d9roI6n#OQU<%FJr?wH`o13 z6Soa`->wns?F61;Dm-OVY`AKR*=wv;SLxoC0QuGqEt7Dfn|B5!TYItxB8bK8t$RaQ zPj=itW1VDOn@|PsvAH+DXo+%E4TOQ>ZptP02Hq6Q_;NN`!#^svQaeY4oRt^lLMV(| z^ATm^8NCf>FKtx1^Ip-2RRk)>i_cA7X0FUBz(e)6JHWCT#Rk4-*?YIbIdYDIY7Owd zjG$XvztJrDZws_=Q^X_s(Hm`YhuKBb3sr?(u7y}7R&UbkdsSiBN=l2Yk9wH3U$IjZ z>ieTNE=M1GJ8?0#p`t&badJiCeh2N+@5|LZ{_Ww|xKD%00TJYf!FG?0z4C;4Wik0z zZ&40`sVcBTiL+4`h7=ajl0yoOgv4a<`~sMGy-nw@d8=RpuF|;?1I7(^65a|rH(E#M z0#5hWKxYJ+{VUO{hd~3jRX8z(o*gUgSBWfKz58v*LUFDhV&{Xjb!iqKy5@#C(#%;{ zLOTTxEu~!7f8&q^Dsu|Dy4PDfc?Jdd$MXau3JU+Sy|qVv!BOA{)G0z z`nffuhsDKy=UfY4AZtGrkIWS|3;I)R^_6fTfY}7=Ve0?*;H%SdAC(XZegYP|LGSfJ zth%QRe&4DW<;f%83L{(?iV}Ga$B|q^^bQVG$ivgiO=f7D1`MiPc|f)%bVBst+dp&S z$Qt7;HWjd!SnLH<*#0u#ZG@{pIDSWl16X!6NlE2!ugX@^(D67M(W2@M_^vXltoa)0 zfF~?%YZ;=N%&Yvo^tMRg7%BC>i`S;)q?O>nv){%7p$2?Z2@CG>9hI?yE||7}Oa|kl zkuWpv;sPcL7ob@{l*S7;mRE`pFULZlBkpzV>-mg8*1rYf(l*Qk{JuqrEDF?thWQEX zFcY5BQ7v&IgKDg%)NcT15v|NGFCU*?y8@m_NVwnm9lH^x39EvH0Qy`Mx+2ux_vB=o zL~{TCI64=9ru+Z@UsqkF5<(F|<&+qeQ#ou`2gG7U$c9}dF>7{-Wf=QZ&c`c?A|$8K z$YB;{Vh&dcF)Wj@DaXd%72Ywse(&$^575o6+uQs7dOe?y$Nj!Rv!ztTrImRzE%MT+ z(YiE5@{eF%c0GsmLJTOsSnRh)%lA8M`Wh|u$4pGM+x!0mn8q{)%KhBK|ACnGGy_M=~of!$>m@g@88x+_iIVRHPqK#V5U&n=-JU6xI8_OL2 znsam?+fmuIMu~Lmng79acU^Qxo~!x#*3}W;g3)DO1p3<) zRFLONl&J!*POMh!IIk!Uu19hCSkzghNI5CPj#2luLQ$1LonE-^{ZW{Cn}%nicbxa4 zbzt)0%mFJefVbFJv)iigP{trOQu6oKJ(hLsJ4=>++g6_RFpih_RyM{nI#ZFN3CK5D z494Lhb#AZ{Bmt1E)CE*K+Z7k8<%`K{^DEE0jM2kF-ht*60HJ8BfBOp<8d*j5$9yEw zqTljr9>SZ2m|7+LMhp{fXLS-KpdFu`VteK3rTFrBJ{aQ-etWH)$rQW6msNd}Ao7$_ z8Pff+qeW^=GXN#1?Hx?^GmG)>ygRl*fkc}D#+?P$b+u=%W?xAQh(KQ!dj7FJNMXmq zN-4PXHUh3jRa)TX)P`0~QZ9jxTLl745(>m`$KaD@AI>T@6)3)m|LFUQ zuoe$!^EPJ)+$4H|j4(HT#nxZue=Nfy)z9Sfj#g2A52sfW(4zPeZZ;X)$#YCxt^Swk z+5#_9E5!7>vw0pIU+cFGe)fm|7aIksuNBhiXKf1)aA~$-$~DD8LYU)FjmqMTlXxqR zm#^t~%F5n!dx;N)>YvQy%(xTnmnMH?B}DRDjCG}O@< znfaseL+8cZ9;a&DUAQ?y!K5)&gh3|zGsbKKBW*W)HbQ7Zgd*;DtS5(g=1$(-R#?iy zi5_m@0cJ#k?z5wexrzk1PEd(`|Th?VB))T=cvSRT2tF4)jhi#onE$* zU5keN@oj~JP$%B56m6KpaS~f3Gzei9#ivmCP<30!Z?(V0@}d$ubH;2v0F~k48G!`t zqaIM4gsSj4`*KN%UJa&!Ia8ew@>iOw8Rt^c4}+L8dT@qd31aWi+mWr&>F1=)nGetg z-y$|GLH`+ImuXt8+*s|Zwl-NFsUjw3NDs`%o*ZQw`k%>KavhoDNMuacw3BR)z{}Un zpy`l!f^6zj-z@EA)j5emRs;=_qA_SrM~sVt>e~i^)9)nV?_v&o{qMI-n#9GnQm{5^ z(Mp9hsHH(Fs#Zo2F~CxIkj~()%08vvXn=r&yMvx641w;+q2msygn<*}je|_YSjAvs zm$N&~`JK~LRw)`Kpf~bF6~v6e(^jhNC#*{oatEy}i;t6ZPFn%yS$@8XejSObuY0ozfH=9se!YQ~<+Z8ruMP zv0zhS7cR>rl~Crxu5#H~`3k&0a}`j+(?|i8zFXzpO3m%6eL#dzHErdup>GnHP>m4o z1j*Do04p|J8=8h|Mk9ZiPwHG14`%)h@*gwTR>r($u*AIJD}xmn*uXy~=bFAz!i2=+ zb`|DHaae~sB5lQF`CH?l=3=h&Frb=bYbfl~sU}x=<<1Uvw9YkWO2mjV;VmJ+O6!w3&9^gwUSb<-5JR@Z^~E08}ZY?jT1&w-y#`WYP*bAu3dDBvoVZN23L77L|Nnm5EqeUfYIo&=JMPp#JwAe1X>XZaM~?`^M;@d? zJ8bc#70__Ga?_fu(FL_2IpUijN?^R6cdJgYB2NyEl$Z%?_kId4B#mY;iYfX4BBjV1 zOcX;mRiD?gncm!gACbBf(e zUWV!GyxRCQVjC%(KhrIYJk= z5%hrp72^*fvJIc&(^YdJxPPWF{4!{CGXniFS z$n+BTly`xtcMHcNJJesUF|s-5V9_V@so$4q*Kd=~+@{fY17UaaqWzEu9!_`T^_FXP zmLPb)@7cj39C|(R_$aIC^k+@;3Y6k2u6QIQr!V>!E04Fh6}x>?!>YjPYHec0ZbZuQ z3X4|SjoO>5OR>4lk{e{Kkeg(V-4%=j0tVtk#bNAI$eGN2)XqL=042qyZ?tE%+@;U> zM)$NU{47$Z*ouh{!ts^way#>GSJwn{>t5xV5A(MHl;lllyVCKeEs-D)A>Ts!FR2C@ z(N(Da`Uin%dzALXZe)Jj3KcICllg`KBK)0%~}27FP|@fNU7&E&k!VMLUq?^Ykh%OKp)Z@xAYOm#d9L)&AB2Bo&clQvbS=X7$G)=2QQ+5r+Pc8#XWKeU-4RY zBC~Qruw=|`~4yN%GX0btD~3ZuEa!MC+zg4Y@gvmlfV1zbc%Eb*t~fr2o494 z&pWET$vQKp9U2G)u!hfHVe=1VjLlsIGLx826{xDuK46;#>>^He!<&WLhp+D>ohH2B zs=4^R;t{ty@EXs@H?Hf;xSfsl!DnsLb>1O6uT-3QrFY1~`f8m=SxTDDE{9k9eRyu6 zDRH?^W@pax&P4m+2Kt-weTG~&7i&45xg`IUY7}_PsuSAgz;fB`^7NG?2n%%-jdmoI zQO<$)#JpZ1BL3({7~!Xj^%M%o{9kx$e!9&?Fy`gr$En7R87w&CJCx2(4fGGKV24$n zmlS%u3hnce+Xlnd`$6{1rgZRfA+7r7juJL*kARP@t9(;iM zK@*NFTi{m0o0*8p;#Pk37NW~*v z{eX4M*ZX*B6ir%Y2C_D>^cK#Cb!&&pDD#th)8+bG8y93hf)zs36u32WjN-ki52+^1 z%&jx>#eKr~_}T}|GvY9TlvXpJilgELHkqI+)3F>mlWx@IrIb{-TY{+25S=8f68yk6 z_S=c>>F>`x&WLG;T(u1zd+d4hc`>dy_b=V8?EG#g!6?bkr!MqrbLHmnm6rmykXXH7 z=8v|EEuNmvF#54>3A(YiddOtLq9R^<|DyJOI!bdQxTql_(HtTJQq(hu$+0K4Dp)%g zA6y2C;#Wca)C<5f>x?#BNY2V6!{sK?l#Ak1IR0sF=G*o^79f5It2j(ZIDX9H7yJn20;Jce z5cl(QEGBLa8HllQ#Us#GtBxLBT`iJDC0wld$kguQAv(2r25gZK3Kb0&T5P4;%0jym z75qkG1|?!Tb0_sSn6*3H_Y{A16?)&)r2O#4sT(U^%=vr4E6-S+kk!AIPYZNop0Sd>SK^&oK9*cLGkR5ASK{i@<5pgFx}x8D!88yx z<-T72wFRg92<_?RyZn>R^Rg*pFu+Lw8jzZSQ!}-RJgd&4-=S&Kw)`IG5J`VXE$z@N zO%-H9n&BUBd8~WF9V52e05)FeZF@b;HaE)`jVwhknubP=No~T!IzmNo)heMJoT!NX zDAgR1DnJjH44bM+W;>47!bGI@or~^1WY-vI&v%6}rXzN!E%y1W55=;A)wzj3@lOIX zuK-D-gr~6KUJ-KhtON!g5m%oBUBuPWeWpn{e9Mn>3EZA2V!|qMTc1qg3&!w`rtI!J z3t451ZmJsC;L$KfIUi<~^qBBd&Zc`kTd?ZzZVRWMpAP71 zdf?{{*<}q{b&itmY=jqBUpqc1doJC|4e-#?r>l*Xxfg71^yOJOk1X``Tgf;mHjb3( zncSQ}PV#t0QxPwQh|#|szx~2bBX(1c0TD?oBaqtN;!%-IGf%gfA#uAPQhSj9+Y@*z z-mT{(?E_$7SKjwe<7Ln#%Zu<5_YXN)JBB{GT2mm{vZEvOpFq#oQ94SdA297LJWc&y zp(b!mESgJ(nQtizgr9?3#_Dj>Pln8a7yVU>m(+dL=x+qkE(d0@cC~J@K{N4K#Be;f zMdTn-r-G$o{yUga%Yq>AI2mOl|TD0RRXN1^2yunxV z5&uAHMHReN#Hpayi+*ZD;Wl>97l(gkLfM*|R2ho?XnWwMQKp6j11bn;roy(~o_y?f zumL5GscD@l2(hWK4v#&>YPruNATADDFd=5^g0(HL>E4}f$Md(Y?EMFf)Yf9_vuB^A zeSdW9mUp@ua`&EtN&fYVp`oPfxQ`{S)m>-Kk8|>$O_!i;e^eZNUs_dWa6H**z{#jA z{MCx)-KKs97peQx7V9YX63K|4N@V=2duu=&p+P4#fx(de0kBF0D}RvVl?7{=CG$MT zB-@iQ{;COI>Y$ouy8QaJTf-6*$)XSJnY#>FmQ{LVeHX{-ik^^sT}@RWt_Aroupn}# zR&8PG(DF}jic^I*IkU3`o;gi0`ZLDs0(LJsf<4xxwf*Ba^o9_imobKq0BYPr!7Z5+ z<~>BCRa0TvgGH$Ki+a%Lf)ws-tys#7S*^%i6?rbX7hUD#xXb63ot=(rs!JGVcN88i z>QbM5Xi`yyiXoM!Yq7mDaHi&4XY_ttlxktfrfc`Nif)3`X^d%t-{FrG{~2n6Iya4J zrqzZMDD1=Yu<UByOqHnF3ds?eCQ zVnmPD7fmItWjy5U-hmENl#{VyH$G74VdPb}oc{5%nS!n=OalZR{ct}$`xZpw{` zu6nh`P`ptIl~g8(s+qPO{H?8m~BkR}y+`F&d0Sk5O+D=6U9?^c*JRD!7< zBQrxaCK9L6TD_|7@^Qpdf1FkZckGsM%=>V2s}J;68OnTHeXR^koLSkq#NVIV!N|*H zLkBmNrC25UcqJIttY+XCPMny_k*s+Td&QrJE=> z+V!x**df{?uHmA9>Qlq?!r`t9HC-s^VeG`Eb*NIhcFfp>@N)^71pUm&Pn&BGkN&u% z(sI?hps<2$W;TBOcUUZ$atnMCdt!+vpDDn&SpK-umcW~G$-DORba-?4_;NS~4~^`4 zUE7&0o9@`bPD-A|3MEAlsKK-!Yqfp3TkOIK(qDeWq1Fzvu2(jk$LoDc=Y zc;jF{GyivY!}30`=w|@*CN)lK`vGgs&>i%0^Tq7*&^(ZhX(Ke!P7?nJ_1$;>y;E_* z0tYY^n0}j^A@VR?EWH9-M{!PH7ymGr>o0!KJ(t_9fZJIt+`?SW6iIa$433|sL8tcK z|C`I*qVBs`^eM~Pzv=tzsEH0~?(OIUaT)w_M6RenpqA7y1xe|N#D#?gM}aEM+?+2r-) z>ItN-;ofs&L~Fy4-MjDk5h1e}O#VCfT+7g^-pH~kt#aLaRS>KKN0`i9W8wPY$ z)$cx-KBDc7gIE$p*Rr!k9y2;;krYj zXDb~SKlyYlp1B@%p4LB{s*xI9o`-_#!CS?Ng4}farmv~6DKWNIn2zwk{P)|=Pm8>? zytHhT;N_)_6u-WgeWXy$F3YCN3mbOKe_5CWv6O-*ZWRmVT zX~E;^ug+Y>UFW!`+(9}9(rBz!KG(0%F|aBKu@B-}mW*SAJXNL2Kt^sLfp*QCsZLC+ zv9LQg*I#dCiPL#f%d{e?+dX64f_=!;U5wjd-BQtEDhs}l&TPUl2Y{pG8&eJp%(d?2 zq`VR|CgufszZOg}2OlMyI3dq42Djqoe^wX}MSkD@gTY>XGP?M_me&16p(s#-)c_}2d zdFE;*njRhPP`vXQ>_vKEz8<)+R4Jy$!~avaH`R1`dDG5oT(g~);>PO zW7>R(xFZ|?kFQ$Jd)ZG%V1YP0(KD2By-u|>eNk#c5n#3alhhe{eFchyU~ zTa;b%9P)$o{<_Zji_tnl*EwP4!|(8qG5gh2cjOHqPdx6H_8Pc+C2C>$zuzKZ7#yrn zh?u{`#KBM%Au& zLhz4UC&F8eaI7BJd*XbTk7J!MGX zy}D!g*Jg+^zS6-WQP}Tj4=z}i3G7@42G0Wr3Yd8n3B)yDlkBqKv4)y0(~oic zv_0}r)9j6B=Z$4%cgmOR$Yrbzlr9dt*advZ@)VWP#-s$_QwvVhc`57J^oh}}#|--# z2(u19A?{LLiyCEBzQ9Gr7U1y_x+if~+`4|bu83iHE^N9`Xz7_C`*OJ^BI!$aeQn(|GnX2q%20CDta+wew7t7X<^lOE2}9(=J`S~08#qKZ{0g`>m--+DHUw>NF+W)-`?PGR z=E(4B($9Wbwx3p0%ScUlf2f;OZKbh)=X-wV&BvD-_$X3DG;HwWjgupFE+gfSMJ?Z< zn&6k!AM;V>fVyjV>CAHL8=jS?i-t0wg(w|GibCJ*Y(LF9G3*7XHRC$8 zia4~?;#tmBEKh_nH}3SQxkO6RWFM)A}DFVz9-khhJQ%)yxK~XV<&qNLR8>qxX9UkzXq@zOJx;ZX&+PX)9=EN-YLQdf zeB-fL1&JT{^tl`YQ#V9Wt08BiGJbSet+ybcjn7Xg7Qi3>=C`A@9jZoNWIe3$CUhOk zv0UF~&-~SXRN@7(XH$TV(GUoUzJqBbNX%PX+yxg&!fc!xWe_9xk9RGfR7{zr@k(f zEa&ixtQr`2$2A%hV^`RQXYCN@K@+yKkrBkmXd#5iwRZX1cn62pRi8_=)vJCND)+&< zD4{p`x)tN~z+QDKulDehcL&(V5|LY_MCmci1i7dSyX^*$*z>UJoqzE)a##nfjABmQ zy?6pQSW7tvFj8$DD=c$`>6qTaGc3R=C2M);gP+1^c>00ce8jHqm*=s+g9yPa9XJr) zrtAtF(o%iUlb4YzNF1S@B^g_5b` zQE(bE;thd`Jw@B4hk~?sF?R@2^5EcG*|G&*L1x!VDp4gKVE3k$cgq(QgEE4MPTX@b zD;2^NwLh?sGw9&aGumf%K#i7|nOX&M(zRgTGhmT_5>QRVTUR`I`va+PF4KIpqEF57 z={V*o+ly80*#CawYg@ZTe<@%86oDc8-E!jVC?O(-MI~hpWXe&T0j}t=)%{qR#lpQ} z{yjkw(_0Kk@Bq~b7#&QcoS$BiBO4%FS%7aw>h!{4lmagH_x8FW$0T#2!ugN}(BHdd z=;vl~4NNcI1diWo3b%n|Dt_>xQt>Ssu2cf02wMPZ=ahV33Ey`-=r#D@5_$EY;6azJ zNuz}<^$5*k%h|kPbahv@8>bU{R^989mJz^Z7u}zod&}AhXm{FepIJ{LrCU-DWGY+> zW zyrf(nq8tFUc7L<@L)-SSu`^sKQTGWHX!Uiw`2<--J!={Lmop7`Rti$>T<5+?4;5NAjYrV6v^1 z|Esq2__;V33?>BAOUZq%9s@;UDOCY%^1U(GKPn>G$y{=euxgiF0gnhincLa0S;ix= zvXn#Mkl_4uo3^khK77(Z=*V^uuiXDBJAeHnlOT`_fuuXJa51-g6s&kT{%4`d7%6-h zVnSik&%bXFziBff$UIH0eJoTiHn!9%JUQSrazCxgAbe#Y*T{7P?MCRu;3#%w26x3j zCt{w{XiXEY6L!Q__?@j<1b$GNoY+~Y1xce`fzg+TEwlym5h_+(G$r=Ph;DPE)0y@^$f%B@eXC@a4Twd7 z55*C0v)RuF#T9~rf}%7gH2g;j`*OMAQ|Z<_2Gg%c7npVU?eUeny-bi;wJfg+TSwm3L=uw0tuERM5G!{_gi(WM`Z`YRmG^po9$v+ zQ=L!)tFig3h0PmiA(_b6x2`EM{X}v9JI6ZF1(02v z2K;hg;qnwZSMKqGptIIb=-CoXX_B{bg{I@ntF%8Cn|OR0#he!CA(m{^R8&Ha_q)lZ zOL003-uZ^m*i!@lLq#wD3#(-nI#VlwlAnx?-sHK=e zoTg-Jm!9_LWCeFIBZVUKGFZ!SRALVuDdzKtaU__P_Z#u@Q@x(-(y8EB)kd+XF8%mv z&@FskxNzmsuPP&nNyrI;{hfh5QRN(G8~xP_$FF3vEo)XvCa$goR!r7&@Dn1KzrY}o z;=rC}rOr&Hs;^}i`=H7BLIZZ|B%%kty`CsTcG2UCC`jNa*W*D)mc*-h$QBVs@GoDq z=C=zA{Qnep#cDS|V)ByUu`4(VRbo8#pk0+>RufbtA3m4}*9Ky86NJvuxy4o7iz>-VN&ABu3$us%2(E7%qTG*P%HL*Z| zPT>gIV02EU>_U!Zp(w6`J>Fyr67T(_CSmtUyk>CW4JDuO)xfgC zgQDG(Ga+j^@O@%#W*-*R5QO9lK4Eu;G3m;<|(t>l=_&VAOwr95r$`<_O3N@9M z&W&A1=2ypPsza11XcaM?Z&MU^0;1&KMH71OnXf2;r+vsaPJlvO`B3>j5QPK_b1FpY zcB}CN#Ssy;i(Wi>T~i%FeyTGnd_#Nf%Q8p9K}-BkeojN1*aqm`pMgCn!>WSG-95!!~ewG8oh6;(w*y5lL|1uXqvT*43%K$b-4g z^~3Z`jd|9w`X-L;`Or`;a`5TA#+jr}0Zw4(d?NdD>)&Zv>u!&>=H9QShR(){em0l7 zxt?+g(|skXs^=_A8bAFzgb1K+BcC;={tnHY2cFI#_TgB^#Iq&SJDFT_lp?vdgQYoU zK~!2C)7m3`rT8`{Q#cvn_)+3`UAPoC4yE#U&#hfp-+md2PZYTpn%cYpufJGSjnFcv zhj>ofkhSGPp-TkYI^$JpwG)uY&hx9R3gR2Kh+nl(kYU#IIZ?KUEgQC>KP_4c7r$oV zFZvU21Ek#hKmYsfH;O*kDxT>?$U>*UzP+AkMi)w{sYKvnz;20nAh}){Jl6%+<<>r@ z7vw1%^s9L)AS(HEmtiJw1{YqBXk84Sz@%o{N#8?dWa561IwNy0*2vp_KT8+TN){9; zN{n8&X8aIpr32?=HHx&ZEk!U0#OZ#6OGCDiH*buY+c-&dN}%wKwn$qZpO;heQum+! zUFWK3wtmN}^1^0AQ|`O8U6*k^c?aF9cUL$OWqGMY|8s3MVUj;Pn<#$pC-`9IBGdmR zUh9xpzT|q0U{G-}iwuXb$s(mOZ*Av~;VYUvFl5Zv0D9_@8&$UT^;oV4A{li1mQmWw z<4X^oW?ulIrur}lX1b4`Z(+&cO+jmI{aUTc7y1p=4+xdM!F6(G-tElj z);A`9KJb7zG9h!Rjt62b@z{g#-$BQ>vDU9Sji%eZ*}(7S2vuLl%83P<(CG9q9%C~z zz2zo{=~8JzzmT2OWS%FzoGgz&&~;bUy5@O9xAl}e4Xi>|r&FCld<2es`)3E0I8&hQ zfMc2o^JR%jV3<@XThOo3l?P)n%=ZQJH9v|H6MovcMtn3irnsMJDjX$=UJ7(ylFWf9ITltr50=jkDv)ifkpw^cWsr|@<&`6$p+AjV!~is5wuC74h1EmmEqgF%JA z4`*hW#6d`CTOEOHL9{!4lnIB*UL;y?vPgmmfYWhhV9UBAQlnjod} z&9pXY2*gF48S(O{OX$=>c5K|rS55wj*h&5yJik^j6z+oRZ1Ovr=Np3BA7ms%(hRPM zb8;)mDH`lQBJu)eoBvSpT(d6B4m_b#}QHT#xR1nfeB1zBChkC~GC*>&Y&G zrbDq?>9c^c)0Iv^zdUGaCQS@XUrkQI7fyo{?#1_Ct43hwk`w4(3VI106ILXF3irQ} zV%(j8qZ26YF7Mm5A*O;Mm;@*Ab>vyZ?PRQUNDRSXRwnx02ASXH$`2jo9S_aZ<=HXV!r4QIqT~ak9 zzF?b8Xrw?ccFk#Q7Kzkp7>$J<@H^|Bo>WEne#%v@vB;b;0zU>i|LD0}`CM`VzDryZ zRCpBF@kZSC?;FnTS0R4^J86NXc3&dYp#absLG?lx;;&|UJLA`&U`6z>7_6R=Zx`GY z7v@IaX(?}@dE@;|S{fK{fZj9-va|Y3& zU8ev*W!Q!aO|^pZ67Nfu$hGV|ZoEaC$zYH4029JX)XI_NWA;eDexBzaZq0&eqZ@$0 z_(qZC4=DM`(Ze7_XIAo7?p8#e%w2@4Xe#_8tYNygJi8`+y9b@abg%prd@PTanA}KS zu4rP7nol%D{Kgn#>Qwe>JymPs5QwPw+59W4AI3D@7L@*wh-o8s_Z?yy0TI1aMc6Xa zB{ZhO5xSHWDQI+0{Q>oDOp{b1EYY_z1^w9GsAb4MeGqr*b3RIT$!|A+z5lc504@yo z-SmNp1?}AYfSsyffG+Ospeop!O;08AVWX=q3Z3V#H9_z491SF%VAQiQR(u-Ti2Fbg zW4fkH2i)Kz0jd|GWr?pbU}4{tkLCquT<-~gCk5_TuQ`*U9#0LlCI|5!L#@IM`oIWl zcy1Oc{7SFk-R^8V_R=Bz#mwE&e4+qH8l|mzQT6Uf8;Y&Ej8szXFH$UoF-T_)6ZQv9 z|0xN2ieiwLbJmfZ)l@mlI$*{I8^zUT-}SQW;Scsi>7}n{Nc4o- zrXp&{$1f>CXRs;n#5L=^JnmX;8pZ2+H0-mV-jD?ZjzYPM5j}R!n~ogli*ztJR!_Au zv^pr_p%3Ub@Lg7;8Ols@lfy|Zxm_(=ZQW(7pW%nN%(KN4OF!oMMjZW7dMotx2>Y~X z?U`>ZHDZkFM&sK&Bo=Ey=9{&UKEOG7`DgL5k$Z|$E@uB;`m&`siRS~^YOJz$TWW6zYVDRlBy!9`)a zk!rASbee)6ggrJt+B|0K#E;r1Nblty;! zih>Ok7ca65fxrX`qB2Nv-YReQ@k<3o95N!{@66(KtheZpIM=J_TR z!HdJfaIeV0yH;vz{sJ!XrV>Mz8|9QQy~$dpRfDN6W6*ze{bw?=oW(_cN!oEkhYGX$EEhjf8NSUpXY*IPP77#NL9qr`Hg)~T5q?7y zmuTSsu01`5lnB9>kE{LAS;O+)^S&}N!xfmj$8>)=<%8j$k1-mbXB`m-G#<-UA51yg z$f2dZ?$e*DZTBDK)viTsrAUuW`2}I?Lmmq?5$DbpgVJ@sA#*t>CQ(P}hY(Q=zmwLF zdm3VhFL7Qf>!q+qQPlX$Ot)a`3#VMx5+sY_zyxYAB7t=bO3@PIS{@bZi8H&^ETGXM zi_MO(e9) zIxpi@+huIEOTf^qUPy>O;U5Ea;+2A1${)r;3oc7lxLUSjOPw$FGHmCI7Euo)@b>OE zt(XM=@FM#hpvFMIBgS1eC%|GELJtwnHC9=@&oz5w#KvGe+sr3M`oBxs)lkM=f?#26 zYeH~UW@F~-{InSZ^?$DZDp&(KjJuh1M93LY`)yR}c_Fa_3NsZR5@)ib@q_a7Me>xB zFVLX-bQh0%Z@K2LM7K8Jrf1Y}5HB(LFX%>@Fl<7wMfiWcHu27uPwL!X1!^O(uNPSgEuk6NNWxyaF0 z66cz7H;+dCWXsikwe_xrSjz1$A#WEs)d5hA3*WNw<%EnavHeWjxvPP|fzi}~mO82; zmWP>IFdIj`oM&HZW2IO&y{Uhr`ul5kcjkR&G^GEte)myiYZL(SXy#Qm?iL8QYp#{v z8v5B<_VkjfmUiBZC-eqF`@AWPKv@*K47WP%GT}SuoT$j>^dOa9e>(nHKn!rUjCbI8 zu75R*t|mZ+N@PrpZ?~Fr*Y(gh^DWoX4jFQ~JL8=nRdQ0krWv?6IJ>%qQa&%EX~wYw z3kX^1(Q%1FelK5{3Be0$wQG;{lk_+~m?w3=I)tL=Zp1eDVL$Il@aW8CX4^H%+2?*>?YSWMAR+AUf5>@T z!()GB1di-|wU)Dv+pRa`@wY>4_EtXJ1}pi!a2A!MEyq*^8an)}TBPp*lSCQ_fpesK zWIr{}B^QXoWYd-RLQ{^E?slM=LxK(q$L(D41=dENn2r@{aXLSu*SKI}4Or5{*Jg2; z$f^PbD{d3|R!JEo@F?HP$Ha%%%7aadj^O~|69@h9X{kJ&%eduFW%j_tY=z;pY<}&U(X<_3% zpzhmZLd%wZQ0$n6i|;R*+l%cJ6U`?f+j-zP^SmvGk6p;R1aTy|Ev$6w zPX))ueK4)_cNr_81iOU`)Si{ZyV{zzr$boE`1XAoU98>rZC=9VP(oAXz86ayUze+Q zHx4mRmV>?BEeW||KAxa2caGyG9=M+>5D`ZIxEqBR=z{J7NCmn-_>?!FY;&%#7lkxR zJ@Rrq7+O{Ws;L&5w(qPaEUvAf&luB0H|{nJYfb(x?T5BHrF#x;IAC=s7 z^_ye7+uR24?z!-jhol-JbJ` z_q*KL&em5eRQuJ+XFlY!a_LP?gB-D~dRM@csi5kr#!;kR%;@mKL_l=yd1!uXN6zhF z?S|Me`H+K66|m=5sxN41jzF`-W+L1`VrMB6Fp&HLOk!&_l(F*aG>}9E$0M94kqv*o z3izQhtDpq(U0iRz_H`I^=BzZf`kvz;hi2|azcdMn%Z$)(xT{C7FUvWrKG99xtAF~a zs-hYF%QTS^4q($I7~p_8l&HnW>RX{<>7|f|7HdChY~f!%u(w4E%IjPYsIae0hUt(V zc(%W0Qt<6S~P?XI#W`bDLo11ApeCl3sZms(>W`ElU9Igp}SP;&c%|DefhT#r+m z_9+6!v!Gz5ru`!22!OpOvY?^0Q~63o*r9-7Q<1HWaMx;nG%{0;+@fa&x?|Kmi~mnV zSkBYDWpjGfbm!8^jtX)GDl0|>zhyp|x=C$?0P0?Xz!XXV5x@$j9w>y8w{q?2bOIFJS)?;`qwk5^m^{8&enXvcPJ-lGrizymQnz#ys;&M^La7A*- zjLAbGHTk5=3rIk>!LNI1A3+741)cH+bA6t?BM4Kq<8ruHf>{od1<#*Lk z7ne&kUf%`#w^o!f2NgA#M=B<+O+jdx4g0e4hik1CQ2i_;3##w@TNz5`m5uVyi-3Jt z48HZ>$7z@xE2D1e6NR@ZUhJkIw^O8JKO~WzHe^N?lZ8uN8;)IARnT$;GcucX<;19Y zZC&YIPi5R|Co@@*#>G)vC;28p2;EnU7e1vu3I)0u(j$_<7BH4&0BVTcdp0ufH>MkP z8ocG(0VZwbWP{6q#TLIfOr^R63_}6bxW;}nml3yNFK+k*#EP=yJ$qMP{;ElO;new+ zfNNEut=%4LmGr*ATZMVmjEeF$V%Ym@&VKwd@0P5z?BGKKOBZq%7-7%0vZy%3E{F^9 zMKP8mH34o_>gUpMP~9s=yzWGmB|DyHF+SQ_A9+0tA^bvqu&`NPV8AC z(5O9tmAKc71)O{b5LNS`>6RMNm_RBx*XPNp@iW@;>Xa(KwjOJXDCVK@r|e%$`sp@G zk4VR~^d1NiajR|Hh0 z=DbI`rKOJo+%7-*RmZ*&Y+-A@zkFu7+2Q?KQ`P!XL{0l0#sQ#cEla%qtsRkRYX>M# zhtMMcj*t;A1WYq2xS3SyKC+n)YX7Itz6)dOwA?kT6qS{?EVZ^qI;M3O#m?Llz*${h z$cs<=W{@7^zlPkZS&22Aga)Lw9htiznjl2f*fl>^r~_8c7)tB{O32O}b{6h63ed0@ zUmas@qc;JX4U0O?SlJ~RbUk2xc~N?VbNgFuBi@wG+ej$cJTCpvuFSHK*lv}=wWY&a z3gF5#Oto`qW<_g4;+gHD^~aD!o1XNE30@K z1e8Nla>zI&ZaM_@n$Fsq&gclS3GIKtHObk+!Zra_30{T|-G+<(ZI~FoWj$)F3*2#> z_EvRo{aAZ_liW7^;yy&#yfyrX{j;=RzaCR|+_t-&`+K&3gYDSKbd$sVIRr16IDzES zBOR^V`o{~d3H>xJ4p~>Yr5)q^om%ih7;!ajOr=0=()HP#p$+!wfX@bmoS$g@$)6EP zWJ;i(N?8S1Bp`~Iwr4gGI8VM_qP;kzOaOC+=A|2DUq90jsZfPoe!d%ickp^qjD;Txl0mj7INC! z+>tVCmYjx}yX4%sBX-Bla+ve1*enb&hsuqdwj3IpoQa|AJ~Y0?Z>M~@pO zVIJdfp$j>=#Qs(e2e-HX{&CIIug*>eJa}CjggBUxiu|+H=K>xrkX*L78 z2+uCtpg~4ZoXQv?0DP8_;x$B-45e1YP`v}pS#YzG;;(mqKPlPrUZV|KDVGE45H7h% z(;uZTabl!PgFkp^o))9ZisqVC002-*-O9IzLMv~z(94A1#}!N~F42nd2~fscCPakd zpWD0EcKdYd&o#_eWu=v$1k^Br-f8XiJX zBI|9x%p9~jDfPtnw(~XnE3-SVu_z7*%jkYnVunG@rFg_~Ht4|1G;4>!4X&cRNaD)8 z+2fk)z)df_f^)u;d*vGSvXk+2h1Su3?rzPt*!k0_m3a91B%Yo z=Y+2x9l%tatmlR_V0Aa5Xb+}tg|q&;+-T#1?_J~mFW{w!(c0CW7AuqYi!L2wwRL@q z+}Lcbw@G2bj>9k-ONHVZfZS4T!Q?%iu1!|=FJebj$|uO$7>w|Lc=nQj^)*DFmEEQl z1{fOUTS4|6F}rD|qm77;n0-sw!?aTT6CPBNL&H%5?%(wuBV?*w&DU7{y+^p%SLj#To0;0e#`IY5wRyD|{3 zaxLA{tjJdLtTX@U%=X{y&F)%97-Hr}0=FtN7irOqK2q9*sz^_~Q_vqkG;B^0vR(vQ z85Lq8%{c%FJj_qRj14fSmw7Eb!TZ=>z?Y8-xP_J7REP3?hNlPZlVuQW2+KXvtq46k zey|xWuPzM)#8aVtXY@|N%u|`GWXCM(tklCiZAC*jqK6q?(s}rQ{qs23+gSIv+z{G6 zxhMedbl2?Z?n0m$s;|8Q3EA;0a#NTflHYWYg>V%oyxXnA+kr570>SiQ;i$|`LXIDu zP))VcnmmZ!4Lo&$#y$CO4wy$7^B6zC<98zAu4LDLI|5j#>&}D4)ky+e2@HrO%46UO z@j&I5cgu*XNQ;g?1iy-@1orN2*}3htG@YIX9nnlD<>~)c*tPk=1(^EemdTxE1VdH> zi;X;ufdD`2?0Bu$-C`5p1MjWYs^AjWt?&MV3-rA&T3sJI<{DMlW*l_XIsh$kZa2%M z7!QAT)5Dh*rNfXmh(=k%a_}rH34B_51PBOmCw`(>wnWknzFROU#pF%B+WeFEhXX$Bq}eh7yQF_vr7cF_@BbS znjis14IkUOeM$mrLGEM97!=qm0BTS``||ELIY%)vibG zdzX$|+yYxd&0XF@W^)lA?Ax1Twe~M9ZJ)y13q1amg-^;W;A6djvKGiV3bsFeVVhU}n=Kp#brgtCfBybUV_dYe!fRYQNH81H1sIkI z5GFt!!WEBw`#PJaS>11XlkHKwpmsMB5##r*lL`g1O1!`oer402g?*1sC3k&#W>hTh zir2pv{IBl4Lwmqih`Z~*-X-dPy`lWh>2PeJn#r8&tFxbT;hA^faSo0LLV&Fu zajiaO?iJ@yIn(awif_2k@Me2VDD(9uwTf_38Z?Qq*1CfqQ-pF z8FsT2Ezk&5U*1hHmB5}eHoNUVWaF9R#b|MW>f6q;0C%_*YlD_cN!D{u-Q>JPtNx9C z@G^~GZ!S|x5MCa{(qsr6RSE>#uZd~@nrHVkm~smzy4{KqS_)$yLVg*n%GtYT?Mgq+ zpxL`nuCki>+*-5R*>`es!9vzzl%ztaMIv}?UTyc?dRlj3Ybu+j<$L7df zwhFZrk87)Us;NM4bS4>?9)uiQ={ADaeOY2#M(@eaCQZL7=oWe|wJm7*O#qDMhk*16 zq^^Y4#uM>5Vq&LYog-LqUt=Lf6<>aJZXbYd-r(bty?4qCrM;!dIj|{{x8i9EMa_5c+9&O#b`=QJ94~S` zaWQ1e_EznXDWcg3#s%UgU`j#fA>;X!a+C$9B#G!&Ezx>rz$B@zLARc**zBH*m`6Sq=V9b@j&o_#nR$ly?4f!b7@9&mxw1nDU1Q;pz|oZfZ*nSbcM|D@9rcqg*{Gq! zFn9H5QyK*O$S8TdCVKrvS`twYUntaUz)VR?9K`DAdP;&RVE)sP){fr#$q1fw-FbcP z1XrbCcvZjK5#sq8MALrv%-y&?K&sUSe~?@Bzf-bY&I8)`R{Sj)W?GV<2~(a|8TWrX zX!3sbAk-wn0VI0yM8gr`;Nq)(?vSq+rYzQ;C-beG{R^mn{R`MK)q$3^oW$U&`-8qo zs7+{Xa@78kc%^~BEXZi$SUcu%x}xbj+lYtyXBB6N5nDDKl(pnA6@#N9M_T-_r8nxS zc_o#m-GgRdTHP_2gDJEc_~@ut&GfOVNr!)muIU|83O8@M1lYXBkMQStSvL3k3z9c) z5BxEBy5BIQG>*x+v8~mobFp1$`P%UMQ9j86LchFIPshnB&pMFT!K&Z>bNeh0@CXH` zq{%nE7GR-_ap2t#H~Z0B`7PkVE|q63UOO0v4&HoGQ(m4t!*O+qedYi8)&0gfM^PzL zX*FPB=n=iaikaY)>1Eo5w=9Wb^f%@VVOYowOPfDx#huU8?;w`NrS125Fm; zQeRTrgY_)RBh~96BJtNbhKQ&xukb0fIM)#v9B;CJo62ryf})%h zJJmlvIt|Hs->6XY?V4`(Xhg#2-N@K93#CG<`>$rHj=-~ws;bMZj|AKU-&b_FnO9nF zczRjkRph4nTK16+EX`spC2qO8u)!Mz^wUc0g) z+h?(Fsr8WiDrbu@|CBms2yBwT%GJEZM+A^B%`mQNo|XMV4O~&?g)*bd4--4G%vE~A zyx5iP`JVSrQ38q1*>k2waTleXc4}I2D+s7Lv>9u;ebigk`x6FJc6DFuefO7;AnZdw z{KT>CUZ8%mH!9d{(=Sh%G(}HaVn96v{F(!Zn4v4}8_ z+D5ntZJ47~(qyh`VOOMxhGUbs%Oc$A{w2dzuTWL8L-pfzi|b#EP4f!&WR}fYU>Oc| zu8XX*WAVyy695>LZpV~Rp1u)plY|JEGUUx&{~!1q2j+P*YBq74=}g?u%VLnW;V-WyQ~V$v&iL_;0&^9O9?Ug zW1NT>l_cff;YzmU+OMko>esqMfrZ&)DV+2BuDhAX0g^Gz$Q8K8nR*}fuQgMJD|c@f z>QH+szN7O_yk{mKqz)9kPrLaR67~0V11q<{qt>_;%FMXg3Kv_~x3+hvMb)y1-Ct2ZqF_PiDts8a$%<&Qwk5@u#^k90l`y~+K2rf&&r^x)v$gi}qa z4wWVEHxZz=jiA(H@U!0&PrCPdgUfnWXk<+`LVF3x?u6vkq!?Aw&z@qFPuT$BW zF7@}Qk=~gMLcT^uhF8-m?zR?SYFr(aI_YmVD|pVWs}<+o=NThLyzZj2D@J>gXq7Sk zUhN-heqis{9x=&C+L7uV17$AVc2eE;Rqb^Po!FBkL^k%Bs}> zAl^~i$eLObTfDnvqUclvQ@^%(^UnT~d4$RYA05=nBCpSdnO`ma1_Ay1_{8y>9QkA7 zl7yyHGTVj$pySbKKW#g|l%H#CS1S&S+bS#@=R4|Pu;)9G?An`pgSyYod5PT7%Zm4Z zLUt*@8P!{gB><_U!$#T4a98#G#v`?`Tx`|sdd}`f`pbZ4%dnxr-SIz$KmT2HvAXL? zPNrkc_ZVC6n($P%)}#n*8Ff%=x76EJ8XX-Zj1#lLhgkzmECM;iFcnYd(kr3LG-?yM zVg%a{7_RMtwT6Z9)7TrrGcTubQM(h%;@*^UNqT2#h#c(M1zB%lj5shl@OR0vS8xB0 z_-q=|49bb}c4EQ9c}+knKx=RFAFvU~q;lJO-hSzkN#wQ8o8m>VMUw=uG^~ALA1}gt zZ{EfS-)e5=8Qy>OnU^iV>|^8@)k;e|-8LWO2$*rtcwU?K0Dhv$oY({2JxJ7*-N+FH zNUwFZ!Fll$_2L=Qqh-TT+d_<*;WMkNRB6DEUdWJ#?NDem9BR>Xu;DT@O0|C#lk5Ju zUqg4=e4EUaKOO)SB+#F3ACbn-xbzb>3D!SG0IDM;1?#q&ri4*rWWgr4B20c2RC7aZ z4pAE?qzLNWuC3Q40cSoP$vUY+SC!_7Yu1R8rQB!%EE)V8Gs6~%X&vEH3NY_J*Dj+4 zb$;FnW{Rk8s~UXuHoQd@0ko; zBK6_hB-Y0jlLWJDd7&sk>&WmY0aEdXG`NB`T_H45H%k+cEY^X7l*v-{%PPR~b9hu> zlS+^i0OSd89?%s#BTVDSKfA4FWjt^>6dwNLY9N>Yr$ zn(bYw-6UO2b}f6sTWpq4F&D;Dz(0%8lBT&0vYmAkXfTXftgylQhylSa6o)ImxhLjN zc9ht%vr7O1Fa5mT&koufXXO?H5dD>UF#?#nY=_idSRAp$xVh5xiXk2+nPB8p_~Y06 ztRC)u1&pKwJz=k0ZH4}=6w}ATi|>;&=+0kqBRya7j~W^}9^eI@1A!X(BTb326yuVl z7LSmH9=G}M^dbF)0!M!GMrdt?P@kmEv7mI~_7e!wPBFPtYX?D3OunTcvrTQ4J3Zeb;py_GAw&h!KaiMTOO>^_M#4vOBT5_H$lN)MWP@G?YkL80Ew!hl! zU?hk;{>tud{?My{|9MRkPZ!WrSZa4QwWAJd*dyw-4wZ28<9{qo{vLZ<%<#}W#$#I+ z4*RcXPyM`6J?HER@jC$?k`!`KO2AUo5;xpqqvqeFEG>+f5MAnm4nC5Z1OJa)09d

Xa&5aT?(h5&Gr8UZbalJ-f0s0K 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 WYi zz2I<;S))-9%kt)^oG5Kf9rNf>o6rq*%%I!Z*4GwQQWa^bHWCMI#&DW`g_&iu$m=}% zO5?x=74m^7AtqUKRw?Ip+GP8-Zu^nj199PNwf#R{>3v7zd~jh}x1%|UVWsYhrswr5 zSLMOR-7bZTI~1`opWGqE+vC#0DNs1F7AnWBH8HQ_&#pA9uL?n!9X-n})m79zx8{ln zOV-tBtb;Y1olCvNIh*|rS&|bH_8HmJ$>MVT-1lm=a`8yRt(uC)kG zzTk$TPL#R(S2W_{7;2W=Hz(?;yH(dTc;5*+(0S;uq@AgEU{B$Vmlo0?etiE^R+h0x1#A##3Z3d0=qZ}n(X38)NHc12&Y-!QkDkIyTXWJqy#89Y^jG;VPG|i9 u$fqx+XxJ8a+YP4SGe7l*4R=|ODZ)ZiY~l-JhSeRpqvLhdC}W%dM*a_d)2wL# literal 0 HcmV?d00001 diff --git a/apps/Tests/data/images/00001.jpg b/apps/Tests/data/images/00001.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a5b6f954564349b0ed798c68bc7fb8c009e13ae7 GIT binary patch literal 303625 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 z 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

f53G{tr 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�AmIevBhd~3w3H-4h?& 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 H{~P~53tO09 literal 0 HcmV?d00001 diff --git a/apps/Tests/data/images/00002.jpg b/apps/Tests/data/images/00002.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d4caa0bd877313fc5e195fe6ba5bd3f6ca9cce10 GIT binary patch literal 295933 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 z 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%0DqS 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\(Z7Lo!_}OD;Qf@Ut}g=mM26C6Xx$lvfR*h8bX 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 G*#85f_G9z_ literal 0 HcmV?d00001 diff --git a/apps/Tests/data/images/00003.jpg b/apps/Tests/data/images/00003.jpg new file mode 100644 index 0000000000000000000000000000000000000000..504155048d8746610d9b28aac2967740be3510db GIT binary patch literal 296986 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

|AxbJSVDH2XPJVfa2NyM#nvi>xeS|^#bzYbG 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 W|8w;J*XRCk^Zviy|DWsN;P_vrGakYK literal 0 HcmV?d00001 diff --git a/apps/TextureMesh/CMakeLists.txt b/apps/TextureMesh/CMakeLists.txt new file mode 100644 index 0000000..bef488e --- /dev/null +++ b/apps/TextureMesh/CMakeLists.txt @@ -0,0 +1,13 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(TextureMesh "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS TextureMesh + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/TextureMesh/TextureMesh.cpp b/apps/TextureMesh/TextureMesh.cpp new file mode 100644 index 0000000..9764849 --- /dev/null +++ b/apps/TextureMesh/TextureMesh.cpp @@ -0,0 +1,1168 @@ +/* + * TextureMesh.cpp + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#include + +#include "../InterfaceCOLMAP/endian.h" + +using namespace MVS; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("TextureMesh") + + +#define COLMAP_IMAGES_FOLDER _T("/images/") +// #define COLMAP_SPARSE_FOLDER _T("/sparse/0/") +#define COLMAP_CAMERAS_TXT _T("/cameras.txt") +#define COLMAP_IMAGES_TXT _T("/images.txt") +#define COLMAP_POINTS_TXT _T("/points3D.txt") +#define COLMAP_CAMERAS_BIN _T("/cameras.bin") +#define COLMAP_IMAGES_BIN _T("/images.bin") +#define COLMAP_POINTS_BIN _T("/points3D.bin") +#define COLMAP_DENSE_POINTS _T("fused.ply") +#define COLMAP_DENSE_POINTS_VISIBILITY _T("fused.ply.vis") + +#define COLMAP_STEREO_FOLDER _T("stereo/") +#define COLMAP_PATCHMATCH COLMAP_STEREO_FOLDER _T("patch-match.cfg") +#define COLMAP_STEREO_DEPTHMAPS_FOLDER COLMAP_STEREO_FOLDER _T("depth_maps/") +#define COLMAP_STEREO_NORMALMAPS_FOLDER COLMAP_STEREO_FOLDER _T("normal_maps/") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { +String strInputFileName; +String strMeshFileName; +String strOutputFileName; +String strViewsFileName; +String strImageFolder; +float fDecimateMesh; +unsigned nCloseHoles; +unsigned nResolutionLevel; +unsigned nMinResolution; +unsigned minCommonCameras; +float fOutlierThreshold; +float fRatioDataSmoothness; +bool bGlobalSeamLeveling; +bool bLocalSeamLeveling; +bool bNormalizeIntrinsics; +bool bOriginFaceview; +unsigned nID; +unsigned nTextureSizeMultiple; +unsigned nRectPackingHeuristic; +uint32_t nColEmpty; +float fSharpnessWeight; +int nIgnoreMaskLabel; +unsigned nOrthoMapResolution; +unsigned nArchiveType; +int nProcessPriority; +unsigned nMaxThreads; +int nMaxTextureSize; +String strExportType; +String strConfigFileName; +boost::program_options::variables_map vm; +} // namespace OPT + +class Application { +public: + Application() {} + ~Application() { Finalize(); } + + bool Initialize(size_t argc, LPCTSTR* argv); + void Finalize(); +}; // Application + +// initialize and parse the command line parameters +bool Application::Initialize(size_t argc, LPCTSTR* argv) +{ + // initialize log and console + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "produce this help message") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("export-type", boost::program_options::value(&OPT::strExportType)->default_value(_T("obj")), "file type used to export the 3D scene (ply, obj, glb or gltf)") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: -1-interface, 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + #ifdef _USE_CUDA + ("cuda-device", boost::program_options::value(&CUDA::desiredDeviceID)->default_value(-1), "CUDA device number to be used to texture the mesh (-2 - CPU processing, -1 - best GPU, >=0 - device index)") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description config("Texture options"); + config.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input filename containing camera poses") + ("mesh-file,m", boost::program_options::value(&OPT::strMeshFileName), "mesh file name to texture (overwrite existing mesh)") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the mesh") + ("decimate", boost::program_options::value(&OPT::fDecimateMesh)->default_value(1.f), "decimation factor in range [0..1] to be applied to the input surface before refinement (0 - auto, 1 - disabled)") + ("close-holes", boost::program_options::value(&OPT::nCloseHoles)->default_value(30), "try to close small holes in the input surface (0 - disabled)") + ("resolution-level", boost::program_options::value(&OPT::nResolutionLevel)->default_value(0), "how many times to scale down the images before mesh refinement") + ("min-resolution", boost::program_options::value(&OPT::nMinResolution)->default_value(640), "do not scale images lower than this resolution") + ("outlier-threshold", boost::program_options::value(&OPT::fOutlierThreshold)->default_value(6e-2f), "threshold used to find and remove outlier face textures (0 - disabled)") + ("cost-smoothness-ratio", boost::program_options::value(&OPT::fRatioDataSmoothness)->default_value(0.1f), "ratio used to adjust the preference for more compact patches (1 - best quality/worst compactness, ~0 - worst quality/best compactness)") + ("virtual-face-images", boost::program_options::value(&OPT::minCommonCameras)->default_value(0), "generate texture patches using virtual faces composed of coplanar triangles sharing at least this number of views (0 - disabled, 3 - good value)") + ("global-seam-leveling", boost::program_options::value(&OPT::bGlobalSeamLeveling)->default_value(true), "generate uniform texture patches using global seam leveling") + ("local-seam-leveling", boost::program_options::value(&OPT::bLocalSeamLeveling)->default_value(true), "generate uniform texture patch borders using local seam leveling") + ("texture-size-multiple", boost::program_options::value(&OPT::nTextureSizeMultiple)->default_value(0), "texture size should be a multiple of this value (0 - power of two)") + ("patch-packing-heuristic", boost::program_options::value(&OPT::nRectPackingHeuristic)->default_value(3), "specify the heuristic used when deciding where to place a new patch (0 - best fit, 3 - good speed, 100 - best speed)") + ("empty-color", boost::program_options::value(&OPT::nColEmpty)->default_value(0x00FF7F27), "color used for faces not covered by any image") + ("sharpness-weight", boost::program_options::value(&OPT::fSharpnessWeight)->default_value(0.9f), "amount of sharpness to be applied on the texture (0 - disabled)") + ("orthographic-image-resolution", boost::program_options::value(&OPT::nOrthoMapResolution)->default_value(0), "orthographic image resolution to be generated from the textured mesh - the mesh is expected to be already geo-referenced or at least properly oriented (0 - disabled)") + ("ignore-mask-label", boost::program_options::value(&OPT::nIgnoreMaskLabel)->default_value(-1), "label value to ignore in the image mask, stored in the MVS scene or next to each image with '.mask.png' extension (-1 - auto estimate mask for lens distortion, -2 - disabled)") + ("max-texture-size", boost::program_options::value(&OPT::nMaxTextureSize)->default_value(8192), "maximum texture size, split it in multiple textures of this size if needed (0 - unbounded)") + ("normalize,f", boost::program_options::value(&OPT::bNormalizeIntrinsics)->default_value(false), "normalize intrinsics while exporting to MVS format") + ("image-folder", boost::program_options::value(&OPT::strImageFolder)->default_value(COLMAP_IMAGES_FOLDER), "folder to the undistorted images") + ("origin-faceview", boost::program_options::value(&OPT::bOriginFaceview)->default_value(false), "use origin faceview selection") + ("id", boost::program_options::value(&OPT::nID)->default_value(0), "id") + ; + + // hidden options, allowed both on command line and + // in config file, but will not be shown to the user + boost::program_options::options_description hidden("Hidden options"); + hidden.add_options() + ("views-file", boost::program_options::value(&OPT::strViewsFileName), "file name containing the list of views to be used for texturing (optional)") + ; + + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(config).add(hidden); + + boost::program_options::options_description config_file_options; + config_file_options.add(config).add(hidden); + + boost::program_options::positional_options_description p; + p.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(p).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + INIT_WORKING_FOLDER; + // parse configuration file + std::ifstream ifs(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (ifs) { + boost::program_options::store(parse_config_file(ifs, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // initialize the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-")+Util::getUniqueName(0)+_T(".log")).c_str()); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // validate input + Util::ensureValidPath(OPT::strInputFileName); + if (OPT::vm.count("help") || OPT::strInputFileName.empty()) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(config); + GET_LOG() << visible; + } + if (OPT::strInputFileName.empty()) + return false; + OPT::strExportType = OPT::strExportType.ToLower(); + if (OPT::strExportType == _T("obj")) + OPT::strExportType = _T(".obj"); + else + if (OPT::strExportType == _T("glb")) + OPT::strExportType = _T(".glb"); + else + if (OPT::strExportType == _T("gltf")) + OPT::strExportType = _T(".gltf"); + else + OPT::strExportType = _T(".ply"); + + // initialize optional options + Util::ensureValidPath(OPT::strMeshFileName); + Util::ensureValidPath(OPT::strOutputFileName); + Util::ensureValidPath(OPT::strViewsFileName); + if (OPT::strMeshFileName.empty() && (ARCHIVE_TYPE)OPT::nArchiveType == ARCHIVE_MVS) + OPT::strMeshFileName = Util::getFileFullName(OPT::strInputFileName) + _T(".ply"); + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = Util::getFileFullName(OPT::strInputFileName) + _T("_texture.mvs"); + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::Finalize() +{ + MVS::Finalize(); + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +} // unnamed namespace + +IIndexArr ParseViewsFile(const String& filename, const Scene& scene) { + IIndexArr views; + std::ifstream file(filename); + printf("filename: %s\n", filename.c_str()); + if (!file.good()) { + VERBOSE("error: unable to open views file '%s'", filename.c_str()); + return views; + } + while (true) { + String imageName; + std::getline(file, imageName); + if (file.fail() || imageName.empty()) + break; + LPTSTR endIdx; + const IDX idx(strtoul(imageName, &endIdx, 10)); + const size_t szIndex(*endIdx == '\0' ? size_t(0) : imageName.size()); + FOREACH(idxImage, scene.images) { + const Image& image = scene.images[idxImage]; + if (szIndex == 0) { + // try to match by index + if (image.ID != idx) + continue; + } else { + // try to match by file name + const String name(Util::getFileNameExt(image.name)); + if (name.size() < szIndex || _tcsnicmp(name, imageName, szIndex) != 0) + continue; + } + views.emplace_back(idxImage); + break; + } + } + return views; +} + +bool DetermineInputSource(const String& filenameTXT, const String& filenameBIN, std::ifstream& fileStream, String& filenameCamera, bool& isBinaryFormat) +{ + // Try to open the TXT file first. + fileStream.open(filenameTXT); + if (fileStream.good()) { + filenameCamera = filenameTXT; + isBinaryFormat = false; + return true; + } + fileStream.open(filenameBIN, std::ios::binary); + if (fileStream.good()) { + filenameCamera = filenameBIN; + isBinaryFormat = true; + return true; + } + VERBOSE("error: unable to open file '%s'", filenameTXT.c_str()); + VERBOSE("error: unable to open file '%s'", filenameBIN.c_str()); + return false; +} + +namespace COLMAP { + +using namespace colmap; + +// See colmap/src/util/types.h +typedef uint32_t camera_t; +typedef uint32_t image_t; +typedef uint64_t image_pair_t; +typedef uint32_t point2D_t; +typedef uint64_t point3D_t; + +const std::vector mapCameraModel = { + "SIMPLE_PINHOLE", + "PINHOLE", + "SIMPLE_RADIAL", + "RADIAL", + "OPENCV", + "OPENCV_FISHEYE", + "FULL_OPENCV", + "FOV", + "SIMPLE_RADIAL_FISHEYE", + "RADIAL_FISHEYE", + "THIN_PRISM_FISHEYE" +}; + +// tools +bool NextLine(std::istream& stream, std::istringstream& in, bool bIgnoreEmpty=true) { + String line; + do { + std::getline(stream, line); + Util::strTrim(line, _T(" ")); + if (stream.fail()) + return false; + } while (((bIgnoreEmpty && line.empty()) || line[0u] == '#') && stream.good()); + in.clear(); + in.str(line); + return true; +} +// structure describing a camera +struct Camera { + uint32_t ID; // ID of the camera + String model; // camera model name + uint32_t width, height; // camera resolution + std::vector params; // camera parameters + uint64_t numCameras{0}; // only for binary format + + Camera() {} + Camera(uint32_t _ID) : ID(_ID) {} + bool operator < (const Camera& rhs) const { return ID < rhs.ID; } + + struct CameraHash { + size_t operator()(const Camera& camera) const { + const size_t h1(std::hash()(camera.model)); + const size_t h2(std::hash()(camera.width)); + const size_t h3(std::hash()(camera.height)); + size_t h(h1 ^ ((h2 ^ (h3 << 1)) << 1)); + for (REAL p: camera.params) + h = std::hash()(p) ^ (h << 1); + return h; + } + }; + struct CameraEqualTo { + bool operator()(const Camera& _Left, const Camera& _Right) const { + return _Left.model == _Right.model && + _Left.width == _Right.width && _Left.height == _Right.height && + _Left.params == _Right.params; + } + }; + + bool Read(std::istream& stream, bool binary) { + if (binary) + return ReadBIN(stream); + return ReadTXT(stream); + } + + bool Write(std::ostream& stream, bool binary) { + if (binary) + return WriteBIN(stream); + return WriteTXT(stream); + } + + // Camera list with one line of data per camera: + // CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[] + bool ReadTXT(std::istream& stream) { + std::istringstream in; + if (!NextLine(stream, in)) + return false; + in >> ID >> model >> width >> height; + if (in.fail()) + return false; + --ID; + if (model != _T("PINHOLE")) + return false; + params.resize(4); + in >> params[0] >> params[1] >> params[2] >> params[3]; + return !in.fail(); + } + + // See: colmap/src/base/reconstruction.cc + // void Reconstruction::ReadCamerasBinary(const std::string& path) + bool ReadBIN(std::istream& stream) { + if (stream.peek() == EOF) + return false; + + if (numCameras == 0) { + // Read the first entry in the binary file + numCameras = ReadBinaryLittleEndian(&stream); + } + + ID = ReadBinaryLittleEndian(&stream)-1; + model = mapCameraModel[ReadBinaryLittleEndian(&stream)]; + width = (uint32_t)ReadBinaryLittleEndian(&stream); + height = (uint32_t)ReadBinaryLittleEndian(&stream); + if (model != _T("PINHOLE")) + return false; + params.resize(4); + ReadBinaryLittleEndian(&stream, ¶ms); + return true; + } + + bool WriteTXT(std::ostream& out) const { + out << ID+1 << _T(" ") << model << _T(" ") << width << _T(" ") << height; + if (out.fail()) + return false; + for (REAL param: params) { + out << _T(" ") << param; + if (out.fail()) + return false; + } + out << std::endl; + return true; + } + + bool WriteBIN(std::ostream& stream) { + if (numCameras != 0) { + // Write the first entry in the binary file + WriteBinaryLittleEndian(&stream, numCameras); + numCameras = 0; + } + + WriteBinaryLittleEndian(&stream, ID+1); + const int64 modelId(std::distance(mapCameraModel.begin(), std::find(mapCameraModel.begin(), mapCameraModel.end(), model))); + WriteBinaryLittleEndian(&stream, (int)modelId); + WriteBinaryLittleEndian(&stream, width); + WriteBinaryLittleEndian(&stream, height); + for (REAL param: params) + WriteBinaryLittleEndian(&stream, param); + return !stream.fail(); + } +}; +typedef std::vector Cameras; +// structure describing an image +struct Image { + struct Proj { + Eigen::Vector2f p; + uint32_t idPoint; + }; + uint32_t ID; // ID of the image + Eigen::Quaterniond q; // rotation + Eigen::Vector3d t; // translation + uint32_t idCamera; // ID of the associated camera + String name; // image file name + std::vector projs; // known image projections + uint64_t numRegImages{0}; // only for binary format + + Image() {} + Image(uint32_t _ID) : ID(_ID) {} + bool operator < (const Image& rhs) const { return ID < rhs.ID; } + + bool Read(std::istream& stream, bool binary) { + if (binary) + return ReadBIN(stream); + return ReadTXT(stream); + } + + bool Write(std::ostream& stream, bool binary) { + if (binary) + return WriteBIN(stream); + return WriteTXT(stream); + } + + // Image list with two lines of data per image: + // IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME + // POINTS2D[] as (X, Y, POINT3D_ID) + bool ReadTXT(std::istream& stream) { + std::istringstream in; + if (!NextLine(stream, in)) + return false; + in >> ID + >> q.w() >> q.x() >> q.y() >> q.z() + >> t(0) >> t(1) >> t(2) + >> idCamera >> name; + if (in.fail()) + return false; + --ID; --idCamera; + Util::ensureValidPath(name); + if (!NextLine(stream, in, false)) + return false; + projs.clear(); + while (true) { + Proj proj; + in >> proj.p(0) >> proj.p(1) >> (int&)proj.idPoint; + if (in.fail()) + break; + --proj.idPoint; + projs.emplace_back(proj); + } + return true; + } + + // See: colmap/src/base/reconstruction.cc + // void Reconstruction::ReadImagesBinary(const std::string& path) + bool ReadBIN(std::istream& stream) { + if (stream.peek() == EOF) + return false; + + if (!numRegImages) { + // Read the first entry in the binary file + numRegImages = ReadBinaryLittleEndian(&stream); + } + + ID = ReadBinaryLittleEndian(&stream)-1; + q.w() = ReadBinaryLittleEndian(&stream); + q.x() = ReadBinaryLittleEndian(&stream); + q.y() = ReadBinaryLittleEndian(&stream); + q.z() = ReadBinaryLittleEndian(&stream); + t(0) = ReadBinaryLittleEndian(&stream); + t(1) = ReadBinaryLittleEndian(&stream); + t(2) = ReadBinaryLittleEndian(&stream); + idCamera = ReadBinaryLittleEndian(&stream)-1; + + name = ""; + while (true) { + char nameChar; + stream.read(&nameChar, 1); + if (nameChar == '\0') + break; + name += nameChar; + } + Util::ensureValidPath(name); + + const size_t numPoints2D = ReadBinaryLittleEndian(&stream); + projs.clear(); + for (size_t j = 0; j < numPoints2D; ++j) { + Proj proj; + proj.p(0) = (float)ReadBinaryLittleEndian(&stream); + proj.p(1) = (float)ReadBinaryLittleEndian(&stream); + proj.idPoint = (uint32_t)ReadBinaryLittleEndian(&stream)-1; + projs.emplace_back(proj); + } + return true; + } + + bool WriteTXT(std::ostream& out) const { + out << ID+1 << _T(" ") + << q.w() << _T(" ") << q.x() << _T(" ") << q.y() << _T(" ") << q.z() << _T(" ") + << t(0) << _T(" ") << t(1) << _T(" ") << t(2) << _T(" ") + << idCamera+1 << _T(" ") << name + << std::endl; + for (const Proj& proj: projs) { + out << proj.p(0) << _T(" ") << proj.p(1) << _T(" ") << (int)proj.idPoint+1 << _T(" "); + if (out.fail()) + return false; + } + out << std::endl; + return !out.fail(); + } + + bool WriteBIN(std::ostream& stream) { + if (numRegImages != 0) { + // Write the first entry in the binary file + WriteBinaryLittleEndian(&stream, numRegImages); + numRegImages = 0; + } + + WriteBinaryLittleEndian(&stream, ID+1); + + WriteBinaryLittleEndian(&stream, q.w()); + WriteBinaryLittleEndian(&stream, q.x()); + WriteBinaryLittleEndian(&stream, q.y()); + WriteBinaryLittleEndian(&stream, q.z()); + + WriteBinaryLittleEndian(&stream, t(0)); + WriteBinaryLittleEndian(&stream, t(1)); + WriteBinaryLittleEndian(&stream, t(2)); + + WriteBinaryLittleEndian(&stream, idCamera+1); + + stream.write(name.c_str(), name.size()+1); + + WriteBinaryLittleEndian(&stream, projs.size()); + for (const Proj& proj: projs) { + WriteBinaryLittleEndian(&stream, proj.p(0)); + WriteBinaryLittleEndian(&stream, proj.p(1)); + WriteBinaryLittleEndian(&stream, proj.idPoint+1); + } + return !stream.fail(); + } +}; +typedef std::vector Images; +// structure describing a 3D point +struct Point { + struct Track { + uint32_t idImage; + uint32_t idProj; + }; + uint32_t ID; // ID of the point + Interface::Pos3f p; // position + Interface::Col3 c; // BGR color + float e; // error + std::vector tracks; // point track + uint64_t numPoints3D{0}; // only for binary format + + Point() {} + Point(uint32_t _ID) : ID(_ID) {} + bool operator < (const Image& rhs) const { return ID < rhs.ID; } + + bool Read(std::istream& stream, bool binary) { + if (binary) + return ReadBIN(stream); + return ReadTXT(stream); + } + + bool Write(std::ostream& stream, bool binary) { + if (binary) + return WriteBIN(stream); + return WriteTXT(stream); + } + + // 3D point list with one line of data per point: + // POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX) + bool ReadTXT(std::istream& stream) { + std::istringstream in; + if (!NextLine(stream, in)) + return false; + int r,g,b; + in >> ID + >> p.x >> p.y >> p.z + >> r >> g >> b + >> e; + c.x = CLAMP(b,0,255); + c.y = CLAMP(g,0,255); + c.z = CLAMP(r,0,255); + if (in.fail()) + return false; + --ID; + tracks.clear(); + while (true) { + Track track; + in >> track.idImage >> track.idProj; + if (in.fail()) + break; + --track.idImage; --track.idProj; + tracks.emplace_back(track); + } + return !tracks.empty(); + } + + // See: colmap/src/base/reconstruction.cc + // void Reconstruction::ReadPoints3DBinary(const std::string& path) + bool ReadBIN(std::istream& stream) { + if (stream.peek() == EOF) + return false; + + if (!numPoints3D) { + // Read the first entry in the binary file + numPoints3D = ReadBinaryLittleEndian(&stream); + } + + int r,g,b; + ID = (uint32_t)ReadBinaryLittleEndian(&stream)-1; + p.x = (float)ReadBinaryLittleEndian(&stream); + p.y = (float)ReadBinaryLittleEndian(&stream); + p.z = (float)ReadBinaryLittleEndian(&stream); + r = ReadBinaryLittleEndian(&stream); + g = ReadBinaryLittleEndian(&stream); + b = ReadBinaryLittleEndian(&stream); + e = (float)ReadBinaryLittleEndian(&stream); + c.x = CLAMP(b,0,255); + c.y = CLAMP(g,0,255); + c.z = CLAMP(r,0,255); + + const size_t trackLength = ReadBinaryLittleEndian(&stream); + tracks.clear(); + for (size_t j = 0; j < trackLength; ++j) { + Track track; + track.idImage = ReadBinaryLittleEndian(&stream)-1; + track.idProj = ReadBinaryLittleEndian(&stream)-1; + tracks.emplace_back(track); + } + return !tracks.empty(); + } + + bool WriteTXT(std::ostream& out) const { + ASSERT(!tracks.empty()); + const int r(c.z),g(c.y),b(c.x); + out << ID+1 << _T(" ") + << p.x << _T(" ") << p.y << _T(" ") << p.z << _T(" ") + << r << _T(" ") << g << _T(" ") << b << _T(" ") + << e << _T(" "); + for (const Track& track: tracks) { + out << track.idImage+1 << _T(" ") << track.idProj+1 << _T(" "); + if (out.fail()) + return false; + } + out << std::endl; + return !out.fail(); + } + + bool WriteBIN(std::ostream& stream) { + ASSERT(!tracks.empty()); + if (numPoints3D != 0) { + // Write the first entry in the binary file + WriteBinaryLittleEndian(&stream, numPoints3D); + numPoints3D = 0; + } + + WriteBinaryLittleEndian(&stream, ID+1); + WriteBinaryLittleEndian(&stream, p.x); + WriteBinaryLittleEndian(&stream, p.y); + WriteBinaryLittleEndian(&stream, p.z); + WriteBinaryLittleEndian(&stream, c.z); + WriteBinaryLittleEndian(&stream, c.y); + WriteBinaryLittleEndian(&stream, c.x); + WriteBinaryLittleEndian(&stream, e); + + WriteBinaryLittleEndian(&stream, tracks.size()); + for (const Track& track: tracks) { + WriteBinaryLittleEndian(&stream, track.idImage+1); + WriteBinaryLittleEndian(&stream, track.idProj+1); + } + return !stream.fail(); + } +}; +typedef std::vector Points; +// structure describing an 2D dynamic matrix +template +struct Mat { + size_t width_ = 0; + size_t height_ = 0; + size_t depth_ = 0; + std::vector data_; + + size_t GetNumBytes() const { + return data_.size() * sizeof(T); + } + const T* GetChannelPtr(size_t c) const { + return data_.data()+width_*height_*c; + } + + // See: colmap/src/mvs/mat.h + void Read(const std::string& path) { + std::streampos pos; { + std::fstream text_file(path, std::ios::in | std::ios::binary); + char unused_char; + text_file >> width_ >> unused_char >> height_ >> unused_char >> depth_ >> + unused_char; + pos = text_file.tellg(); + } + data_.resize(width_ * height_ * depth_); + std::fstream binary_file(path, std::ios::in | std::ios::binary); + binary_file.seekg(pos); + ReadBinaryLittleEndian(&binary_file, &data_); + } + void Write(const std::string& path) const { + { + std::fstream text_file(path, std::ios::out); + text_file << width_ << "&" << height_ << "&" << depth_ << "&"; + } + std::fstream binary_file(path, std::ios::out | std::ios::binary | std::ios::app); + WriteBinaryLittleEndian(&binary_file, data_); + } +}; +} // namespace COLMAP + +typedef Eigen::Matrix EMat33d; +typedef Eigen::Matrix EVec3d; + +bool ImportScene(const String& inputFolder, const String& outputFolder, Interface& scene, PointCloud& pointcloud) +{ + // Define a map to store camera IDs and associated platform index. + typedef std::unordered_map CamerasMap; + CamerasMap mapCameras; + { + // Determine the camera file source between TXT and BIN formats. + const String filenameCamerasTXT(inputFolder+COLMAP_CAMERAS_TXT); + const String filenameCamerasBIN(inputFolder+COLMAP_CAMERAS_BIN); + std::ifstream cameraFile; + bool isBinaryFormat; + String filenameCamera; + if (!DetermineInputSource(filenameCamerasTXT, filenameCamerasBIN, cameraFile, filenameCamera, isBinaryFormat)) { + return false; // Exit if file source determination fails. + } + LOG_OUT() << "Reading cameras: " << filenameCamera << std::endl; + + typedef std::unordered_map CamerasSet; + CamerasSet setCameras; + COLMAP::Camera colmapCamera; + while (cameraFile.good() && colmapCamera.Read(cameraFile, isBinaryFormat)) { + const auto setIt(setCameras.emplace(colmapCamera, (uint32_t)scene.platforms.size())); + mapCameras.emplace(colmapCamera.ID, setIt.first->second); + if (!setIt.second) { + // reuse existing platform + continue; + } + // create new platform + Interface::Platform platform; + platform.name = String::FormatString(_T("platform%03u"), colmapCamera.ID); // only one camera per platform supported + Interface::Platform::Camera camera; + camera.name = colmapCamera.model; + camera.K = Interface::Mat33d::eye(); + // account for different pixel center conventions as COLMAP uses pixel center at (0.5,0.5) + camera.K(0,0) = colmapCamera.params[0]; + camera.K(1,1) = colmapCamera.params[1]; + camera.K(0,2) = colmapCamera.params[2]-REAL(0.5); + camera.K(1,2) = colmapCamera.params[3]-REAL(0.5); + camera.R = Interface::Mat33d::eye(); + camera.C = Interface::Pos3d(0,0,0); + if (OPT::bNormalizeIntrinsics) { + // normalize camera intrinsics + camera.K = Camera::ScaleK(camera.K, 1.0/Camera::GetNormalizationScale(colmapCamera.width, colmapCamera.height)); + } else { + camera.width = colmapCamera.width; + camera.height = colmapCamera.height; + } + platform.cameras.emplace_back(camera); + scene.platforms.emplace_back(platform); + } + } + if (mapCameras.empty()) { + VERBOSE("error: no valid cameras (make sure they are in PINHOLE model)"); + return false; + } + + // Function to read and process image data from input files + typedef std::map ImagesMap; + ImagesMap mapImages; + { + // Define a map to associate COLMAP image structures with scene image indices + const String filenameImagesTXT(inputFolder+COLMAP_IMAGES_TXT); + const String filenameImagesBIN(inputFolder+COLMAP_IMAGES_BIN); + std::ifstream file; + bool binary; + String filenameImages; + if (!DetermineInputSource(filenameImagesTXT, filenameImagesBIN, file, filenameImages, binary)) { + return false; + } + LOG_OUT() << "Reading images: " << filenameImages << std::endl; + + // Read image data from the file and store in the scene structure + COLMAP::Image imageColmap; + while (file.good() && imageColmap.Read(file, binary)) { + mapImages.emplace(imageColmap, (uint32_t)scene.images.size()); + Interface::Platform::Pose pose; + Eigen::Map(pose.R.val) = imageColmap.q.toRotationMatrix(); + EnsureRotationMatrix((Matrix3x3d&)pose.R); + Eigen::Map(&pose.C.x) = -(imageColmap.q.inverse() * imageColmap.t); + + Interface::Image image; + // Start Generation Here + if (!OPT::strImageFolder.empty() && OPT::strImageFolder.back() != '/') + OPT::strImageFolder += '/'; + image.name = MAKE_PATH_REL(outputFolder,OPT::strImageFolder+imageColmap.name); + image.platformID = mapCameras.at(imageColmap.idCamera); + image.cameraID = 0; + image.ID = imageColmap.ID; + + Interface::Platform& platform = scene.platforms[image.platformID]; + image.poseID = (uint32_t)platform.poses.size(); + platform.poses.emplace_back(pose); + scene.images.emplace_back(image); + } + } + + // read points list + const String filenameDensePoints(inputFolder+COLMAP_DENSE_POINTS); + const String filenameDenseVisPoints(inputFolder+COLMAP_DENSE_POINTS_VISIBILITY); + printf("Don't read any cloud points\n"); + // { + // // parse sparse point-cloud + // const String filenamePointsTXT(inputFolder+COLMAP_POINTS_TXT); + // const String filenamePointsBIN(inputFolder+COLMAP_POINTS_BIN); + // std::ifstream file; + // bool binary; + // String filenamePoints; + // if (!DetermineInputSource(filenamePointsTXT, filenamePointsBIN, file, filenamePoints, binary)) { + // return false; + // } + // LOG_OUT() << "Reading points: " << filenamePoints << std::endl; + + // // Process each point in the file + // COLMAP::Point point; + // while (file.good() && point.Read(file, binary)) { + // Interface::Vertex vertex; + // vertex.X = point.p; // Assign 3D coordinates to vertex + + // // Process each track associated with the point + // for (const COLMAP::Point::Track& track: point.tracks) { + // Interface::Vertex::View view; + // view.imageID = mapImages.at(COLMAP::Image(track.0)); // Map COLMAP image ID to internal image ID + // view.confidence = 0; + // vertex.views.emplace_back(view); + // } + + // // Ensure views are sorted by image ID to maintain consistency + // std::sort(vertex.views.begin(), vertex.views.end(), + // [](const Interface::Vertex::View& view0, const Interface::Vertex::View& view1) { return view0.imageID < view1.imageID; }); + // scene.vertices.emplace_back(std::move(vertex)); + // scene.verticesColor.emplace_back(Interface::Color{point.c}); + // } + // } + pointcloud.Release(); + if (File::access(filenameDensePoints) && File::access(filenameDenseVisPoints)) { + // parse dense point-cloud + LOG_OUT() << "Reading points: " << filenameDensePoints << " and " << filenameDenseVisPoints << std::endl; + if (!pointcloud.Load(filenameDensePoints)) { + VERBOSE("error: unable to open file '%s'", filenameDensePoints.c_str()); + return false; + } + File file(filenameDenseVisPoints, File::READ, File::OPEN); + if (!file.isOpen()) { + VERBOSE("error: unable to open file '%s'", filenameDenseVisPoints.c_str()); + return false; + } + uint64_t numPoints(0); + file.read(&numPoints, sizeof(uint64_t)); + if (pointcloud.GetSize() != numPoints) { + VERBOSE("error: point-cloud and visibility have different size"); + return false; + } + pointcloud.pointViews.resize(numPoints); + for (size_t i=0; isecond]; + CLISTDEF2(String) neighborNames; + Util::strSplit(neighbors, _T(','), neighborNames); + FOREACH(i, neighborNames) { + String& neighborName = neighborNames[i]; + Util::strTrim(neighborName, _T(" ")); + const ImagesMap::const_iterator it_neighbor = std::find_if(mapImages.begin(), mapImages.end(), + [&neighborName](const ImagesMap::value_type& image) { + return image.first.name == neighborName; + }); + if (it_neighbor == mapImages.end()) { + if (i == 0) + break; + continue; + } + imageNeighbors.emplace_back(scene.images[it_neighbor->second].ID); + } + } + } + LOG_OUT() << "Reading depth-maps/normal-maps: " << pathDepthMaps << " and " << pathNormalMaps << std::endl; + Util::ensureFolder(outputFolder); + const String strType[] = {".geometric.bin", ".photometric.bin"}; + FOREACH(idx, scene.images) { + const Interface::Image& image = scene.images[idx]; + COLMAP::Mat colDepthMap, colNormalMap; + const String filenameImage(Util::getFileNameExt(image.name)); + for (int i=0; i<2; ++i) { + const String filenameDepthMaps(pathDepthMaps+filenameImage+strType[i]); + if (File::isFile(filenameDepthMaps)) { + colDepthMap.Read(filenameDepthMaps); + const String filenameNormalMaps(pathNormalMaps+filenameImage+strType[i]); + if (File::isFile(filenameNormalMaps)) { + colNormalMap.Read(filenameNormalMaps); + } + break; + } + } + if (!colDepthMap.data_.empty()) { + IIndexArr IDs = {image.ID}; + IDs.Join(imagesNeighbors[(IIndex)idx]); + const Interface::Platform& platform = scene.platforms[image.platformID]; + const Interface::Platform::Pose pose(platform.GetPose(image.cameraID, image.poseID)); + const Interface::Mat33d K(platform.GetFullK(image.cameraID, (uint32_t)colDepthMap.width_, (uint32_t)colDepthMap.height_)); + MVS::DepthMap depthMap((int)colDepthMap.height_, (int)colDepthMap.width_); + memcpy(depthMap.getData(), colDepthMap.data_.data(), colDepthMap.GetNumBytes()); + MVS::NormalMap normalMap; + if (!colNormalMap.data_.empty()) { + normalMap.create((int)colNormalMap.height_, (int)colNormalMap.width_); + cv::merge(std::vector{ + cv::Mat((int)colNormalMap.height_, (int)colNormalMap.width_, CV_32F, (void*)colNormalMap.GetChannelPtr(0)), + cv::Mat((int)colNormalMap.height_, (int)colNormalMap.width_, CV_32F, (void*)colNormalMap.GetChannelPtr(1)), + cv::Mat((int)colNormalMap.height_, (int)colNormalMap.width_, CV_32F, (void*)colNormalMap.GetChannelPtr(2)) + }, normalMap); + } + MVS::ConfidenceMap confMap; + MVS::ViewsMap viewsMap; + const auto depthMM(std::minmax_element(colDepthMap.data_.cbegin(), colDepthMap.data_.cend())); + const MVS::Depth dMin(*depthMM.first), dMax(*depthMM.second); + if (!ExportDepthDataRaw(outputFolder+String::FormatString("depth%04u.dmap", image.ID), MAKE_PATH_FULL(outputFolder, image.name), IDs, depthMap.size(), K, pose.R, pose.C, dMin, dMax, depthMap, normalMap, confMap, viewsMap)) + return false; + } + } + } + return true; +} + +bool checkFileExistence(const std::string& filename) { + std::ifstream file(filename); + return file.good(); +} + +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application application; + if (!application.Initialize(argc, argv)) + return EXIT_FAILURE; + + if (1) { + if (checkFileExistence(MAKE_PATH_SAFE(OPT::strOutputFileName).c_str())) { + std::cout << "The file exists." << std::endl; + } else { + std::cout << "The file does not exist." << std::endl; + } + + // read COLMAP input data + Interface scene; + const String strOutFolder(Util::getFilePath(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strOutputFileName))); + printf("Util::getFilePath(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strInputFileName)): %s\n", Util::getFilePath(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strInputFileName)).c_str()); + PointCloud pointcloud; + if (!ImportScene(MAKE_PATH_SAFE(OPT::strInputFileName), strOutFolder, scene, pointcloud)) + return EXIT_FAILURE; + printf("###########\n"); + // write MVS input data + Util::ensureFolder(strOutFolder); + if (!ARCHIVE::SerializeSave(scene, MAKE_PATH_SAFE(OPT::strOutputFileName))) + return EXIT_FAILURE; + printf("MAKE_PATH_SAFE(OPT::strOutputFileName): %s\n", MAKE_PATH_SAFE(OPT::strOutputFileName).c_str()); + + + if (checkFileExistence(MAKE_PATH_SAFE(OPT::strOutputFileName).c_str())) { + std::cout << "The file exists." << std::endl; + } else { + std::cout << "The file does not exist." << std::endl; + } + } + + Scene scene(OPT::nMaxThreads); + printf("OPT::strViewsFileName: %s\n", OPT::strViewsFileName.c_str()); + printf("MAKE_PATH_SAFE(OPT::strViewsFileName): %s\n", MAKE_PATH_SAFE(OPT::strViewsFileName).c_str()); + // load and texture the mesh + printf("MAKE_PATH_SAFE(OPT::strOutputFileName): %s\n", MAKE_PATH_SAFE(OPT::strOutputFileName).c_str()); + const Scene::SCENE_TYPE sceneType(scene.Load(MAKE_PATH_SAFE(OPT::strOutputFileName))); + if (sceneType == Scene::SCENE_NA) + return EXIT_FAILURE; + printf("Now\n"); + VERBOSE(MAKE_PATH_SAFE(OPT::strMeshFileName)); + if (!OPT::strMeshFileName.empty() && !scene.mesh.Load(MAKE_PATH_SAFE(OPT::strMeshFileName))) { + VERBOSE("error: cannot load mesh file"); + return EXIT_FAILURE; + } + if (scene.mesh.IsEmpty()) { + VERBOSE("error: empty initial mesh"); + return EXIT_FAILURE; + } + const String baseFileName(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName))); + if (OPT::nOrthoMapResolution && !scene.mesh.HasTexture()) { + // the input mesh is already textured and an orthographic projection was requested + goto ProjectOrtho; + } + + { + // decimate to the desired resolution + if (OPT::fDecimateMesh < 1.f) { + ASSERT(OPT::fDecimateMesh > 0.f); + scene.mesh.Clean(OPT::fDecimateMesh, 0.f, false, OPT::nCloseHoles, 0u, 0.f, false); + scene.mesh.Clean(1.f, 0.f, false, 0u, 0u, 0.f, true); // extra cleaning to remove non-manifold problems created by closing holes + #if TD_VERBOSE != TD_VERBOSE_OFF + if (VERBOSITY_LEVEL > 3) + scene.mesh.Save(baseFileName +_T("_decim")+OPT::strExportType); + #endif + } + // fetch list of views to be used for texturing + IIndexArr views; + // VERBOSE(MAKE_PATH_SAFE(OPT::strViewsFileName)); + printf("MAKE_PATH_SAFE(OPT::strViewsFileName): %s\n", MAKE_PATH_SAFE(OPT::strViewsFileName).c_str()); + if (!OPT::strViewsFileName.empty()) + views = ParseViewsFile(MAKE_PATH_SAFE(OPT::strViewsFileName), scene); + printf("Second\n"); + // compute mesh texture + TD_TIMER_START(); + if (!scene.TextureMesh(OPT::nResolutionLevel, OPT::nMinResolution, OPT::minCommonCameras, OPT::fOutlierThreshold, OPT::fRatioDataSmoothness, + OPT::bGlobalSeamLeveling, OPT::bLocalSeamLeveling, OPT::nTextureSizeMultiple, OPT::nRectPackingHeuristic, Pixel8U(OPT::nColEmpty), + OPT::fSharpnessWeight, OPT::nIgnoreMaskLabel, OPT::nMaxTextureSize, views, baseFileName, OPT::bOriginFaceview)) + return EXIT_FAILURE; + VERBOSE("Mesh texturing completed: %u vertices, %u faces (%s)", scene.mesh.vertices.GetSize(), scene.mesh.faces.GetSize(), TD_TIMER_GET_FMT().c_str()); + + // save the final mesh + scene.mesh.Save(baseFileName+OPT::strExportType); + #if TD_VERBOSE != TD_VERBOSE_OFF + if (VERBOSITY_LEVEL > 2) + scene.ExportCamerasMLP(baseFileName+_T(".mlp"), baseFileName+OPT::strExportType); + #endif + if ((ARCHIVE_TYPE)OPT::nArchiveType != ARCHIVE_MVS || sceneType != Scene::SCENE_INTERFACE) + scene.Save(baseFileName+_T(".mvs"), (ARCHIVE_TYPE)OPT::nArchiveType); + } + + if (OPT::nOrthoMapResolution) { + // project mesh as an orthographic image + ProjectOrtho: + Image8U3 imageRGB; + Image8U imageRGBA[4]; + Point3 center; + scene.mesh.ProjectOrthoTopDown(OPT::nOrthoMapResolution, imageRGB, imageRGBA[3], center); + Image8U4 image; + cv::split(imageRGB, imageRGBA); + cv::merge(imageRGBA, 4, image); + image.Save(baseFileName+_T("_orthomap.png")); + SML sml(_T("OrthoMap")); + sml[_T("Center")].val = String::FormatString(_T("%g %g %g"), center.x, center.y, center.z); + sml.Save(baseFileName+_T("_orthomap.txt")); + } + + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/TransformScene/CMakeLists.txt b/apps/TransformScene/CMakeLists.txt new file mode 100644 index 0000000..2ea8a7c --- /dev/null +++ b/apps/TransformScene/CMakeLists.txt @@ -0,0 +1,13 @@ +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(TransformScene "Apps" "${cxx_default}" "MVS;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Install +INSTALL(TARGETS TransformScene + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/TransformScene/TransformScene.cpp b/apps/TransformScene/TransformScene.cpp new file mode 100644 index 0000000..95f06f2 --- /dev/null +++ b/apps/TransformScene/TransformScene.cpp @@ -0,0 +1,329 @@ +/* + * TransformScene.cpp + * + * Copyright (c) 2014-2021 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" +#include + +using namespace MVS; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("TransformScene") +#define MVS_FILE_EXTENSION _T(".mvs") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { + String strInputFileName; + String strPointCloudFileName; + String strMeshFileName; + String strOutputFileName; + String strAlignFileName; + String strTransformFileName; + String strTransferTextureFileName; + String strIndicesFileName; + bool bComputeVolume; + float fPlaneThreshold; + float fSampleMesh; + unsigned nMaxResolution; + unsigned nUpAxis; + unsigned nArchiveType; + int nProcessPriority; + unsigned nMaxThreads; + String strExportType; + String strConfigFileName; + boost::program_options::variables_map vm; +} // namespace OPT + +class Application { +public: + Application() {} + ~Application() { Finalize(); } + + bool Initialize(size_t argc, LPCTSTR* argv); + void Finalize(); +}; // Application + +// initialize and parse the command line parameters +bool Application::Initialize(size_t argc, LPCTSTR* argv) +{ + // initialize log and console + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "produce this help message") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("export-type", boost::program_options::value(&OPT::strExportType)->default_value(_T("ply")), "file type used to export the 3D scene (ply, obj, glb or gltf)") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: -1-interface, 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(-1), "process priority (below normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads (0 for using all available cores)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description config("Main options"); + config.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input scene filename") + ("pointcloud-file,p", boost::program_options::value(&OPT::strPointCloudFileName), "dense point-cloud with views file name to transform (overwrite existing point-cloud)") + ("mesh-file,m", boost::program_options::value(&OPT::strMeshFileName), "mesh file name to transform (overwrite existing mesh)") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the scene") + ("align-file,a", boost::program_options::value(&OPT::strAlignFileName), "input scene filename to which the scene will be cameras aligned") + ("transform-file,t", boost::program_options::value(&OPT::strTransformFileName), "input transform filename by which the scene will transformed") + ("transfer-texture-file", boost::program_options::value(&OPT::strTransferTextureFileName), "input mesh filename to which the texture of the scene's mesh will be transfered to (the two meshes should be aligned and the new mesh to have UV-map)") + ("indices-file", boost::program_options::value(&OPT::strIndicesFileName), "input indices filename to be used with ex. texture transfer to select a subset of the scene's mesh") + ("compute-volume", boost::program_options::value(&OPT::bComputeVolume)->default_value(false), "compute the volume of the given watertight mesh, or else try to estimate the ground plane and assume the mesh is bounded by it") + ("plane-threshold", boost::program_options::value(&OPT::fPlaneThreshold)->default_value(0.f), "threshold used to estimate the ground plane (<0 - disabled, 0 - auto, >0 - desired threshold)") + ("sample-mesh", boost::program_options::value(&OPT::fSampleMesh)->default_value(-300000.f), "uniformly samples points on a mesh (0 - disabled, <0 - number of points, >0 - sample density per square unit)") + ("max-resolution", boost::program_options::value(&OPT::nMaxResolution)->default_value(0), "make sure image resolution are not not larger than this (0 - disabled)") + ("up-axis", boost::program_options::value(&OPT::nUpAxis)->default_value(2), "scene axis considered to point upwards (0 - x, 1 - y, 2 - z)") + ; + + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(config); + + boost::program_options::options_description config_file_options; + config_file_options.add(config); + + boost::program_options::positional_options_description p; + p.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(p).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + INIT_WORKING_FOLDER; + // parse configuration file + std::ifstream ifs(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (ifs) { + boost::program_options::store(parse_config_file(ifs, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + // initialize the log file + OPEN_LOGFILE(MAKE_PATH(APPNAME _T("-") + Util::getUniqueName(0) + _T(".log")).c_str()); + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // validate input + Util::ensureValidPath(OPT::strInputFileName); + Util::ensureValidPath(OPT::strAlignFileName); + Util::ensureValidPath(OPT::strTransformFileName); + Util::ensureValidPath(OPT::strTransferTextureFileName); + Util::ensureValidPath(OPT::strIndicesFileName); + const String strInputFileNameExt(Util::getFileExt(OPT::strInputFileName).ToLower()); + const bool bInvalidCommand(OPT::strInputFileName.empty() || + (OPT::strAlignFileName.empty() && OPT::strTransformFileName.empty() && OPT::strTransferTextureFileName.empty() && !OPT::bComputeVolume)); + if (OPT::vm.count("help") || bInvalidCommand) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(config); + GET_LOG() << visible; + } + if (bInvalidCommand) + return false; + OPT::strExportType = OPT::strExportType.ToLower(); + if (OPT::strExportType == _T("obj")) + OPT::strExportType = _T(".obj"); + else + if (OPT::strExportType == _T("glb")) + OPT::strExportType = _T(".glb"); + else + if (OPT::strExportType == _T("gltf")) + OPT::strExportType = _T(".gltf"); + else + OPT::strExportType = _T(".ply"); + + // initialize optional options + Util::ensureValidPath(OPT::strPointCloudFileName); + Util::ensureValidPath(OPT::strMeshFileName); + Util::ensureValidPath(OPT::strOutputFileName); + if (OPT::strMeshFileName.empty() && (ARCHIVE_TYPE)OPT::nArchiveType == ARCHIVE_MVS && strInputFileNameExt == MVS_FILE_EXTENSION) + OPT::strMeshFileName = Util::getFileFullName(OPT::strInputFileName) + _T(".ply"); + if (OPT::strOutputFileName.empty()) + OPT::strOutputFileName = Util::getFileName(OPT::strInputFileName) + _T("_transformed") MVS_FILE_EXTENSION; + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::Finalize() +{ + MVS::Finalize(); + + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +} // unnamed namespace + +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application application; + if (!application.Initialize(argc, argv)) + return EXIT_FAILURE; + + TD_TIMER_START(); + + Scene scene(OPT::nMaxThreads); + + // load given scene + const Scene::SCENE_TYPE sceneType(scene.Load(MAKE_PATH_SAFE(OPT::strInputFileName), + !OPT::strTransformFileName.empty() || !OPT::strTransferTextureFileName.empty() || OPT::bComputeVolume)); + if (sceneType == Scene::SCENE_NA) + return EXIT_FAILURE; + if (!OPT::strPointCloudFileName.empty() && !scene.pointcloud.Load(MAKE_PATH_SAFE(OPT::strPointCloudFileName))) { + VERBOSE("error: cannot load point-cloud file"); + return EXIT_FAILURE; + } + if (!OPT::strMeshFileName.empty() && !scene.mesh.Load(MAKE_PATH_SAFE(OPT::strMeshFileName))) { + VERBOSE("error: cannot load mesh file"); + return EXIT_FAILURE; + } + const String baseFileName(MAKE_PATH_SAFE(Util::getFileFullName(OPT::strOutputFileName))); + + if (!OPT::strAlignFileName.empty()) { + // transform this scene such that it best aligns with the given scene based on the camera positions + Scene sceneRef(OPT::nMaxThreads); + if (!sceneRef.Load(MAKE_PATH_SAFE(OPT::strAlignFileName))) + return EXIT_FAILURE; + if (!scene.AlignTo(sceneRef)) + return EXIT_FAILURE; + VERBOSE("Scene aligned to the given reference scene (%s)", TD_TIMER_GET_FMT().c_str()); + } + + if (!OPT::strTransformFileName.empty()) { + // transform this scene by the given transform matrix + std::ifstream file(MAKE_PATH_SAFE(OPT::strTransformFileName)); + std::string value; + std::vector transformValues; + while (file >> value) { + double v; + try { + v = std::stod(value); + } + catch (...) { + continue; + } + transformValues.push_back(v); + } + if (transformValues.size() != 12 && + (transformValues.size() != 16 || transformValues[12] != 0 || transformValues[13] != 0 || transformValues[14] != 0 || transformValues[15] != 1)) { + VERBOSE("error: invalid transform"); + return EXIT_FAILURE; + } + Matrix3x4 transform; + for (unsigned i=0; i<12; ++i) + transform[i] = transformValues[i]; + scene.Transform(transform); + VERBOSE("Scene transformed by the given transformation matrix (%s)", TD_TIMER_GET_FMT().c_str()); + } + + if (!OPT::strTransferTextureFileName.empty()) { + // transfer the texture of the scene's mesh to the new mesh; + // the two meshes should be aligned and the new mesh to have UV-coordinates + Mesh newMesh; + if (!newMesh.Load(MAKE_PATH_SAFE(OPT::strTransferTextureFileName))) + return EXIT_FAILURE; + Mesh::FaceIdxArr faceSubsetIndices; + if (!OPT::strIndicesFileName.empty()) { + std::ifstream in(OPT::strIndicesFileName.c_str()); + while (true) { + String index; + in >> index; + if (!in.good()) + break; + faceSubsetIndices.emplace_back(index.From()); + } + } + if (!scene.mesh.TransferTexture(newMesh, faceSubsetIndices)) + return EXIT_FAILURE; + newMesh.Save(baseFileName + OPT::strExportType); + VERBOSE("Texture transfered (%s)", TD_TIMER_GET_FMT().c_str()); + return EXIT_SUCCESS; + } + + if (OPT::nMaxResolution > 0) { + // scale scene images + const String folderName(Util::getFilePath(MAKE_PATH_FULL(WORKING_FOLDER_FULL, OPT::strInputFileName)) + String::FormatString("images%u" PATH_SEPARATOR_STR, OPT::nMaxResolution)); + if (!scene.ScaleImages(OPT::nMaxResolution, 0, folderName)) { + DEBUG("error: can not scale scene images to '%s'", folderName.c_str()); + return EXIT_FAILURE; + } + } + + if (OPT::bComputeVolume && !scene.mesh.IsEmpty()) { + // compute the mesh volume + const REAL volume(scene.ComputeLeveledVolume(OPT::fPlaneThreshold, OPT::fSampleMesh, OPT::nUpAxis)); + VERBOSE("Mesh volume: %g (%s)", volume, TD_TIMER_GET_FMT().c_str()); + } + + // write transformed scene + if (scene.IsValid()) + scene.Save(MAKE_PATH_SAFE(OPT::strOutputFileName), (ARCHIVE_TYPE)OPT::nArchiveType); + if (!scene.IsValid() || (ARCHIVE_TYPE)OPT::nArchiveType == ARCHIVE_MVS) { + if (!scene.pointcloud.IsEmpty()) + scene.pointcloud.Save(baseFileName + _T(".ply"), (ARCHIVE_TYPE)OPT::nArchiveType == ARCHIVE_MVS); + if (!scene.mesh.IsEmpty()) + scene.mesh.Save(baseFileName + OPT::strExportType); + } + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/Viewer/CMakeLists.txt b/apps/Viewer/CMakeLists.txt new file mode 100644 index 0000000..fe09202 --- /dev/null +++ b/apps/Viewer/CMakeLists.txt @@ -0,0 +1,47 @@ +if((NOT OpenMVS_USE_OPENGL) OR (NOT _USE_OPENGL)) + RETURN() +endif() + +if(NOT VIEWER_NAME) + set(VIEWER_NAME "Viewer") +endif() + +# Find required packages +FIND_PACKAGE(GLEW QUIET) +if(GLEW_FOUND) + INCLUDE_DIRECTORIES(${GLEW_INCLUDE_DIRS}) + ADD_DEFINITIONS(${GLEW_DEFINITIONS}) + MESSAGE(STATUS "GLEW ${GLEW_VERSION} found (include: ${GLEW_INCLUDE_DIRS})") +else() + MESSAGE("-- Can't find GLEW. Continuing without it.") + RETURN() +endif() +FIND_PACKAGE(glfw3 QUIET) +if(glfw3_FOUND) + INCLUDE_DIRECTORIES(${glfw3_INCLUDE_DIRS}) + ADD_DEFINITIONS(${glfw3_DEFINITIONS}) + MESSAGE(STATUS "GLFW3 ${glfw3_VERSION} found (include: ${glfw3_INCLUDE_DIRS})") +else() + MESSAGE("-- Can't find GLFW3. Continuing without it.") + RETURN() +endif() + +# List sources files +if(MSVC) + FILE(GLOB LIBRARY_FILES_C "*.cpp" "*.rc") +else() + FILE(GLOB LIBRARY_FILES_C "*.cpp") +endif() +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_executable_with_flags(${VIEWER_NAME} "Apps" "${cxx_default}" "MVS;${OPENGL_LIBRARIES};${GLEW_LIBRARY};${GLFW_STATIC_LIBRARIES};GLEW::GLEW;${glfw3_LIBRARY};${GLFW3_LIBRARY};glfw;${OpenMVS_EXTRA_LIBS}" ${LIBRARY_FILES_C} ${LIBRARY_FILES_H}) + +# Manually set Common.h as the precompiled header +IF(CMAKE_VERSION VERSION_GREATER_EQUAL 3.16.0) + TARGET_PRECOMPILE_HEADERS(${VIEWER_NAME} PRIVATE "Common.h") +endif() + +# Install +INSTALL(TARGETS ${VIEWER_NAME} + EXPORT OpenMVSTargets + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin) diff --git a/apps/Viewer/Camera.cpp b/apps/Viewer/Camera.cpp new file mode 100644 index 0000000..e6cb750 --- /dev/null +++ b/apps/Viewer/Camera.cpp @@ -0,0 +1,180 @@ +/* + * Camera.cpp + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "Common.h" +#include "Camera.h" + +using namespace VIEWER; + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +Camera::Camera(const AABB3d& _box, const Point3d& _center, float _scaleF, float _fov) + : + boxScene(_box), + centerScene(_center), + rotation(Eigen::Quaterniond::Identity()), + center(Eigen::Vector3d::Zero()), + dist(0), radius(100), + fovDef(_fov), scaleFDef(_scaleF), + prevCamID(NO_ID), currentCamID(NO_ID), maxCamID(0) +{ + Reset(); +} + +void Camera::Reset() +{ + if (boxScene.IsEmpty()) { + center = Point3d::ZERO; + radius = 1; + } else { + center = centerScene; + radius = boxScene.GetSize().norm()*0.5; + } + rotation = Eigen::Quaterniond::Identity(); + scaleF = scaleFDef; + prevCamID = currentCamID = NO_ID; + fov = fovDef; + dist = radius*0.5 / SIN(D2R((double)fov)); + if (size.area()) + Resize(size); +} + +void Camera::Resize(const cv::Size& _size) +{ + ASSERT(MINF(_size.width, _size.height) > 0); + size = _size; + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + const GLfloat zNear = 1e-3f; + const GLfloat zFar = (float)boxScene.GetSize().norm()*10; + const GLfloat aspect = float(size.width)/float(size.height); + if (fov == 5.f) { + // orthographic projection + const GLfloat fH = (float)boxScene.GetSize().norm()*0.5f; + const GLfloat fW = fH * aspect; + glOrtho(-fW, fW, -fH, fH, zNear, zFar); + } else { + // perspective projection + const GLfloat fH = TAN(FD2R(fov)) * zNear; + const GLfloat fW = fH * aspect; + glFrustum(-fW, fW, -fH, fH, zNear, zFar); + } +} + +void Camera::SetFOV(float _fov) +{ + fov = MAXF(_fov, 5.f); + Resize(size); +} + + +Eigen::Vector3d Camera::GetPosition() const +{ + const Eigen::Matrix3d R(GetRotation()); + return center + R.col(2) * dist; +} + +Eigen::Matrix3d Camera::GetRotation() const +{ + return rotation.toRotationMatrix(); +} + +Eigen::Matrix4d Camera::GetLookAt() const +{ + const Eigen::Matrix3d R(GetRotation()); + const Eigen::Vector3d eye(center + R.col(2) * dist); + const Eigen::Vector3d up(R.col(1)); + + const Eigen::Vector3d n((center-eye).normalized()); + const Eigen::Vector3d s(n.cross(up)); + const Eigen::Vector3d v(s.cross(n)); + + Eigen::Matrix4d m; m << + s(0), s(1), s(2), -eye.dot(s), + v(0), v(1), v(2), -eye.dot(v), + -n(0), -n(1), -n(2), eye.dot(n), + 0.0, 0.0, 0.0, 1.0; + return m; +} +void Camera::GetLookAt(Eigen::Vector3d& _eye, Eigen::Vector3d& _center, Eigen::Vector3d& _up) const +{ + const Eigen::Matrix3d R(GetRotation()); + _eye = center + R.col(2) * dist; + _center = center; + _up = R.col(1); +} + + +void Camera::Rotate(const Eigen::Vector2d& pos, const Eigen::Vector2d& prevPos) +{ + if (pos.isApprox(prevPos, ZEROTOLERANCE())) + return; + + Eigen::Vector3d oldp(prevPos.x(), prevPos.y(), 0); + Eigen::Vector3d newp(pos.x(), pos.y(), 0); + const double radiusSphere(0.9); + ProjectOnSphere(radiusSphere, oldp); + ProjectOnSphere(radiusSphere, newp); + rotation *= Eigen::Quaterniond().setFromTwoVectors(newp, oldp); + + // disable camera view mode + prevCamID = currentCamID; +} + +void Camera::Translate(const Eigen::Vector2d& pos, const Eigen::Vector2d& prevPos) +{ + if (pos.isApprox(prevPos, ZEROTOLERANCE())) + return; + + Eigen::Matrix P, V; + glGetDoublev(GL_MODELVIEW_MATRIX, V.data()); + glGetDoublev(GL_PROJECTION_MATRIX, P.data()); + Eigen::Vector3d centerScreen((P*V*center.homogeneous().eval()).hnormalized()); + centerScreen.head<2>() += prevPos - pos; + center = (V.inverse()*P.inverse()*centerScreen.homogeneous().eval()).hnormalized(); + + // disable camera view mode + prevCamID = currentCamID; +} + +void Camera::ProjectOnSphere(double radius, Eigen::Vector3d& p) const +{ + p.z() = 0; + const double d = p.x()* p.x()+ p.y() * p.y(); + const double r = radius * radius; + if (d < r) p.z() = SQRT(r - d); + else p *= radius / p.norm(); +} +/*----------------------------------------------------------------*/ diff --git a/apps/Viewer/Camera.h b/apps/Viewer/Camera.h new file mode 100644 index 0000000..d83880a --- /dev/null +++ b/apps/Viewer/Camera.h @@ -0,0 +1,87 @@ +/* + * Camera.h + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#ifndef _VIEWER_CAMERA_H_ +#define _VIEWER_CAMERA_H_ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace VIEWER { + +class Camera +{ +public: + EIGEN_MAKE_ALIGNED_OPERATOR_NEW + + cv::Size size; + AABB3d boxScene; + Eigen::Vector3d centerScene; + Eigen::Quaterniond rotation; + Eigen::Vector3d center; + double dist, radius; + float fov, fovDef; + float scaleF, scaleFDef; + MVS::IIndex prevCamID, currentCamID, maxCamID; + +public: + Camera(const AABB3d& _box=AABB3d(true), const Point3d& _center=Point3d::ZERO, float _scaleF=1, float _fov=40); + + void Reset(); + void Resize(const cv::Size&); + void SetFOV(float _fov); + + const cv::Size& GetSize() const { return size; } + + Eigen::Vector3d GetPosition() const; + Eigen::Matrix3d GetRotation() const; + Eigen::Matrix4d GetLookAt() const; + + void GetLookAt(Eigen::Vector3d& eye, Eigen::Vector3d& center, Eigen::Vector3d& up) const; + void Rotate(const Eigen::Vector2d& pos, const Eigen::Vector2d& prevPos); + void Translate(const Eigen::Vector2d& pos, const Eigen::Vector2d& prevPos); + + bool IsCameraViewMode() const { return prevCamID != currentCamID && currentCamID != NO_ID; } + +protected: + void ProjectOnSphere(double radius, Eigen::Vector3d& p) const; +}; +/*----------------------------------------------------------------*/ + +} // namespace VIEWER + +#endif // _VIEWER_CAMERA_H_ diff --git a/apps/Viewer/Common.cpp b/apps/Viewer/Common.cpp new file mode 100644 index 0000000..ec1dc0a --- /dev/null +++ b/apps/Viewer/Common.cpp @@ -0,0 +1,36 @@ +/* + * Common.cpp + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +// Source file that includes just the standard includes +// Common.pch will be the pre-compiled header +// Common.obj will contain the pre-compiled type information + +#include "Common.h" diff --git a/apps/Viewer/Common.h b/apps/Viewer/Common.h new file mode 100644 index 0000000..f338783 --- /dev/null +++ b/apps/Viewer/Common.h @@ -0,0 +1,104 @@ +/* + * Common.h + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#ifndef _VIEWER_COMMON_H_ +#define _VIEWER_COMMON_H_ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include +#include "../../libs/MVS/Common.h" +#include "../../libs/MVS/Scene.h" + +#if defined(_MSC_VER) +#include +#elif defined(__APPLE__) +#include +#else +#include +#endif +#include + + +// D E F I N E S /////////////////////////////////////////////////// + + +// P R O T O T Y P E S ///////////////////////////////////////////// + +using namespace SEACAVE; + +namespace VIEWER { + +// the conversion matrix from OpenGL default coordinate system +// to the camera coordinate system (NADIR orientation): +// [ 1 0 0 0] * [ x ] = [ x ] +// 0 -1 0 0 y -y +// 0 0 -1 0 z -z +// 0 0 0 1 1 1 +static const Eigen::Matrix4d gs_convert = [] { + Eigen::Matrix4d tmp; tmp << + 1, 0, 0, 0, + 0, -1, 0, 0, + 0, 0, -1, 0, + 0, 0, 0, 1; + return tmp; +}(); + +/// given rotation matrix R and translation vector t, +/// column-major matrix m is equal to: +/// [ R11 R12 R13 t.x ] +/// | R21 R22 R23 t.y | +/// | R31 R32 R33 t.z | +/// [ 0.0 0.0 0.0 1.0 ] +// +// World to Local +inline Eigen::Matrix4d TransW2L(const Eigen::Matrix3d& R, const Eigen::Vector3d& t) +{ + Eigen::Matrix4d m(Eigen::Matrix4d::Identity()); + m.block(0,0,3,3) = R; + m.block(0,3,3,1) = t; + return m; +} +// Local to World +// same as above, but with the inverse of the two +inline Eigen::Matrix4d TransL2W(const Eigen::Matrix3d& R, const Eigen::Vector3d& t) +{ + Eigen::Matrix4d m(Eigen::Matrix4d::Identity()); + m.block(0,0,3,3) = R.transpose(); + m.block(0,3,3,1) = -t; + return m; +} +/*----------------------------------------------------------------*/ + +} // namespace MVS + +#endif // _VIEWER_COMMON_H_ diff --git a/apps/Viewer/Image.cpp b/apps/Viewer/Image.cpp new file mode 100644 index 0000000..4268196 --- /dev/null +++ b/apps/Viewer/Image.cpp @@ -0,0 +1,124 @@ +/* + * Image.cpp + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "Common.h" +#include "Image.h" + +using namespace VIEWER; + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +Image::Image(MVS::IIndex _idx) + : + idx(_idx), + texture(0) +{ +} +Image::~Image() +{ + Release(); +} + +void Image::Release() +{ + if (IsValid()) { + glDeleteTextures(1, &texture); + texture = 0; + } + ReleaseImage(); +} +void Image::ReleaseImage() +{ + if (IsImageValid()) { + cv::Mat* const p(pImage); + Thread::safeExchange(pImage.ptr, (int_t)IMG_NULL); + delete p; + } +} + +void Image::SetImageLoading() +{ + ASSERT(IsImageEmpty()); + Thread::safeExchange(pImage.ptr, (int_t)IMG_LOADING); +} +void Image::AssignImage(cv::InputArray img) +{ + ASSERT(IsImageLoading()); + ImagePtrInt pImg(new cv::Mat(img.getMat())); + if (pImg.pImage->cols%4 != 0) { + // make sure the width is multiple of 4 (seems to be an OpenGL limitation) + cv::resize(*pImg.pImage, *pImg.pImage, cv::Size((pImg.pImage->cols/4)*4, pImg.pImage->rows), 0, 0, cv::INTER_AREA); + } + Thread::safeExchange(pImage.ptr, pImg.ptr); +} +bool Image::TransferImage() +{ + if (!IsImageValid()) + return false; + SetImage(*pImage); + glfwPostEmptyEvent(); + ReleaseImage(); + return true; +} + +void Image::SetImage(cv::InputArray img) +{ + cv::Mat image(img.getMat()); + glEnable(GL_TEXTURE_2D); + // create texture + glGenTextures(1, &texture); + // select our current texture + glBindTexture(GL_TEXTURE_2D, texture); + // load texture + width = image.cols; + height = image.rows; + ASSERT(image.channels() == 1 || image.channels() == 3); + ASSERT(image.isContinuous()); + glTexImage2D(GL_TEXTURE_2D, + 0, image.channels(), + width, height, + 0, (image.channels() == 1) ? GL_LUMINANCE : GL_BGR, + GL_UNSIGNED_BYTE, image.ptr()); + glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); +} +void Image::GenerateMipmap() const { + glBindTexture(GL_TEXTURE_2D, texture); + glGenerateMipmap(GL_TEXTURE_2D); +} +void Image::Bind() const { + glBindTexture(GL_TEXTURE_2D, texture); +} +/*----------------------------------------------------------------*/ diff --git a/apps/Viewer/Image.h b/apps/Viewer/Image.h new file mode 100644 index 0000000..41c1851 --- /dev/null +++ b/apps/Viewer/Image.h @@ -0,0 +1,97 @@ +/* + * Image.h + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#ifndef _VIEWER_IMAGE_H_ +#define _VIEWER_IMAGE_H_ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace VIEWER { + +class Image +{ +public: + typedef CLISTDEFIDX(Image,uint32_t) ImageArr; + enum { + IMG_NULL = 0, + IMG_LOADING, + IMG_VALID + }; + union ImagePtrInt { + cv::Mat* pImage; + int_t ptr; + inline ImagePtrInt() : ptr(IMG_NULL) {} + inline ImagePtrInt(cv::Mat* p) : pImage(p) {} + inline operator cv::Mat* () const { return pImage; } + inline operator cv::Mat*& () { return pImage; } + }; + +public: + MVS::IIndex idx; // image index in the current scene + int width, height; + GLuint texture; + double opacity; + ImagePtrInt pImage; + +public: + Image(MVS::IIndex = NO_ID); + ~Image(); + + void Release(); + void ReleaseImage(); + inline bool IsValid() const { return texture > 0; } + inline bool IsImageEmpty() const { return pImage.ptr == IMG_NULL; } + inline bool IsImageLoading() const { return pImage.ptr == IMG_LOADING; } + inline bool IsImageValid() const { return pImage.ptr >= IMG_VALID; } + + void SetImageLoading(); + void AssignImage(cv::InputArray); + bool TransferImage(); + + void SetImage(cv::InputArray); + void GenerateMipmap() const; + void Bind() const; + +protected: +}; +typedef Image::ImageArr ImageArr; +/*----------------------------------------------------------------*/ + +} // namespace VIEWER + +#endif // _VIEWER_IMAGE_H_ diff --git a/apps/Viewer/Scene.cpp b/apps/Viewer/Scene.cpp new file mode 100644 index 0000000..161c33e --- /dev/null +++ b/apps/Viewer/Scene.cpp @@ -0,0 +1,823 @@ +/* + * Scene.cpp + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "Common.h" +#include "Scene.h" + +using namespace VIEWER; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define IMAGE_MAX_RESOLUTION 1024 + + +// S T R U C T S /////////////////////////////////////////////////// + +enum EVENT_TYPE { + EVT_JOB = 0, + EVT_CLOSE, +}; + +class EVTClose : public Event +{ +public: + EVTClose() : Event(EVT_CLOSE) {} +}; +class EVTLoadImage : public Event +{ +public: + Scene* pScene; + MVS::IIndex idx; + unsigned nMaxResolution; + bool Run(void*) { + Image& image = pScene->images[idx]; + ASSERT(image.idx != NO_ID); + MVS::Image& imageData = pScene->scene.images[image.idx]; + ASSERT(imageData.IsValid()); + if (imageData.image.empty() && !imageData.ReloadImage(nMaxResolution)) + return false; + imageData.UpdateCamera(pScene->scene.platforms); + image.AssignImage(imageData.image); + imageData.ReleaseImage(); + glfwPostEmptyEvent(); + return true; + } + EVTLoadImage(Scene* _pScene, MVS::IIndex _idx, unsigned _nMaxResolution=0) + : Event(EVT_JOB), pScene(_pScene), idx(_idx), nMaxResolution(_nMaxResolution) {} +}; +class EVTComputeOctree : public Event +{ +public: + Scene* pScene; + bool Run(void*) { + MVS::Scene& scene = pScene->scene; + if (!scene.mesh.IsEmpty()) { + Scene::OctreeMesh octMesh(scene.mesh.vertices, [](Scene::OctreeMesh::IDX_TYPE size, Scene::OctreeMesh::Type /*radius*/) { + return size > 256; + }); + scene.mesh.ListIncidenteFaces(); + pScene->octMesh.Swap(octMesh); + } else + if (!scene.pointcloud.IsEmpty()) { + Scene::OctreePoints octPoints(scene.pointcloud.points, [](Scene::OctreePoints::IDX_TYPE size, Scene::OctreePoints::Type /*radius*/) { + return size > 512; + }); + pScene->octPoints.Swap(octPoints); + } + return true; + } + EVTComputeOctree(Scene* _pScene) + : Event(EVT_JOB), pScene(_pScene) {} +}; + +void* Scene::ThreadWorker(void*) { + while (true) { + CAutoPtr evt(events.GetEvent()); + switch (evt->GetID()) { + case EVT_JOB: + evt->Run(); + break; + case EVT_CLOSE: + return NULL; + default: + ASSERT("Should not happen!" == NULL); + } + } + return NULL; +} +/*----------------------------------------------------------------*/ + + +// S T R U C T S /////////////////////////////////////////////////// + +SEACAVE::EventQueue Scene::events; +SEACAVE::Thread Scene::thread; + +Scene::Scene(ARCHIVE_TYPE _nArchiveType) + : + nArchiveType(_nArchiveType), + listPointCloud(0) +{ +} +Scene::~Scene() +{ + Release(); +} + +void Scene::Empty() +{ + ReleasePointCloud(); + ReleaseMesh(); + obbPoints.Release(); + if (window.IsValid()) { + window.ReleaseClbk(); + window.Reset(); + window.SetName(_T("(empty)")); + } + textures.Release(); + images.Release(); + scene.Release(); + sceneName.clear(); + geometryName.clear(); +} +void Scene::Release() +{ + if (window.IsValid()) + window.SetVisible(false); + if (!thread.isRunning()) { + events.AddEvent(new EVTClose()); + thread.join(); + } + Empty(); + window.Release(); + glfwTerminate(); +} +void Scene::ReleasePointCloud() +{ + if (listPointCloud) { + glDeleteLists(listPointCloud, 1); + listPointCloud = 0; + } +} +void Scene::ReleaseMesh() +{ + if (!listMeshes.empty()) { + for (GLuint listMesh: listMeshes) + glDeleteLists(listMesh, 1); + listMeshes.Release(); + } +} + +bool Scene::Init(const cv::Size& size, LPCTSTR windowName, LPCTSTR fileName, LPCTSTR geometryFileName) +{ + ASSERT(scene.IsEmpty()); + + // init window + if (glfwInit() == GL_FALSE) + return false; + if (!window.Init(size, windowName)) + return false; + if (glewInit() != GLEW_OK) + return false; + name = windowName; + window.clbkOpenScene = DELEGATEBINDCLASS(Window::ClbkOpenScene, &Scene::Open, this); + + // init OpenGL + glPolygonMode(GL_FRONT, GL_FILL); + glEnable(GL_DEPTH_TEST); + glClearColor(0.f, 0.5f, 0.9f, 1.f); + + static const float light0_ambient[] = {0.1f, 0.1f, 0.1f, 1.0f}; + static const float light0_diffuse[] = {1.0f, 1.0f, 1.0f, 1.0f}; + static const float light0_position[] = {0.0f, 0.0f, 1000.0f, 0.0f}; + static const float light0_specular[] = {0.4f, 0.4f, 0.4f, 1.0f}; + + glLightfv(GL_LIGHT0, GL_AMBIENT, light0_ambient); + glLightfv(GL_LIGHT0, GL_DIFFUSE, light0_diffuse); + glLightfv(GL_LIGHT0, GL_SPECULAR, light0_specular); + glLightfv(GL_LIGHT0, GL_POSITION, light0_position); + glLightModelf(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE); + + glEnable(GL_LIGHT0); + glDisable(GL_LIGHTING); + + // init working thread + thread.start(ThreadWorker); + + // open scene or init empty scene + window.SetCamera(Camera()); + if (fileName != NULL) + Open(fileName, geometryFileName); + window.SetVisible(true); + return true; +} +bool Scene::Open(LPCTSTR fileName, LPCTSTR geometryFileName) +{ + ASSERT(fileName); + DEBUG_EXTRA("Loading: '%s'", Util::getFileNameExt(fileName).c_str()); + Empty(); + sceneName = fileName; + + // load the scene + WORKING_FOLDER = Util::getFilePath(fileName); + INIT_WORKING_FOLDER; + if (!scene.Load(fileName, true)) + return false; + if (geometryFileName) { + // try to load given mesh + MVS::Mesh mesh; + MVS::PointCloud pointcloud; + if (mesh.Load(geometryFileName)) { + scene.mesh.Swap(mesh); + geometryName = geometryFileName; + geometryMesh = true; + } else + // try to load as a point-cloud + if (pointcloud.Load(geometryFileName)) { + scene.pointcloud.Swap(pointcloud); + geometryName = geometryFileName; + geometryMesh = false; + } + } + if (!scene.pointcloud.IsEmpty()) + scene.pointcloud.PrintStatistics(scene.images.data(), &scene.obb); + + #if 1 + // create octree structure used to accelerate selection functionality + if (!scene.IsEmpty()) + events.AddEvent(new EVTComputeOctree(this)); + #endif + + // init scene + AABB3d bounds(true); + Point3d center(Point3d::INF); + if (scene.IsBounded()) { + bounds = AABB3d(scene.obb.GetAABB()); + center = bounds.GetCenter(); + } else { + if (!scene.pointcloud.IsEmpty()) { + bounds = scene.pointcloud.GetAABB(MINF(3u,scene.nCalibratedImages)); + if (bounds.IsEmpty()) + bounds = scene.pointcloud.GetAABB(); + center = scene.pointcloud.GetCenter(); + } + if (!scene.mesh.IsEmpty()) { + scene.mesh.ComputeNormalFaces(); + bounds.Insert(scene.mesh.GetAABB()); + center = scene.mesh.GetCenter(); + } + } + + // init images + AABB3d imageBounds(true); + images.Reserve(scene.images.size()); + FOREACH(idxImage, scene.images) { + const MVS::Image& imageData = scene.images[idxImage]; + if (!imageData.IsValid()) + continue; + images.emplace_back(idxImage); + imageBounds.InsertFull(imageData.camera.C); + } + if (imageBounds.IsEmpty()) + imageBounds.Enlarge(0.5); + if (bounds.IsEmpty()) + bounds = imageBounds; + + // init and load texture + if (scene.mesh.HasTexture()) { + FOREACH(i, scene.mesh.texturesDiffuse) { + Image& image = textures.emplace_back(); + ASSERT(image.idx == NO_ID); + #if 0 + Image8U3& textureDiffuse = scene.mesh.texturesDiffuse[i]; + cv::flip(textureDiffuse, textureDiffuse, 0); + image.SetImage(textureDiffuse); + textureDiffuse.release(); + #else // preserve texture, used only to be able to export the mesh + Image8U3 textureDiffuse; + cv::flip(scene.mesh.texturesDiffuse[i], textureDiffuse, 0); + image.SetImage(textureDiffuse); + #endif + image.GenerateMipmap(); + } + } + + // init display lists + // compile point-cloud + CompilePointCloud(); + // compile mesh + CompileMesh(); + // compile bounding-box + CompileBounds(); + + // init camera + window.SetCamera(Camera(bounds, + center == Point3d::INF ? Point3d(bounds.GetCenter()) : center, + images.size()<2?1.f:(float)imageBounds.EnlargePercent(REAL(1)/images.size()).GetSize().norm())); + window.camera.maxCamID = images.size(); + window.SetName(String::FormatString((name + _T(": %s")).c_str(), Util::getFileName(fileName).c_str())); + window.clbkSaveScene = DELEGATEBINDCLASS(Window::ClbkSaveScene, &Scene::Save, this); + window.clbkExportScene = DELEGATEBINDCLASS(Window::ClbkExportScene, &Scene::Export, this); + window.clbkCenterScene = DELEGATEBINDCLASS(Window::ClbkCenterScene, &Scene::Center, this); + window.clbkCompilePointCloud = DELEGATEBINDCLASS(Window::ClbkCompilePointCloud, &Scene::CompilePointCloud, this); + window.clbkCompileMesh = DELEGATEBINDCLASS(Window::ClbkCompileMesh, &Scene::CompileMesh, this); + window.clbkTogleSceneBox = DELEGATEBINDCLASS(Window::ClbkTogleSceneBox, &Scene::TogleSceneBox, this); + window.clbkCropToBounds = DELEGATEBINDCLASS(Window::ClbkCropToBounds, &Scene::CropToBounds, this); + if (scene.IsBounded()) + window.clbkCompileBounds = DELEGATEBINDCLASS(Window::ClbkCompileBounds, &Scene::CompileBounds, this); + if (!scene.IsEmpty()) + window.clbkRayScene = DELEGATEBINDCLASS(Window::ClbkRayScene, &Scene::CastRay, this); + window.Reset(!scene.pointcloud.IsEmpty()&&!scene.mesh.IsEmpty()?Window::SPR_NONE:Window::SPR_ALL, + MINF(2u,images.size())); + return true; +} + +// export the scene +bool Scene::Save(LPCTSTR _fileName, bool bRescaleImages) +{ + if (!IsOpen()) + return false; + REAL imageScale = 0; + if (bRescaleImages) { + window.SetVisible(false); + std::cout << "Enter image resolution scale: "; + String strScale; + std::cin >> strScale; + window.SetVisible(true); + imageScale = strScale.From(0); + } + const String fileName(_fileName != NULL ? String(_fileName) : Util::insertBeforeFileExt(sceneName, _T("_new"))); + MVS::Mesh mesh; + if (!scene.mesh.IsEmpty() && !geometryName.empty() && geometryMesh) + mesh.Swap(scene.mesh); + MVS::PointCloud pointcloud; + if (!scene.pointcloud.IsEmpty() && !geometryName.empty() && !geometryMesh) + pointcloud.Swap(scene.pointcloud); + if (imageScale > 0 && imageScale < 1) { + // scale and save images + const String folderName(Util::getFilePath(MAKE_PATH_FULL(WORKING_FOLDER_FULL, fileName)) + String::FormatString("images%d" PATH_SEPARATOR_STR, ROUND2INT(imageScale*100))); + if (!scene.ScaleImages(0, imageScale, folderName)) { + DEBUG("error: can not scale scene images to '%s'", folderName.c_str()); + return false; + } + } + if (!scene.Save(fileName, nArchiveType)) { + DEBUG("error: can not save scene to '%s'", fileName.c_str()); + return false; + } + if (!mesh.IsEmpty()) + scene.mesh.Swap(mesh); + if (!pointcloud.IsEmpty()) + scene.pointcloud.Swap(pointcloud); + sceneName = fileName; + return true; +} + +// export the scene +bool Scene::Export(LPCTSTR _fileName, LPCTSTR exportType) const +{ + if (!IsOpen()) + return false; + ASSERT(!sceneName.IsEmpty()); + String lastFileName; + const String fileName(_fileName != NULL ? String(_fileName) : sceneName); + const String baseFileName(Util::getFileFullName(fileName)); + const bool bPoints(scene.pointcloud.Save(lastFileName=(baseFileName+_T("_pointcloud.ply")), nArchiveType==ARCHIVE_MVS)); + const bool bMesh(scene.mesh.Save(lastFileName=(baseFileName+_T("_mesh")+(exportType?exportType:(Util::getFileExt(fileName)==_T(".obj")?_T(".obj"):_T(".ply")))), cList(), true)); + #if TD_VERBOSE != TD_VERBOSE_OFF + if (VERBOSITY_LEVEL > 2 && (bPoints || bMesh)) + scene.ExportCamerasMLP(Util::getFileFullName(lastFileName)+_T(".mlp"), lastFileName); + #endif + AABB3f aabb(true); + if (scene.IsBounded()) { + std::ofstream fs(baseFileName+_T("_roi.txt")); + if (fs) + fs << scene.obb; + aabb = scene.obb.GetAABB(); + } else + if (!scene.pointcloud.IsEmpty()) { + aabb = scene.pointcloud.GetAABB(); + } else + if (!scene.mesh.IsEmpty()) { + aabb = scene.mesh.GetAABB(); + } + if (!aabb.IsEmpty()) { + std::ofstream fs(baseFileName+_T("_roi_box.txt")); + if (fs) + fs << aabb; + } + return bPoints || bMesh; +} + +void Scene::CompilePointCloud() +{ + if (scene.pointcloud.IsEmpty()) + return; + ReleasePointCloud(); + listPointCloud = glGenLists(1); + glNewList(listPointCloud, GL_COMPILE); + ASSERT((window.sparseType&(Window::SPR_POINTS|Window::SPR_LINES)) != 0); + // compile point-cloud + if ((window.sparseType&Window::SPR_POINTS) != 0) { + ASSERT_ARE_SAME_TYPE(float, MVS::PointCloud::Point::Type); + glBegin(GL_POINTS); + glColor3f(1.f,1.f,1.f); + FOREACH(i, scene.pointcloud.points) { + if (!scene.pointcloud.pointViews.empty() && + scene.pointcloud.pointViews[i].size() < window.minViews) + continue; + if (!scene.pointcloud.colors.empty()) { + const MVS::PointCloud::Color& c = scene.pointcloud.colors[i]; + glColor3ub(c.r,c.g,c.b); + } + const MVS::PointCloud::Point& X = scene.pointcloud.points[i]; + glVertex3fv(X.ptr()); + } + glEnd(); + } + glEndList(); +} + +void Scene::CompileMesh() +{ + if (scene.mesh.IsEmpty()) + return; + ReleaseMesh(); + if (scene.mesh.faceNormals.empty()) + scene.mesh.ComputeNormalFaces(); + // translate, normalize and flip Y axis of the texture coordinates + MVS::Mesh::TexCoordArr normFaceTexcoords; + if (scene.mesh.HasTexture() && window.bRenderTexture) + scene.mesh.FaceTexcoordsNormalize(normFaceTexcoords, true); + MVS::Mesh::TexIndex texIdx(0); + do { + GLuint& listMesh = listMeshes.emplace_back(glGenLists(1)); + listMesh = glGenLists(1); + glNewList(listMesh, GL_COMPILE); + // compile mesh + ASSERT_ARE_SAME_TYPE(float, MVS::Mesh::Vertex::Type); + ASSERT_ARE_SAME_TYPE(float, MVS::Mesh::Normal::Type); + ASSERT_ARE_SAME_TYPE(float, MVS::Mesh::TexCoord::Type); + glColor3f(1.f, 1.f, 1.f); + glBegin(GL_TRIANGLES); + FOREACH(idxFace, scene.mesh.faces) { + if (!scene.mesh.faceTexindices.empty() && scene.mesh.faceTexindices[idxFace] != texIdx) + continue; + const MVS::Mesh::Face& face = scene.mesh.faces[idxFace]; + const MVS::Mesh::Normal& n = scene.mesh.faceNormals[idxFace]; + glNormal3fv(n.ptr()); + for (int j = 0; j < 3; ++j) { + if (!normFaceTexcoords.empty()) { + const MVS::Mesh::TexCoord& t = normFaceTexcoords[idxFace*3 + j]; + glTexCoord2fv(t.ptr()); + } + const MVS::Mesh::Vertex& p = scene.mesh.vertices[face[j]]; + glVertex3fv(p.ptr()); + } + } + glEnd(); + glEndList(); + } while (++texIdx < scene.mesh.texturesDiffuse.size()); +} + +void Scene::CompileBounds() +{ + obbPoints.Release(); + if (!scene.IsBounded()) { + window.bRenderBounds = false; + return; + } + window.bRenderBounds = !window.bRenderBounds; + if (window.bRenderBounds) { + static const uint8_t indices[12*2] = { + 0,2, 2,3, 3,1, 1,0, + 0,6, 2,4, 3,5, 1,7, + 6,4, 4,5, 5,7, 7,6 + }; + OBB3f::POINT corners[OBB3f::numCorners]; + scene.obb.GetCorners(corners); + for (int i=0; i<12; ++i) { + obbPoints.emplace_back(corners[indices[i*2+0]]); + obbPoints.emplace_back(corners[indices[i*2+1]]); + } + } +} + +void Scene::CropToBounds() +{ + if (!IsOpen()) + return; + if (!scene.IsBounded()) + return; + scene.pointcloud.RemovePointsOutside(scene.obb); + scene.mesh.RemoveFacesOutside(scene.obb); + Center(); +} + +void Scene::Draw() +{ + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glPointSize(window.pointSize); + + // render point-cloud + if (listPointCloud) { + glDisable(GL_TEXTURE_2D); + glCallList(listPointCloud); + } + // render mesh + if (!listMeshes.empty()) { + glEnable(GL_DEPTH_TEST); + glEnable(GL_CULL_FACE); + if (!scene.mesh.faceTexcoords.empty() && window.bRenderTexture) { + glEnable(GL_TEXTURE_2D); + FOREACH(i, listMeshes) { + textures[i].Bind(); + glCallList(listMeshes[i]); + } + glDisable(GL_TEXTURE_2D); + } else { + glEnable(GL_LIGHTING); + for (GLuint listMesh: listMeshes) + glCallList(listMesh); + glDisable(GL_LIGHTING); + } + } + // render cameras + if (window.bRenderCameras) { + glDisable(GL_CULL_FACE); + const Point3* ptrPrevC(NULL); + FOREACH(idx, images) { + Image& image = images[idx]; + const MVS::Image& imageData = scene.images[image.idx]; + const MVS::Camera& camera = imageData.camera; + // cache image corner coordinates + const double scaleFocal(window.camera.scaleF); + const Point2d pp(camera.GetPrincipalPoint()); + const double focal(camera.GetFocalLength()/scaleFocal); + const double cx(-pp.x/focal); + const double cy(-pp.y/focal); + const double px((double)imageData.width/focal+cx); + const double py((double)imageData.height/focal+cy); + const Point3d ic1(cx, cy, scaleFocal); + const Point3d ic2(cx, py, scaleFocal); + const Point3d ic3(px, py, scaleFocal); + const Point3d ic4(px, cy, scaleFocal); + // change coordinates system to the camera space + glPushMatrix(); + glMultMatrixd((GLdouble*)TransL2W((const Matrix3x3::EMat)camera.R, -(const Point3::EVec)camera.C).data()); + glPointSize(window.pointSize+1.f); + glDisable(GL_TEXTURE_2D); + // draw camera position and image center + glBegin(GL_POINTS); + glColor3f(1,0,0); glVertex3f(0,0,0); // camera position + glColor3f(0,1,0); glVertex3f(0,0,(float)scaleFocal); // image center + glColor3f(0,0,1); glVertex3d((0.5*imageData.width-pp.x)/focal, cy, scaleFocal); // image up + glEnd(); + // draw image thumbnail + const bool bSelectedImage(idx == window.camera.currentCamID); + if (bSelectedImage) { + if (image.IsValid()) { + // render image + glEnable(GL_TEXTURE_2D); + image.Bind(); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + glDisable(GL_DEPTH_TEST); + glColor4f(1,1,1,window.cameraBlend); + glBegin(GL_QUADS); + glTexCoord2d(0,0); glVertex3dv(ic1.ptr()); + glTexCoord2d(0,1); glVertex3dv(ic2.ptr()); + glTexCoord2d(1,1); glVertex3dv(ic3.ptr()); + glTexCoord2d(1,0); glVertex3dv(ic4.ptr()); + glEnd(); + glDisable(GL_TEXTURE_2D); + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + } else { + // start and wait to load the image + if (image.IsImageEmpty()) { + // start loading + image.SetImageLoading(); + events.AddEvent(new EVTLoadImage(this, idx, IMAGE_MAX_RESOLUTION)); + } else { + // check if the image is available and set it + image.TransferImage(); + } + } + } + // draw camera frame + glColor3f(bSelectedImage ? 0.f : 1.f, 1.f, 0.f); + glBegin(GL_LINES); + glVertex3d(0,0,0); glVertex3dv(ic1.ptr()); + glVertex3d(0,0,0); glVertex3dv(ic2.ptr()); + glVertex3d(0,0,0); glVertex3dv(ic3.ptr()); + glVertex3d(0,0,0); glVertex3dv(ic4.ptr()); + glVertex3dv(ic1.ptr()); glVertex3dv(ic2.ptr()); + glVertex3dv(ic2.ptr()); glVertex3dv(ic3.ptr()); + glVertex3dv(ic3.ptr()); glVertex3dv(ic4.ptr()); + glVertex3dv(ic4.ptr()); glVertex3dv(ic1.ptr()); + glEnd(); + // restore coordinate system + glPopMatrix(); + // render image visibility info + if (window.bRenderImageVisibility && idx != NO_ID && idx==window.camera.currentCamID) { + if (scene.pointcloud.IsValid()) { + const Image& image = images[idx]; + glPointSize(window.pointSize*1.1f); + glDisable(GL_DEPTH_TEST); + glBegin(GL_POINTS); + glColor3f(1.f,0.f,0.f); + FOREACH(i, scene.pointcloud.points) { + ASSERT(!scene.pointcloud.pointViews[i].empty()); + if (scene.pointcloud.pointViews[i].size() < window.minViews) + continue; + if (scene.pointcloud.pointViews[i].FindFirst(image.idx) == MVS::PointCloud::ViewArr::NO_INDEX) + continue; + glVertex3fv(scene.pointcloud.points[i].ptr()); + } + glEnd(); + glEnable(GL_DEPTH_TEST); + glPointSize(window.pointSize); + } + } + // render camera trajectory + if (window.bRenderCameraTrajectory && ptrPrevC) { + glBegin(GL_LINES); + glColor3f(1.f,0.5f,0.f); + glVertex3dv(ptrPrevC->ptr()); + glVertex3dv(camera.C.ptr()); + glEnd(); + } + ptrPrevC = &camera.C; + } + } + // render selection + if (window.selectionType != Window::SEL_NA) { + glPointSize(window.pointSize+4); + glDisable(GL_DEPTH_TEST); + glBegin(GL_POINTS); + glColor3f(1,0,0); glVertex3fv(window.selectionPoints[0].ptr()); + if (window.selectionType == Window::SEL_TRIANGLE) { + glColor3f(0,1,0); glVertex3fv(window.selectionPoints[1].ptr()); + glColor3f(0,0,1); glVertex3fv(window.selectionPoints[2].ptr()); + } + glEnd(); + if (window.bRenderViews && window.selectionType == Window::SEL_POINT) { + if (!scene.pointcloud.pointViews.empty()) { + glBegin(GL_LINES); + const MVS::PointCloud::ViewArr& views = scene.pointcloud.pointViews[(MVS::PointCloud::Index)window.selectionIdx]; + ASSERT(!views.empty()); + for (MVS::PointCloud::View idxImage: views) { + const MVS::Image& imageData = scene.images[idxImage]; + glVertex3dv(imageData.camera.C.ptr()); + glVertex3fv(window.selectionPoints[0].ptr()); + } + glEnd(); + } + } + glEnable(GL_DEPTH_TEST); + glPointSize(window.pointSize); + } + // render oriented-bounding-box + if (!obbPoints.empty()) { + glDepthMask(GL_FALSE); + glBegin(GL_LINES); + glColor3f(0.5f,0.1f,0.8f); + for (IDX i=0; i timeClick) { + // this is a long click, ignore it + break; + } else + if (window.selectionType != Window::SEL_NA && + now-window.selectionTime < timeDblClick) { + // this is a double click, center scene at the selected point + window.CenterCamera(window.selectionPoints[3]); + window.selectionTime = now; + } else + if (!octMesh.IsEmpty()) { + // find ray intersection with the mesh + const MVS::IntersectRayMesh intRay(octMesh, ray, scene.mesh); + if (intRay.pick.IsValid()) { + const MVS::Mesh::Face& face = scene.mesh.faces[(MVS::Mesh::FIndex)intRay.pick.idx]; + window.selectionPoints[0] = scene.mesh.vertices[face[0]]; + window.selectionPoints[1] = scene.mesh.vertices[face[1]]; + window.selectionPoints[2] = scene.mesh.vertices[face[2]]; + window.selectionPoints[3] = ray.GetPoint(intRay.pick.dist).cast(); + window.selectionType = Window::SEL_TRIANGLE; + window.selectionTime = now; + window.selectionIdx = intRay.pick.idx; + DEBUG("Face selected:\n\tindex: %u\n\tvertex 1: %u (%g %g %g)\n\tvertex 2: %u (%g %g %g)\n\tvertex 3: %u (%g %g %g)", + intRay.pick.idx, + face[0], window.selectionPoints[0].x, window.selectionPoints[0].y, window.selectionPoints[0].z, + face[1], window.selectionPoints[1].x, window.selectionPoints[1].y, window.selectionPoints[1].z, + face[2], window.selectionPoints[2].x, window.selectionPoints[2].y, window.selectionPoints[2].z + ); + } else { + window.selectionType = Window::SEL_NA; + } + } else + if (!octPoints.IsEmpty()) { + // find ray intersection with the points + const MVS::IntersectRayPoints intRay(octPoints, ray, scene.pointcloud, window.minViews); + if (intRay.pick.IsValid()) { + window.selectionPoints[0] = window.selectionPoints[3] = scene.pointcloud.points[intRay.pick.idx]; + window.selectionType = Window::SEL_POINT; + window.selectionTime = now; + window.selectionIdx = intRay.pick.idx; + DEBUG("Point selected:\n\tindex: %u (%g %g %g)%s", + intRay.pick.idx, + window.selectionPoints[0].x, window.selectionPoints[0].y, window.selectionPoints[0].z, + [&]() { + if (scene.pointcloud.pointViews.empty()) + return String(); + const MVS::PointCloud::ViewArr& views = scene.pointcloud.pointViews[intRay.pick.idx]; + ASSERT(!views.empty()); + String strViews(String::FormatString("\n\tviews: %u", views.size())); + for (MVS::PointCloud::View idxImage: views) { + const MVS::Image& imageData = scene.images[idxImage]; + const Point2 x(imageData.camera.TransformPointW2I(Cast(window.selectionPoints[0]))); + strViews += String::FormatString("\n\t\t%s (%.2f %.2f)", Util::getFileNameExt(imageData.name).c_str(), x.x, x.y); + } + return strViews; + }().c_str() + ); + } else { + window.selectionType = Window::SEL_NA; + } + } + break; } + } +} +/*----------------------------------------------------------------*/ diff --git a/apps/Viewer/Scene.h b/apps/Viewer/Scene.h new file mode 100644 index 0000000..9315b35 --- /dev/null +++ b/apps/Viewer/Scene.h @@ -0,0 +1,111 @@ +/* + * Scene.h + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#ifndef _VIEWER_SCENE_H_ +#define _VIEWER_SCENE_H_ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Window.h" + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace VIEWER { + +class Scene +{ +public: + typedef MVS::PointCloud::Octree OctreePoints; + typedef MVS::Mesh::Octree OctreeMesh; + +public: + ARCHIVE_TYPE nArchiveType; + String name; + + String sceneName; + String geometryName; + bool geometryMesh; + MVS::Scene scene; + Window window; + ImageArr images; // scene photos + ImageArr textures; // mesh textures + + OctreePoints octPoints; + OctreeMesh octMesh; + Point3fArr obbPoints; + + GLuint listPointCloud; + CLISTDEF0IDX(GLuint,MVS::Mesh::TexIndex) listMeshes; + + // multi-threading + static SEACAVE::EventQueue events; // internal events queue (processed by the working threads) + static SEACAVE::Thread thread; // worker thread + +public: + explicit Scene(ARCHIVE_TYPE _nArchiveType = ARCHIVE_MVS); + ~Scene(); + + void Empty(); + void Release(); + void ReleasePointCloud(); + void ReleaseMesh(); + inline bool IsValid() const { return window.IsValid(); } + inline bool IsOpen() const { return IsValid() && !scene.IsEmpty(); } + inline bool IsOctreeValid() const { return !octPoints.IsEmpty() || !octMesh.IsEmpty(); } + + bool Init(const cv::Size&, LPCTSTR windowName, LPCTSTR fileName=NULL, LPCTSTR geometryFileName=NULL); + bool Open(LPCTSTR fileName, LPCTSTR geometryFileName=NULL); + bool Save(LPCTSTR fileName=NULL, bool bRescaleImages=false); + bool Export(LPCTSTR fileName, LPCTSTR exportType=NULL) const; + void CompilePointCloud(); + void CompileMesh(); + void CompileBounds(); + void CropToBounds(); + + void Draw(); + void Loop(); + + void Center(); + void TogleSceneBox(); + void CastRay(const Ray3&, int); +protected: + static void* ThreadWorker(void*); +}; +/*----------------------------------------------------------------*/ + +} // namespace VIEWER + +#endif // _VIEWER_SCENE_H_ diff --git a/apps/Viewer/Viewer.cpp b/apps/Viewer/Viewer.cpp new file mode 100644 index 0000000..5bff3c2 --- /dev/null +++ b/apps/Viewer/Viewer.cpp @@ -0,0 +1,226 @@ +/* + * Viewer.cpp + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "Common.h" +#include + +#include "Scene.h" + +using namespace VIEWER; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define APPNAME _T("Viewer") + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace { + +namespace OPT { +String strInputFileName; +String strGeometryFileName; +String strOutputFileName; +unsigned nArchiveType; +int nProcessPriority; +unsigned nMaxThreads; +unsigned nMaxMemory; +String strExportType; +String strConfigFileName; +#if TD_VERBOSE != TD_VERBOSE_OFF +bool bLogFile; +#endif +boost::program_options::variables_map vm; +} // namespace OPT + +class Application { +public: + Application() {} + ~Application() { Finalize(); } + + bool Initialize(size_t argc, LPCTSTR* argv); + void Finalize(); +}; // Application + +// initialize and parse the command line parameters +bool Application::Initialize(size_t argc, LPCTSTR* argv) +{ + // initialize log and console + OPEN_LOG(); + OPEN_LOGCONSOLE(); + + // group of options allowed only on command line + boost::program_options::options_description generic("Generic options"); + generic.add_options() + ("help,h", "produce this help message") + ("working-folder,w", boost::program_options::value(&WORKING_FOLDER), "working directory (default current directory)") + ("config-file,c", boost::program_options::value(&OPT::strConfigFileName)->default_value(APPNAME _T(".cfg")), "file name containing program options") + ("export-type", boost::program_options::value(&OPT::strExportType), "file type used to export the 3D scene (ply or obj)") + ("archive-type", boost::program_options::value(&OPT::nArchiveType)->default_value(ARCHIVE_MVS), "project archive type: -1-interface, 0-text, 1-binary, 2-compressed binary") + ("process-priority", boost::program_options::value(&OPT::nProcessPriority)->default_value(0), "process priority (normal by default)") + ("max-threads", boost::program_options::value(&OPT::nMaxThreads)->default_value(0), "maximum number of threads that this process should use (0 - use all available cores)") + ("max-memory", boost::program_options::value(&OPT::nMaxMemory)->default_value(0), "maximum amount of memory in MB that this process should use (0 - use all available memory)") + #if TD_VERBOSE != TD_VERBOSE_OFF + ("log-file", boost::program_options::value(&OPT::bLogFile)->default_value(false), "dump log to a file") + ("verbosity,v", boost::program_options::value(&g_nVerbosityLevel)->default_value( + #if TD_VERBOSE == TD_VERBOSE_DEBUG + 3 + #else + 2 + #endif + ), "verbosity level") + #endif + ; + + // group of options allowed both on command line and in config file + boost::program_options::options_description config("Viewer options"); + config.add_options() + ("input-file,i", boost::program_options::value(&OPT::strInputFileName), "input project filename containing camera poses and scene (point-cloud/mesh)") + ("geometry-file,g", boost::program_options::value(&OPT::strGeometryFileName), "mesh or point-cloud with views file name (overwrite existing geometry)") + ("output-file,o", boost::program_options::value(&OPT::strOutputFileName), "output filename for storing the mesh") + ; + + boost::program_options::options_description cmdline_options; + cmdline_options.add(generic).add(config); + + boost::program_options::options_description config_file_options; + config_file_options.add(config); + + boost::program_options::positional_options_description p; + p.add("input-file", -1); + + try { + // parse command line options + boost::program_options::store(boost::program_options::command_line_parser((int)argc, argv).options(cmdline_options).positional(p).run(), OPT::vm); + boost::program_options::notify(OPT::vm); + INIT_WORKING_FOLDER; + // parse configuration file + std::ifstream ifs(MAKE_PATH_SAFE(OPT::strConfigFileName)); + if (ifs) { + boost::program_options::store(parse_config_file(ifs, config_file_options), OPT::vm); + boost::program_options::notify(OPT::vm); + } + } + catch (const std::exception& e) { + LOG(e.what()); + return false; + } + + #if TD_VERBOSE != TD_VERBOSE_OFF + // initialize the log file + if (OPT::bLogFile) + OPEN_LOGFILE((MAKE_PATH(APPNAME _T("-")+Util::getUniqueName(0)+_T(".log"))).c_str()); + #endif + + // print application details: version and command line + Util::LogBuild(); + LOG(_T("Command line: ") APPNAME _T("%s"), Util::CommandLineToString(argc, argv).c_str()); + + // validate input + Util::ensureValidPath(OPT::strInputFileName); + if (OPT::vm.count("help")) { + boost::program_options::options_description visible("Available options"); + visible.add(generic).add(config); + GET_LOG() << _T("\n" + "Visualize any know point-cloud/mesh formats or MVS projects. Supply files through command line or Drag&Drop.\n" + "Keys:\n" + "\tE: export scene\n" + "\tR: reset scene\n" + "\tB: render bounds\n" + "\tB + Shift: togle bounds\n" + "\tC: render cameras\n" + "\tC + Shift: render camera trajectory\n" + "\tC + Ctrl: center scene\n" + "\tLeft/Right: select next camera to view the scene\n" + "\tS: save scene\n" + "\tS + Shift: rescale images and save scene\n" + "\tT: render mesh texture\n" + "\tW: render wire-frame mesh\n" + "\tV: render view rays to the selected point\n" + "\tV + Shift: render points seen by the current view\n" + "\tUp/Down: adjust point size\n" + "\tUp/Down + Shift: adjust minimum number of views accepted when displaying a point or line\n" + "\t+/-: adjust camera thumbnail transparency\n" + "\t+/- + Shift: adjust camera cones' length\n" + "\n") + << visible; + } + if (!OPT::strExportType.empty()) + OPT::strExportType = OPT::strExportType.ToLower() == _T("obj") ? _T(".obj") : _T(".ply"); + + // initialize optional options + Util::ensureValidPath(OPT::strGeometryFileName); + Util::ensureValidPath(OPT::strOutputFileName); + + MVS::Initialize(APPNAME, OPT::nMaxThreads, OPT::nProcessPriority); + return true; +} + +// finalize application instance +void Application::Finalize() +{ + MVS::Finalize(); + + if (OPT::bLogFile) + CLOSE_LOGFILE(); + CLOSE_LOGCONSOLE(); + CLOSE_LOG(); +} + +} // unnamed namespace + +int main(int argc, LPCTSTR* argv) +{ + #ifdef _DEBUGINFO + // set _crtBreakAlloc index to stop in at allocation + _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);// | _CRTDBG_CHECK_ALWAYS_DF); + #endif + + Application application; + if (!application.Initialize(argc, argv)) + return EXIT_FAILURE; + + // create viewer + Scene viewer; + if (!viewer.Init(cv::Size(1280, 720), APPNAME, + OPT::strInputFileName.empty() ? NULL : MAKE_PATH_SAFE(OPT::strInputFileName).c_str(), + OPT::strGeometryFileName.empty() ? NULL : MAKE_PATH_SAFE(OPT::strGeometryFileName).c_str())) + return EXIT_FAILURE; + if (viewer.IsOpen() && !OPT::strOutputFileName.empty()) { + // export the scene + viewer.Export(MAKE_PATH_SAFE(OPT::strOutputFileName), OPT::strExportType.empty()?LPCTSTR(NULL):OPT::strExportType.c_str()); + } + // enter viewer loop + viewer.Loop(); + return EXIT_SUCCESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/Viewer/Window.cpp b/apps/Viewer/Window.cpp new file mode 100644 index 0000000..cfd2f12 --- /dev/null +++ b/apps/Viewer/Window.cpp @@ -0,0 +1,493 @@ +/* + * Window.cpp + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#include "Common.h" +#include "Window.h" + +using namespace VIEWER; + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +Window::WindowsMap Window::g_mapWindows; + +Window::Window() + : + window(NULL), + pos(Eigen::Vector2d::Zero()), + prevPos(Eigen::Vector2d::Zero()) +{ +} +Window::~Window() +{ + Release(); +} + +void Window::Release() +{ + if (IsValid()) { + #ifdef _USE_NUKLEAR + nk_glfw3_shutdown(); + #endif + glfwDestroyWindow(window); + window = NULL; + } + clbkOpenScene.reset(); + ReleaseClbk(); +} + +void Window::ReleaseClbk() +{ + clbkSaveScene.reset(); + clbkExportScene.reset(); + clbkCenterScene.reset(); + clbkRayScene.reset(); + clbkCompilePointCloud.reset(); + clbkCompileMesh.reset(); + clbkCompileBounds.reset(); + clbkTogleSceneBox.reset(); + clbkCropToBounds.reset(); +} + +bool Window::Init(const cv::Size& _size, LPCTSTR name) +{ + sizeScale = 1; + size = _size; + + glfwDefaultWindowHints(); + glfwWindowHint(GLFW_VISIBLE, 0); + window = glfwCreateWindow(size.width, size.height, name, NULL, NULL); + if (!window) + return false; + glfwMakeContextCurrent(window); + glfwSetFramebufferSizeCallback(window, Window::Resize); + glfwSetKeyCallback(window, Window::Key); + glfwSetMouseButtonCallback(window, Window::MouseButton); + glfwSetCursorPosCallback(window, Window::MouseMove); + glfwSetScrollCallback(window, Window::Scroll); + glfwSetDropCallback(window, Window::Drop); + g_mapWindows[window] = this; + + Reset(); + return true; +} +void Window::SetCamera(const Camera& cam) +{ + camera = cam; + cv::Size _size; + glfwGetFramebufferSize(window, &_size.width, &_size.height); + Resize(_size); +} +void Window::SetName(LPCTSTR name) +{ + glfwSetWindowTitle(window, name); +} +void Window::SetVisible(bool v) +{ + if (v) + glfwShowWindow(window); + else + glfwHideWindow(window); +} +bool Window::IsVisible() const +{ + return glfwGetWindowAttrib(window, GLFW_VISIBLE) != 0; +} +void Window::Reset(SPARSE _sparseType, unsigned _minViews) +{ + camera.Reset(); + inputType = INP_NA; + sparseType = _sparseType; + minViews = _minViews; + pointSize = 2.f; + cameraBlend = 0.5f; + bRenderCameras = true; + bRenderCameraTrajectory = true; + bRenderImageVisibility = false; + bRenderViews = true; + bRenderSolid = true; + bRenderTexture = true; + bRenderBounds = false; + selectionType = SEL_NA; + selectionIdx = NO_IDX; + if (clbkCompilePointCloud != NULL) + clbkCompilePointCloud(); + if (clbkCompileMesh != NULL) + clbkCompileMesh(); + glfwPostEmptyEvent(); +} + + +void Window::CenterCamera(const Point3& pos) +{ + camera.center = pos; + camera.dist *= 0.7; +} + + +void Window::UpdateView(const ImageArr& images, const MVS::ImageArr& sceneImagesMVS) +{ + if (camera.IsCameraViewMode()) { + // enable camera view mode and apply current camera transform + const Image& image = images[camera.currentCamID]; + const MVS::Camera& camera = sceneImagesMVS[image.idx].camera; + UpdateView((const Matrix3x3::EMat)camera.R, camera.GetT()); + } else { + // apply view point transform + glMatrixMode(GL_MODELVIEW); + const Eigen::Matrix4d trans(camera.GetLookAt()); + glLoadMatrixd((GLdouble*)trans.data()); + } +} + +void Window::UpdateView(const Eigen::Matrix3d& R, const Eigen::Vector3d& t) +{ + glMatrixMode(GL_MODELVIEW); + transform = gs_convert * TransW2L(R, t); + glLoadMatrixd((GLdouble*)transform.data()); +} + +void Window::UpdateMousePosition(double xpos, double ypos) +{ + prevPos = pos; + pos.x() = xpos; + pos.y() = ypos; + // normalize position to [-1:1] range + const int w(camera.size.width); + const int h(camera.size.height); + pos.x() = (2.0 * pos.x() - w) / w; + pos.y() = (h - 2.0 * pos.y()) / h; +} + + +void Window::GetFrame(Image8U3& image) const +{ + image.create(GetSize()); + glReadPixels(0, 0, image.width(), image.height(), GL_BGR_EXT, GL_UNSIGNED_BYTE, image.ptr()); + cv::flip(image, image, 0); +} + + +cv::Size Window::GetSize() const +{ + cv::Size _size; + glfwGetWindowSize(window, &_size.width, &_size.height); + return _size; +} +void Window::Resize(const cv::Size& _size) +{ + // detect scaled window + sizeScale = (double)GetSize().width/_size.width; + size = _size; + // update resolution + glfwMakeContextCurrent(window); + glViewport(0, 0, size.width, size.height); + camera.Resize(cv::Size(ROUND2INT(size.width*sizeScale), ROUND2INT(size.height*sizeScale))); +} +void Window::Resize(GLFWwindow* window, int width, int height) +{ + g_mapWindows[window]->Resize(cv::Size(width, height)); +} + +void Window::Key(int k, int /*scancode*/, int action, int mod) +{ + switch (k) { + case GLFW_KEY_ESCAPE: + if (action == GLFW_RELEASE) + glfwSetWindowShouldClose(window, 1); + break; + case GLFW_KEY_DOWN: + if (action == GLFW_RELEASE) { + if (mod & GLFW_MOD_SHIFT) { + if (minViews > 2) { + minViews--; + if (clbkCompilePointCloud != NULL) + clbkCompilePointCloud(); + } + } else { + pointSize = MAXF(pointSize-0.5f, 0.5f); + } + } + break; + case GLFW_KEY_UP: + if (action == GLFW_RELEASE) { + if (mod & GLFW_MOD_SHIFT) { + minViews++; + if (clbkCompilePointCloud != NULL) + clbkCompilePointCloud(); + } else { + pointSize += 0.5f; + } + } + break; + case GLFW_KEY_LEFT: + if (action != GLFW_RELEASE) { + camera.prevCamID = camera.currentCamID; + camera.currentCamID--; + if (camera.currentCamID < NO_ID && camera.currentCamID >= camera.maxCamID) + camera.currentCamID = camera.maxCamID-1; + } + break; + case GLFW_KEY_RIGHT: + if (action != GLFW_RELEASE) { + camera.prevCamID = camera.currentCamID; + camera.currentCamID++; + if (camera.currentCamID >= camera.maxCamID) + camera.currentCamID = NO_ID; + } + break; + case GLFW_KEY_B: + if (action == GLFW_RELEASE) { + if (mod & GLFW_MOD_CONTROL) { + if (clbkCropToBounds != NULL) + clbkCropToBounds(); + } else if (mod & GLFW_MOD_SHIFT) { + if (clbkTogleSceneBox != NULL) + clbkTogleSceneBox(); + } else { + if (clbkCompileBounds != NULL) + clbkCompileBounds(); + } + } + break; + case GLFW_KEY_C: + if (action == GLFW_RELEASE) { + if (mod & GLFW_MOD_SHIFT) { + bRenderCameraTrajectory = !bRenderCameraTrajectory; + } else if (mod & GLFW_MOD_CONTROL) { + if (clbkCenterScene != NULL) + clbkCenterScene(); + } else { + bRenderCameras = !bRenderCameras; + } + } + break; + case GLFW_KEY_E: + if (action == GLFW_RELEASE && clbkExportScene != NULL) + clbkExportScene(NULL, NULL); + break; + case GLFW_KEY_P: + switch (sparseType) { + case SPR_POINTS: sparseType = SPR_LINES; break; + case SPR_LINES: sparseType = SPR_ALL; break; + case SPR_ALL: sparseType = SPR_POINTS; break; + } + if (clbkCompilePointCloud != NULL) + clbkCompilePointCloud(); + break; + case GLFW_KEY_R: + if (action == GLFW_RELEASE) + Reset(); + break; + case GLFW_KEY_S: + if (action == GLFW_RELEASE) { + if (clbkSaveScene != NULL) + clbkSaveScene(NULL, (mod & GLFW_MOD_SHIFT) != 0); + } + break; + case GLFW_KEY_T: + if (action == GLFW_RELEASE) { + bRenderTexture = !bRenderTexture; + if (clbkCompileMesh != NULL) + clbkCompileMesh(); + } + break; + case GLFW_KEY_V: + if (action == GLFW_RELEASE) { + if (mod & GLFW_MOD_SHIFT) { + bRenderImageVisibility = !bRenderImageVisibility; + } else { + bRenderViews = !bRenderViews; + } + } + break; + case GLFW_KEY_W: + if (action == GLFW_RELEASE) { + if (bRenderSolid) { + bRenderSolid = false; + glPolygonMode(GL_FRONT, GL_LINE); + } else { + bRenderSolid = true; + glPolygonMode(GL_FRONT, GL_FILL); + } + } + break; + case GLFW_KEY_KP_SUBTRACT: + if (action == GLFW_RELEASE) { + if (mod & GLFW_MOD_CONTROL) + camera.SetFOV(camera.fov-5.f); + else if (mod & GLFW_MOD_SHIFT) + camera.scaleF *= 0.9f; + else + cameraBlend = MAXF(cameraBlend-0.1f, 0.f); + } + break; + case GLFW_KEY_KP_ADD: + if (action == GLFW_RELEASE) { + if (mod & GLFW_MOD_CONTROL) + camera.SetFOV(camera.fov+5.f); + else if (mod & GLFW_MOD_SHIFT) + camera.scaleF *= 1.1111f; + else + cameraBlend = MINF(cameraBlend+0.1f, 1.f); + } + break; + } +} +void Window::Key(GLFWwindow* window, int k, int scancode, int action, int mod) +{ + g_mapWindows[window]->Key(k, scancode, action, mod); +} + +void Window::MouseButton(int button, int action, int /*mods*/) +{ + switch (button) { + case GLFW_MOUSE_BUTTON_LEFT: { + if (action == GLFW_PRESS) { + inputType.set(INP_MOUSE_LEFT); + } else + if (action == GLFW_RELEASE) { + inputType.unset(INP_MOUSE_LEFT); + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } + if (clbkRayScene != NULL) { + typedef Eigen::Matrix Mat4; + Mat4 P, V; + glGetDoublev(GL_MODELVIEW_MATRIX, V.data()); + glGetDoublev(GL_PROJECTION_MATRIX, P.data()); + // 4d Homogeneous Clip Coordinates + const Eigen::Vector4d ray_clip(pos.x(), pos.y(), -1.0, 1.0); + // 4d Eye (Camera) Coordinates + Eigen::Vector4d ray_eye(P.inverse()*ray_clip); + ray_eye.z() = -1.0; + ray_eye.w() = 0.0; + // 4d World Coordinates + const Mat4 invV(V.inverse()); + ASSERT(ISEQUAL(invV(3,3),1.0)); + const Eigen::Vector3d start(invV.topRightCorner<3,1>()); + const Eigen::Vector4d ray_wor(invV*ray_eye); + const Eigen::Vector3d dir(ray_wor.topRows<3>().normalized()); + clbkRayScene(Ray3d(start, dir), action); + } + } break; + case GLFW_MOUSE_BUTTON_MIDDLE: { + if (action == GLFW_PRESS) { + inputType.set(INP_MOUSE_MIDDLE); + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + } else + if (action == GLFW_RELEASE) { + inputType.unset(INP_MOUSE_MIDDLE); + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } + } break; + case GLFW_MOUSE_BUTTON_RIGHT: { + if (action == GLFW_PRESS) { + inputType.set(INP_MOUSE_RIGHT); + } else + if (action == GLFW_RELEASE) { + inputType.unset(INP_MOUSE_RIGHT); + } + } + } +} +void Window::MouseButton(GLFWwindow* window, int button, int action, int mods) +{ + g_mapWindows[window]->MouseButton(button, action, mods); +} + +void Window::MouseMove(double xpos, double ypos) +{ + UpdateMousePosition(xpos, ypos); + if (inputType.isSet(INP_MOUSE_LEFT)) { + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + camera.Rotate(pos, prevPos); + } else + if (inputType.isSet(INP_MOUSE_MIDDLE)) { + camera.Translate(pos, prevPos); + } +} +void Window::MouseMove(GLFWwindow* window, double xpos, double ypos) +{ + g_mapWindows[window]->MouseMove(xpos, ypos); +} + +void Window::Scroll(double /*xoffset*/, double yoffset) +{ + camera.dist *= (yoffset>0 ? POW(1.11,yoffset) : POW(0.9,-yoffset)); +} +void Window::Scroll(GLFWwindow* window, double xoffset, double yoffset) +{ + g_mapWindows[window]->Scroll(xoffset, yoffset); +} + +void Window::Drop(int count, const char** paths) +{ + if (clbkOpenScene && count > 0) { + SetVisible(false); + String fileName(paths[0]); + Util::ensureUnifySlash(fileName); + if (count > 1) { + String geometryFileName(paths[1]); + Util::ensureUnifySlash(geometryFileName); + clbkOpenScene(fileName, geometryFileName); + } else { + clbkOpenScene(fileName, NULL); + } + SetVisible(true); + } +} +void Window::Drop(GLFWwindow* window, int count, const char** paths) +{ + g_mapWindows[window]->Drop(count, paths); +} + +bool Window::IsShiftKeyPressed() const +{ + return + glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS || + glfwGetKey(window, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS; +} +bool Window::IsCtrlKeyPressed() const +{ + return + glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS || + glfwGetKey(window, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS; +} +bool Window::IsAltKeyPressed() const +{ + return + glfwGetKey(window, GLFW_KEY_LEFT_ALT) == GLFW_PRESS || + glfwGetKey(window, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS; +} +/*----------------------------------------------------------------*/ diff --git a/apps/Viewer/Window.h b/apps/Viewer/Window.h new file mode 100644 index 0000000..ad9a957 --- /dev/null +++ b/apps/Viewer/Window.h @@ -0,0 +1,167 @@ +/* + * Window.h + * + * Copyright (c) 2014-2015 SEACAVE + * + * Author(s): + * + * cDc + * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * Additional Terms: + * + * You are required to preserve legal notices and author attributions in + * that material or in the Appropriate Legal Notices displayed by works + * containing it. + */ + +#ifndef _VIEWER_WINDOW_H_ +#define _VIEWER_WINDOW_H_ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Camera.h" +#include "Image.h" + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace VIEWER { + +class Window +{ +public: + GLFWwindow* window; // window handle + Camera camera; // current camera + Eigen::Vector2d pos, prevPos; // current and previous mouse position (normalized) + Eigen::Matrix4d transform; // view matrix corresponding to the currently selected image + cv::Size size; // resolution in pixels, sometimes not equal to window resolution, ex. on Retina display + double sizeScale; // window/screen resolution scale + + enum INPUT : unsigned { + INP_NA = 0, + INP_MOUSE_LEFT = (1 << 0), + INP_MOUSE_MIDDLE = (1 << 1), + INP_MOUSE_RIGHT = (1 << 2), + }; + Flags inputType; + + enum SPARSE { + SPR_NONE = 0, + SPR_POINTS = (1 << 0), + SPR_LINES = (1 << 1), + SPR_ALL = SPR_POINTS|SPR_LINES + }; + SPARSE sparseType; + unsigned minViews; + float pointSize; + float cameraBlend; + bool bRenderCameras; + bool bRenderCameraTrajectory; + bool bRenderImageVisibility; + bool bRenderViews; + bool bRenderSolid; + bool bRenderTexture; + bool bRenderBounds; + + enum SELECTION { + SEL_NA = 0, + SEL_POINT, + SEL_TRIANGLE + }; + SELECTION selectionType; + Point3f selectionPoints[4]; + double selectionTimeClick, selectionTime; + IDX selectionIdx; + + typedef DELEGATE ClbkOpenScene; + ClbkOpenScene clbkOpenScene; + typedef DELEGATE ClbkSaveScene; + ClbkSaveScene clbkSaveScene; + typedef DELEGATE ClbkExportScene; + ClbkExportScene clbkExportScene; + typedef DELEGATE ClbkCenterScene; + ClbkCenterScene clbkCenterScene; + typedef DELEGATE ClbkRayScene; + ClbkRayScene clbkRayScene; + typedef DELEGATE ClbkCompilePointCloud; + ClbkCompilePointCloud clbkCompilePointCloud; + typedef DELEGATE ClbkCompileMesh; + ClbkCompileMesh clbkCompileMesh; + typedef DELEGATE ClbkCompileBounds; + ClbkCompileBounds clbkCompileBounds; + typedef DELEGATE ClbkTogleSceneBox; + ClbkTogleSceneBox clbkTogleSceneBox; + typedef DELEGATE ClbkCropToBounds; + ClbkCropToBounds clbkCropToBounds; + + typedef std::unordered_map WindowsMap; + static WindowsMap g_mapWindows; + +public: + Window(); + ~Window(); + + void Release(); + void ReleaseClbk(); + inline bool IsValid() const { return window != NULL; } + + inline GLFWwindow* GetWindow() { return window; } + + bool Init(const cv::Size&, LPCTSTR name); + void SetCamera(const Camera&); + void SetName(LPCTSTR); + void SetVisible(bool); + bool IsVisible() const; + void Reset(SPARSE sparseType=SPR_ALL, unsigned minViews=2); + + void CenterCamera(const Point3&); + + void UpdateView(const ImageArr&, const MVS::ImageArr&); + void UpdateView(const Eigen::Matrix3d& R, const Eigen::Vector3d& t); + void UpdateMousePosition(double xpos, double ypos); + + void GetFrame(Image8U3&) const; + + cv::Size GetSize() const; + void Resize(const cv::Size&); + static void Resize(GLFWwindow* window, int width, int height); + void Key(int k, int scancode, int action, int mod); + static void Key(GLFWwindow* window, int k, int scancode, int action, int mod); + void MouseButton(int button, int action, int mods); + static void MouseButton(GLFWwindow* window, int button, int action, int mods); + void MouseMove(double xpos, double ypos); + static void MouseMove(GLFWwindow* window, double xpos, double ypos); + void Scroll(double xoffset, double yoffset); + static void Scroll(GLFWwindow* window, double xoffset, double yoffset); + void Drop(int count, const char** paths); + static void Drop(GLFWwindow* window, int count, const char** paths); + +protected: + bool IsShiftKeyPressed() const; + bool IsCtrlKeyPressed() const; + bool IsAltKeyPressed() const; +}; +/*----------------------------------------------------------------*/ + +} // namespace VIEWER + +#endif // _VIEWER_WINDOW_H_ diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt new file mode 100644 index 0000000..3b6adb2 --- /dev/null +++ b/libs/CMakeLists.txt @@ -0,0 +1,8 @@ +# Add libraries +ADD_SUBDIRECTORY(Common) +ADD_SUBDIRECTORY(Math) +ADD_SUBDIRECTORY(IO) +ADD_SUBDIRECTORY(MVS) + +# Install +INSTALL(FILES "MVS.h" DESTINATION "${INSTALL_INCLUDE_DIR}") diff --git a/libs/Common/AABB.h b/libs/Common/AABB.h new file mode 100644 index 0000000..da291f3 --- /dev/null +++ b/libs/Common/AABB.h @@ -0,0 +1,121 @@ +//////////////////////////////////////////////////////////////////// +// AABB.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_AABB_H__ +#define __SEACAVE_AABB_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +// Basic axis-aligned bounding-box class +template +class TAABB +{ + STATIC_ASSERT(DIMS > 0 && DIMS <= 3); + +public: + typedef TYPE Type; + typedef Eigen::Matrix POINT; + typedef Eigen::Matrix MATRIX; + enum { numChildren = (2<<(DIMS-1)) }; + enum { numCorners = (DIMS==1 ? 2 : (DIMS==2 ? 4 : 8)) }; // 2^DIMS + enum { numScalar = (2*DIMS) }; + + POINT ptMin, ptMax; // box extreme points + + //--------------------------------------- + + inline TAABB() {} + inline TAABB(bool); + inline TAABB(const POINT& _pt); + inline TAABB(const POINT& _ptMin, const POINT& _ptMax); + inline TAABB(const POINT& center, const TYPE& radius); + template + inline TAABB(const TPoint* pts, size_t n); + template + inline TAABB(const TAABB&); + + inline void Reset(); + inline void Set(const POINT& _pt); + inline void Set(const POINT& _ptMin, const POINT& _ptMax); + inline void Set(const POINT& center, const TYPE& radius); + template + inline void Set(const TPoint* pts, size_t n); + + inline bool IsEmpty() const; + + inline TAABB& Enlarge(TYPE); + inline TAABB& EnlargePercent(TYPE); + + void InsertFull(const POINT&); + void Insert(const POINT&); + void Insert(const TAABB&); + void BoundBy(const TAABB&); + + inline void Translate(const POINT&); + inline void Transform(const MATRIX&); + + inline POINT GetCenter() const; + inline void GetCenter(POINT&) const; + + inline POINT GetSize() const; + inline void GetSize(POINT&) const; + + inline void GetCorner(BYTE i, POINT&) const; + inline POINT GetCorner(BYTE i) const; + inline void GetCorners(POINT pts[numCorners]) const; + + bool Intersects(const TAABB&) const; + bool IntersectsComplete(const TAABB&, TYPE) const; + bool IntersectsComplete(const TAABB&) const; + bool Intersects(const POINT&) const; + bool IntersectsComplete(const POINT&, TYPE) const; + bool IntersectsComplete(const POINT&) const; + + unsigned SplitBy(TYPE, int, TAABB [2]) const; + unsigned SplitBy(const POINT&, TAABB [numChildren], unsigned&) const; + + inline TYPE& operator [] (BYTE i) { ASSERT(i> (std::istream& st, TAABB& obb) { + st >> obb.ptMin; + st >> obb.ptMax; + return st; + } + + #ifdef _USE_BOOST + // implement BOOST serialization + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & ptMin; + ar & ptMax; + } + #endif +}; // class TAABB +/*----------------------------------------------------------------*/ + + +#include "AABB.inl" +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_AABB_H__ diff --git a/libs/Common/AABB.inl b/libs/Common/AABB.inl new file mode 100644 index 0000000..b645ef1 --- /dev/null +++ b/libs/Common/AABB.inl @@ -0,0 +1,419 @@ +//////////////////////////////////////////////////////////////////// +// AABB.inl +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +template +inline TAABB::TAABB(bool) + : + ptMin(POINT::Constant(std::numeric_limits::max())), + ptMax(POINT::Constant(std::numeric_limits::lowest())) +{ +} +template +inline TAABB::TAABB(const POINT& _pt) + : + ptMin(_pt), ptMax(_pt) +{ +} +template +inline TAABB::TAABB(const POINT& _ptMin, const POINT& _ptMax) + : + ptMin(_ptMin), ptMax(_ptMax) +{ +} +template +inline TAABB::TAABB(const POINT& center, const TYPE& radius) + : + ptMin(center-POINT::Constant(radius)), ptMax(center+POINT::Constant(radius)) +{ +} +template +template +inline TAABB::TAABB(const TPoint* pts, size_t n) +{ + Set(pts, n); +} // constructor +template +template +inline TAABB::TAABB(const TAABB& rhs) + : + ptMin(rhs.ptMin.template cast()), ptMax(rhs.ptMax.template cast()) +{ +} // copy constructor +/*----------------------------------------------------------------*/ + + +template +inline void TAABB::Reset() +{ + ptMin = POINT::Constant(std::numeric_limits::max()); + ptMax = POINT::Constant(std::numeric_limits::lowest()); +} +template +inline void TAABB::Set(const POINT& _pt) +{ + ptMin = ptMax = _pt; +} +template +inline void TAABB::Set(const POINT& _ptMin, const POINT& _ptMax) +{ + ptMin = _ptMin; + ptMax = _ptMax; +} +template +inline void TAABB::Set(const POINT& center, const TYPE& radius) +{ + ptMin = center-POINT::Constant(radius); + ptMax = center+POINT::Constant(radius); +} +template +template +inline void TAABB::Set(const TPoint* pts, size_t n) +{ + ASSERT(n > 0); + ptMin = ptMax = pts[0]; + for (size_t i=1; i +inline bool TAABB::IsEmpty() const +{ + for (int i=0; i= ptMax[i]) + return true; + return false; +} // IsEmpty +/*----------------------------------------------------------------*/ + + +template +inline TAABB& TAABB::Enlarge(TYPE x) +{ + ptMin.array() -= x; + ptMax.array() += x; + return *this; +} +template +inline TAABB& TAABB::EnlargePercent(TYPE x) +{ + const POINT ptSizeDelta(GetSize() * (x - TYPE(1)) / TYPE(2)); + ptMin -= ptSizeDelta; + ptMax += ptSizeDelta; + return *this; +} // Enlarge +/*----------------------------------------------------------------*/ + + +template +inline typename TAABB::POINT TAABB::GetCenter() const +{ + return (ptMax + ptMin) * TYPE(0.5); +} +template +inline void TAABB::GetCenter(POINT& ptCenter) const +{ + ptCenter = (ptMax + ptMin) * TYPE(0.5); +} // GetCenter +/*----------------------------------------------------------------*/ + + +template +inline typename TAABB::POINT TAABB::GetSize() const +{ + return ptMax - ptMin; +} +template +inline void TAABB::GetSize(POINT& ptSize) const +{ + ptSize = ptMax - ptMin; +} // GetSize +/*----------------------------------------------------------------*/ + + +template +inline void TAABB::GetCorner(BYTE i, POINT& ptCorner) const +{ + ASSERT(i +inline typename TAABB::POINT TAABB::GetCorner(BYTE i) const +{ + POINT ptCorner; + GetCorner(i, ptCorner); + return ptCorner; +} // GetCorner +template +inline void TAABB::GetCorners(POINT pts[numCorners]) const +{ + for (BYTE i=0; i +inline void TAABB::Translate(const POINT& d) +{ + ptMin += d; + ptMax += d; +} +/*----------------------------------------------------------------*/ + + +// Update the box by the given transform. +template +inline void TAABB::Transform(const MATRIX& m) +{ + ptMin = m * ptMin; + ptMax = m * ptMax; +} +/*----------------------------------------------------------------*/ + + +// Update the box such that it contains the given point. +template +void TAABB::InsertFull(const POINT& pt) +{ + if (ptMin[0] > pt[0]) + ptMin[0] = pt[0]; + if (ptMax[0] < pt[0]) + ptMax[0] = pt[0]; + + if (DIMS > 1) { + if (ptMin[1] > pt[1]) + ptMin[1] = pt[1]; + if (ptMax[1] < pt[1]) + ptMax[1] = pt[1]; + } + + if (DIMS > 2) { + if (ptMin[2] > pt[2]) + ptMin[2] = pt[2]; + if (ptMax[2] < pt[2]) + ptMax[2] = pt[2]; + } +} +// same as above, but for the initialized case +template +void TAABB::Insert(const POINT& pt) +{ + if (ptMin[0] > pt[0]) + ptMin[0] = pt[0]; + else if (ptMax[0] < pt[0]) + ptMax[0] = pt[0]; + + if (DIMS > 1) { + if (ptMin[1] > pt[1]) + ptMin[1] = pt[1]; + else if (ptMax[1] < pt[1]) + ptMax[1] = pt[1]; + } + + if (DIMS > 2) { + if (ptMin[2] > pt[2]) + ptMin[2] = pt[2]; + else if (ptMax[2] < pt[2]) + ptMax[2] = pt[2]; + } +} +/*----------------------------------------------------------------*/ + +// Update the box such that it contains the given bounding box. +template +void TAABB::Insert(const TAABB& aabb) +{ + if (ptMin[0] > aabb.ptMin[0]) + ptMin[0] = aabb.ptMin[0]; + if (ptMax[0] < aabb.ptMax[0]) + ptMax[0] = aabb.ptMax[0]; + + if (DIMS > 1) { + if (ptMin[1] > aabb.ptMin[1]) + ptMin[1] = aabb.ptMin[1]; + if (ptMax[1] < aabb.ptMax[1]) + ptMax[1] = aabb.ptMax[1]; + } + + if (DIMS > 2) { + if (ptMin[2] > aabb.ptMin[2]) + ptMin[2] = aabb.ptMin[2]; + if (ptMax[2] < aabb.ptMax[2]) + ptMax[2] = aabb.ptMax[2]; + } +} +/*----------------------------------------------------------------*/ + +// Update the box such that it does not exceed the given bounding box. +template +void TAABB::BoundBy(const TAABB& aabb) +{ + if (ptMin[0] < aabb.ptMin[0]) + ptMin[0] = aabb.ptMin[0]; + if (ptMax[0] > aabb.ptMax[0]) + ptMax[0] = aabb.ptMax[0]; + + if (DIMS > 1) { + if (ptMin[1] < aabb.ptMin[1]) + ptMin[1] = aabb.ptMin[1]; + if (ptMax[1] > aabb.ptMax[1]) + ptMax[1] = aabb.ptMax[1]; + } + + if (DIMS > 2) { + if (ptMin[2] < aabb.ptMin[2]) + ptMin[2] = aabb.ptMin[2]; + if (ptMax[2] > aabb.ptMax[2]) + ptMax[2] = aabb.ptMax[2]; + } +} +/*----------------------------------------------------------------*/ + + +// intersection between two AABBs +template +bool TAABB::Intersects(const TAABB& aabb) const +{ + if (aabb.ptMax[0] < ptMin[0]) return false; + if (aabb.ptMin[0] >= ptMax[0]) return false; + if (DIMS > 1) { + if (aabb.ptMax[1] < ptMin[1]) return false; + if (aabb.ptMin[1] >= ptMax[1]) return false; + } + if (DIMS > 2) { + if (aabb.ptMax[2] < ptMin[2]) return false; + if (aabb.ptMin[2] >= ptMax[2]) return false; + } + return true; +} +template +bool TAABB::IntersectsComplete(const TAABB& aabb, TYPE tolerance) const +{ + if (aabb.ptMax[0]+tolerance < ptMin[0]) return false; + if (aabb.ptMin[0] > ptMax[0]+tolerance) return false; + if (DIMS > 1) { + if (aabb.ptMax[1]+tolerance < ptMin[1]) return false; + if (aabb.ptMin[1] > ptMax[1]+tolerance) return false; + } + if (DIMS > 2) { + if (aabb.ptMax[2]+tolerance < ptMin[2]) return false; + if (aabb.ptMin[2] > ptMax[2]+tolerance) return false; + } + return true; +} +template +bool TAABB::IntersectsComplete(const TAABB& aabb) const +{ + return IntersectsComplete(aabb, ZEROTOLERANCE()); +} // Intersects(TAABB) +/*----------------------------------------------------------------*/ + +// does TAABB contain the given point +template +bool TAABB::Intersects(const POINT& pt) const +{ + if (pt[0] < ptMin[0]) return false; + if (pt[0] >= ptMax[0]) return false; + if (DIMS > 1) { + if (pt[1] < ptMin[1]) return false; + if (pt[1] >= ptMax[1]) return false; + } + if (DIMS > 2) { + if (pt[2] < ptMin[2]) return false; + if (pt[2] >= ptMax[2]) return false; + } + return true; +} +template +bool TAABB::IntersectsComplete(const POINT& pt, TYPE tolerance) const +{ + if (pt[0]+tolerance < ptMin[0]) return false; + if (pt[0] > ptMax[0]+tolerance) return false; + if (DIMS > 1) { + if (pt[1]+tolerance < ptMin[1]) return false; + if (pt[1] > ptMax[1]+tolerance) return false; + } + if (DIMS > 2) { + if (pt[2]+tolerance < ptMin[2]) return false; + if (pt[2] > ptMax[2]+tolerance) return false; + } + return true; +} +template +bool TAABB::IntersectsComplete(const POINT& pt) const +{ + return IntersectsComplete(pt, ZEROTOLERANCE()); +} // Intersects(point) +/*----------------------------------------------------------------*/ + + +// split this TAABB by the given axis +// returns number of children (0 or 2) +template +unsigned TAABB::SplitBy(TYPE d, int a, TAABB child[2]) const +{ + ASSERT(a < DIMS); + if (d >= ptMin[a] && d < ptMax[a]) { + TAABB& child0 = child[0]; + child0.Set(ptMin, ptMax); + TAABB& child1 = child[1]; + child1.Set(ptMin, ptMax); + child0.ptMax[a] = child1.ptMin[a] = d; + return 2; + } + return 0; +} // SplitBy(axis) +/*----------------------------------------------------------------*/ + +// split this TAABB by the given point +// returns number of children (0 - numScalar) +// idxFirst returns the index of the first aabb +template +unsigned TAABB::SplitBy(const POINT& pt, TAABB child[numChildren], unsigned& idxFirst) const +{ + idxFirst = 0; + unsigned size; + int a = 0; + do { + size = SplitBy(pt[a], a, child); + if (++a == DIMS) + return size; + } while (size == 0); + do { + unsigned n = 0; + for (unsigned b=0; b=n) return(0.0); + if (n-k +static inline void makelogcombi_n(size_t n, std::vector& l) +{ + l.resize(n+1); + for (size_t k = 0; k <= n; ++k) + l[k] = static_cast(logcombi(k,n)); +} + +/// tabulate logcombi(k,.) +template +static inline void makelogcombi_k(size_t nmax, std::vector& l) +{ + l.resize(nmax+1); + for (size_t n = 0; n <= nmax; ++n) + l[n] = static_cast(logcombi(k,n)); +} + +/// Distance and associated index +struct ErrorIndex { + double first; + size_t second; + inline ErrorIndex() {} + inline ErrorIndex(double _first, size_t _second) : first(_first), second(_second) {} + inline bool operator==(const ErrorIndex& r) const { return (first == r.first); } + inline bool operator <(const ErrorIndex& r) const { return (first < r.first); } +}; +#ifdef ACRANSAC_STD_VECTOR +typedef std::vector ErrorIndexArr; +#else +typedef CLISTDEF0(ErrorIndex) ErrorIndexArr; +#endif + +/// Find best NFA and its index wrt square error threshold in e. +template +static ErrorIndex bestNFA( + double logalpha0, + const ErrorIndexArr& e, + double loge0, + double maxThresholdSq, + const std::vector& logc_n, + const std::vector& logc_k, + double multError = 1.0) +{ + ErrorIndex bestIndex(DBL_MAX, NSAMPLES); + const size_t n = e.size(); + for (size_t k=NSAMPLES+1; k<=n && e[k-1].first<=maxThresholdSq; ++k) { + const double logalpha = logalpha0 + multError * log10(e[k-1].first+FLT_EPSILON); + const double NFA = loge0 + + logalpha * (double)(k-NSAMPLES) + + logc_n[k] + + logc_k[k]; + if (NFA < bestIndex.first) + bestIndex = ErrorIndex(NFA, k); + } + return bestIndex; +} + +/// Pick a random subset of the integers [0, total), in random order. +/// Note that this can behave badly if num_samples is close to total; runtime +/// could be unlimited! +/// +/// This uses a quadratic rejection strategy and should only be used for small +/// num_samples. +/// +/// \param NSAMPLES The number of samples to produce. +/// \param n The number of samples available. +/// \param samples num_samples of numbers in [0, total_samples) is placed +/// here on return. +struct UniformSampler { + template + inline void Sample( + size_t n, + size_t* samples) + { + ASSERT(NSAMPLES > 0); + size_t* const samplesBegin = samples; + size_t* const samplesEnd = samples + NSAMPLES; + *samples = size_t(RAND() % n); + while (++samples < samplesEnd) { + do { + *samples = size_t(RAND() % n); + } while (std::find(samplesBegin, samples, *samples) != samples); + } + } +}; +/// Same as above, but ensure that the first point is always included +struct UniformSamplerLockFirst { + template + inline void Sample( + size_t n, + size_t* samples) + { + ASSERT(NSAMPLES > 1); + size_t* const samplesBegin = samples; + size_t* const samplesEnd = samples + NSAMPLES; + *samples++ = 0; + do { + do { + *samples = size_t(RAND() % n); + } while (std::find(samplesBegin, samples, *samples) != samples); + } while (++samples < samplesEnd); + } +}; + +/// Pick a random sample: +/// get a (sorted) random sample of size NSAMPLES in [0:n-1] +/// \param NSAMPLES The number of samples to produce. +/// \param vec_index The possible data indices. +/// \param n The number of samples available. +/// \param samples The random sample of NSAMPLES indices (output). +struct UniformSamplerSorted { + template + inline void Sample( + #ifdef ACRANSAC_SAMPLE_INLIERS + const std::vector& vec_index, + #else + size_t n, + #endif + size_t* samples) + { + #ifdef ACRANSAC_SAMPLE_INLIERS + const size_t n = vec_index.size(); + #endif + for (size_t i=0; i>3)%(n-i), j; + for (j=0; j=samples[j]; ++j) + ++r; + size_t j0 = j; + for (j=i; j>j0; --j) + samples[j] = samples[j-1]; + samples[j0] = r; + } + #ifdef ACRANSAC_SAMPLE_INLIERS + for (size_t i=0; i + inline void Sample( + size_t n, + size_t* samples) + { + ASSERT(NSAMPLES > 1); + samples[0] = 0; + for (size_t i=1; i>3)%(n-i), j; + for (j=0; j=samples[j]; ++j) + ++r; + size_t j0 = j; + for (j=i; j>j0; --j) + samples[j] = samples[j-1]; + samples[j0] = r; + } + } +}; + +/// Returns the requested matrix rows +template +TMat ExtractRows(const TMat &A, const TRows &rows) { + TMat compressed((int)rows.size(), A.cols); + for (size_t i=0; i(rows.size()); ++i) + A.row(rows[i]).copyTo(compressed.row((int)i)); + return compressed; +} + +/// ACRANSAC routine (ErrorThreshold, NFA) +template +std::pair ACRANSAC( + Kernel& kernel, + Sampler& sampler, + std::vector& vec_inliers, + typename Kernel::Model& model, + double maxThreshold = DBL_MAX, + #ifndef ACRANSAC_SAMPLE_INLIERS + double confidence = 0.9999, + #endif + size_t nIter = 0) +{ + const size_t nData = kernel.NumSamples(); + if (nData < Kernel::MINIMUM_SAMPLES) { + vec_inliers.clear(); + return std::make_pair(0.0,0.0); + } + const double maxThresholdSq = SQUARE(maxThreshold); + std::vector vec_sample(Kernel::MINIMUM_SAMPLES); // sample indices + typename Kernel::Models vec_models; // up to max_models solutions + if (nData == Kernel::MINIMUM_SAMPLES) { + vec_inliers.resize(Kernel::MINIMUM_SAMPLES); + std::iota(vec_inliers.begin(), vec_inliers.end(), 0); + if (!kernel.Fit(vec_inliers, vec_models) || vec_models.size() != 1) { + vec_inliers.clear(); + return std::make_pair(0.0,0.0); + } + model = vec_models[0]; + return std::make_pair(maxThresholdSq,0.0); + } + + #ifdef ACRANSAC_SAMPLE_INLIERS + // Possible sampling indices (could change in the optimization phase) + std::vector vec_index(nData); + for (size_t i = 0; i < nData; ++i) + vec_index[i] = i; + #endif + + // Precompute log combi + const double loge0 = log10((double)Kernel::MAX_MODELS * (nData-Kernel::MINIMUM_SAMPLES)); + std::vector vec_logc_n, vec_logc_k; + makelogcombi_n(nData, vec_logc_n); + makelogcombi_k(nData, vec_logc_k); + + // Output parameters + double minNFA = DBL_MAX; + double errorSqMax = DBL_MAX; + + // Reserve 10% of iterations for focused sampling + if (nIter == 0) + nIter = MINF((size_t)4000, MAXF((size_t)300, nData*3/2)); + const size_t nMinIter = nIter*8/100; + + // Main estimation loop + ErrorIndexArr vec_residuals(nData); // [residual,index] + for (size_t iter=0; iter( + #ifdef ACRANSAC_SAMPLE_INLIERS + vec_index, + #else + nData, + #endif + &vec_sample[0] + ); + + if (!kernel.Fit(vec_sample, vec_models)) + continue; + + // Evaluate models + #ifdef ACRANSAC_SAMPLE_INLIERS + bool better = false; + #else + const size_t nTrialsRound = nIter; + #endif + for (typename Kernel::Models::const_iterator itModel=vec_models.cbegin(); itModel!=vec_models.cend(); ++itModel) { + // Residuals computation and ordering + kernel.EvaluateModel(*itModel); + for (size_t i = 0; i < nData; ++i) + vec_residuals[i] = ErrorIndex(kernel.Error(i), i); + std::sort(vec_residuals.begin(), vec_residuals.end()); + + // Most meaningful discrimination inliers/outliers + ErrorIndex best = bestNFA( + kernel.logalpha0(), + vec_residuals, + loge0, + maxThresholdSq, + vec_logc_n, + vec_logc_k, + kernel.multError()); + + if (best.first < minNFA /*&& vec_residuals[best.second-1].first < errorSqMax*/) { + // A better model was found + minNFA = best.first; + vec_inliers.resize(best.second); + for (size_t i=0; inData/3) { + // Optimization: draw samples among best set of inliers so far + vec_index = vec_inliers; + } + #endif + } + + if (minNFA >= 0) + vec_inliers.clear(); + + return std::make_pair(errorSqMax, minNFA); +} +/*----------------------------------------------------------------*/ + + +/// RANSAC routine (standard robust fitter, based on a known threshold) +template +void RANSAC( + Kernel& kernel, + Sampler& sampler, + std::vector& vec_inliers, + typename Kernel::Model& model, + double threshold, + #ifndef ACRANSAC_SAMPLE_INLIERS + double confidence = 0.9999, + #endif + size_t nIter = 0) +{ + vec_inliers.clear(); + const size_t nData = kernel.NumSamples(); + if (nData < Kernel::MINIMUM_SAMPLES) + return; + const double thresholdSq = SQUARE(threshold); + std::vector vec_sample(Kernel::MINIMUM_SAMPLES); // sample indices + typename Kernel::Models vec_models; // up to max_models solutions + + #ifdef ACRANSAC_SAMPLE_INLIERS + // Possible sampling indices (could change in the optimization phase) + std::vector vec_index(nData); + for (size_t i = 0; i < nData; ++i) + vec_index[i] = i; + #endif + + // Reserve 10% of iterations for focused sampling + if (nIter == 0) + nIter = MINF((size_t)4000, MAXF((size_t)300, nData*3/2)); + const size_t nMinIter = nIter*8/100; + + // Main estimation loop + #ifdef _USE_MSAC + double best_score = thresholdSq*nData; + #endif + std::vector vec_inliers_tmp; vec_inliers_tmp.reserve(nData); vec_inliers.reserve(nData); + for (size_t iter=0; iter( + #ifdef ACRANSAC_SAMPLE_INLIERS + vec_index, + #else + nData, + #endif + &vec_sample[0] + ); + + if (!kernel.Fit(vec_sample, vec_models)) + continue; + + // Evaluate models + #ifdef ACRANSAC_SAMPLE_INLIERS + bool better = false; + #else + const size_t nTrialsRound = nIter; + #endif + for (typename Kernel::Models::const_iterator itModel=vec_models.cbegin(); itModel!=vec_models.cend(); ++itModel) { + // Residuals computation and ordering + #ifdef _USE_MSAC + double score = 0; + #endif + vec_inliers_tmp.clear(); + kernel.EvaluateModel(*itModel); + for (size_t i = 0; i < nData; ++i) { + const double errSq = kernel.Error(i); + if (errSq <= thresholdSq) { + #ifdef _USE_MSAC + score += errSq; + #endif + vec_inliers_tmp.push_back(i); + } else { + #ifdef _USE_MSAC + score += thresholdSq; + #endif + } + #ifdef _USE_MSAC + if (score >= best_score) + break; + #endif + } + // Store best model + #ifdef _USE_MSAC + if (score < best_score) { + #else + if (vec_inliers_tmp.size() > vec_inliers.size()) { + #endif + // A better model was found + model = *itModel; + vec_inliers.swap(vec_inliers_tmp); + if (vec_inliers.size() == nData) { + nIter = 0; // force loop exit + break; + } + #ifdef _USE_MSAC + best_score = score; + DEBUG_LEVEL(4, "\titer=%3u inliers=%u score=%g", iter, vec_inliers.size(), best_score); + #else + DEBUG_LEVEL(4, "\titer=%3u inliers=%u", iter, vec_inliers.size()); + #endif + #ifdef ACRANSAC_SAMPLE_INLIERS + better = true; + #else + #ifdef _USE_MSAC + nIter = cvRANSACUpdateNumIters(confidence, (double)score/((double)nData*thresholdSq), unsigned(Kernel::MINIMUM_SAMPLES), (unsigned)nTrialsRound); + #else + nIter = cvRANSACUpdateNumIters(confidence, (double)(nData-vec_inliers.size())/nData, unsigned(Kernel::MINIMUM_SAMPLES), (unsigned)nTrialsRound); + #endif + #endif + } + } + vec_models.clear(); + + #ifdef ACRANSAC_SAMPLE_INLIERS + if (better && vec_inliers.size()>nData/3) { + // Optimization: draw samples among best set of inliers so far + vec_index = vec_inliers; + } + #endif + } + + if (vec_inliers.size() < Kernel::MINIMUM_SAMPLES) + vec_inliers.clear(); +} +/*----------------------------------------------------------------*/ + + +/// Two view Kernel adaptator for the A contrario model estimator +/// Handle data normalization and compute the corresponding logalpha 0 +/// that depends of the error model (point to line, or point to point) +/// This kernel adaptor is working for affine, homography, fundamental matrix +/// estimation. +template +class ACKernelAdaptor +{ +public: + typedef SOLVER Solver; + typedef typename SOLVER::Model Model; + typedef typename SOLVER::Models Models; + + enum { MINIMUM_SAMPLES = Solver::MINIMUM_SAMPLES }; + enum { MAX_MODELS = Solver::MAX_MODELS }; + + ACKernelAdaptor( + const DMatrix32F &x1, float w1, float h1, + const DMatrix32F &x2, float w2, float h2, + bool bPointToLine = true) + : x1_(x1), x2_(x2), + bPointToLine_(bPointToLine) + { + ASSERT(2 == x1_.cols); + ASSERT(x1_.rows == x2_.rows); + ASSERT(x1_.cols == x2_.cols); + ASSERT(NumSamples() >= MINIMUM_SAMPLES); + + // LogAlpha0 is used to make error data scale invariant + const float s2 = 1.0f; + if (bPointToLine) { + // Ratio of containing diagonal image rectangle over image area + const float D = sqrt(w2*w2 + h2*h2); // diameter + const float A = w2*h2; // area + logalpha0_ = log10(2.0f*D/A/s2); + } + else { + // ratio of area : unit circle over image area + logalpha0_ = log10(M_PI/(w2*h2)/(s2*s2)); + } + } + + inline bool Fit(const std::vector& samples, Models& models) const { + const DMatrix32F x1(ExtractRows(x1_, samples)); + if (CheckCollinearity(x1.ptr(), x1.rows)) + return false; + const DMatrix32F x2(ExtractRows(x2_, samples)); + if (CheckCollinearity(x2.ptr(), x2.rows)) + return false; + Solver::Solve(x1, x2, models); + return true; + } + + inline void EvaluateModel(const Model& model) { + model2evaluate = model; + } + + inline double Error(size_t sample) const { + return Solver::Error(model2evaluate, *x1_.ptr((int)sample), *x2_.ptr((int)sample)); + } + + inline size_t NumSamples() const { return static_cast(x1_.rows); } + inline double logalpha0() const { return logalpha0_; } + inline double multError() const { return (bPointToLine_ ? 0.5 : 1.0); } + +protected: + DMatrix32F x1_, x2_; // Normalized input data + double logalpha0_; // Alpha0 is used to make the error adaptive to the image size + bool bPointToLine_;// Store if error model is pointToLine or point to point + Model model2evaluate; // current model to be evaluated +}; + +/// Pose/Resection Kernel adaptator for the A contrario model estimator +template +class ACKernelAdaptorResection +{ +public: + typedef SOLVER Solver; + typedef typename SOLVER::Model Model; + typedef typename SOLVER::Models Models; + + enum { MINIMUM_SAMPLES = Solver::MINIMUM_SAMPLES }; + enum { MAX_MODELS = Solver::MAX_MODELS }; + + ACKernelAdaptorResection(const DMatrix32F &x2d, const DMatrix &x3D) + : x2d_(x2d), x3D_(x3D), + logalpha0_(log10(M_PI)) + { + ASSERT(2 == x2d_.cols); + ASSERT(3 == x3D_.cols); + ASSERT(x2d_.rows == x3D_.rows); + ASSERT(NumSamples() >= MINIMUM_SAMPLES); + } + + inline bool Fit(const std::vector& samples, Models& models) const { + const DMatrix32F x1(ExtractRows(x2d_, samples)); + if (CheckCollinearity(x1.ptr(), x1.rows)) + return false; + const DMatrix x2(ExtractRows(x3D_, samples)); + Solver::Solve(x1, x2, models); + return true; + } + + inline void EvaluateModel(const Model &model) { + model2evaluate = model; + } + + inline double Error(size_t sample) const { + return Solver::Error(model2evaluate, *x2d_.ptr((int)sample), *x3D_.ptr((int)sample)); + } + + inline size_t NumSamples() const { return x2d_.rows; } + inline double logalpha0() const { return logalpha0_; } + inline double multError() const { return 1.0; } // point to point error + +protected: + DMatrix32F x2d_; + DMatrix x3D_; + double logalpha0_; // Alpha0 is used to make the error adaptive to the image size + Model model2evaluate; // current model to be evaluated +}; + +/// Essential matrix Kernel adaptator for the A contrario model estimator +template +class ACKernelAdaptorEssential +{ +public: + typedef SOLVER Solver; + typedef typename SOLVER::Model Model; + typedef typename SOLVER::Models Models; + + enum { MINIMUM_SAMPLES = Solver::MINIMUM_SAMPLES }; + enum { MAX_MODELS = Solver::MAX_MODELS }; + + ACKernelAdaptorEssential( + const DMatrix32F &x1, float w1, float h1, const Matrix3x3f& invK1, + const DMatrix32F &x2, float w2, float h2, const Matrix3x3f& invK2) + : solver(invK1, invK2), + x1_(x1), x2_(x2) + { + ASSERT(2 == x1_.cols); + ASSERT(x1_.rows == x2_.rows); + ASSERT(x1_.cols == x2_.cols); + ASSERT(NumSamples() >= MINIMUM_SAMPLES); + + //Point to line probability (line is the epipolar line) + const float D = sqrt(w2*w2 + h2*h2); // diameter + const float A = w2*h2+1.f; // area + logalpha0_ = log10(2.0f*D/A*0.5f); + } + + inline bool Fit(const std::vector& samples, Models& models) const { + const DMatrix32F x1(ExtractRows(x1_, samples)); + if (CheckCollinearity(x1.ptr(), x1.rows)) + return false; + const DMatrix32F x2(ExtractRows(x2_, samples)); + if (CheckCollinearity(x2.ptr(), x2.rows)) + return false; + solver.Solve(x1, x2, models); + return true; + } + + inline void EvaluateModel(const Model &model) { + solver.EvaluateModel(model); + } + + inline double Error(size_t sample) const { + return solver.Error(*x1_.ptr((int)sample), *x2_.ptr((int)sample)); + } + + inline size_t NumSamples() const { return x1_.rows; } + inline double logalpha0() const { return logalpha0_; } + inline double multError() const { return 0.5; } // point to line error + +protected: + Solver solver; + DMatrix32F x1_, x2_; // image point and camera plane point. + double logalpha0_; // Alpha0 is used to make the error adaptive to the image size +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_AUTO_ESTIMATOR_H__ diff --git a/libs/Common/AutoPtr.h b/libs/Common/AutoPtr.h new file mode 100644 index 0000000..375cca4 --- /dev/null +++ b/libs/Common/AutoPtr.h @@ -0,0 +1,275 @@ +//////////////////////////////////////////////////////////////////// +// AutoPtr.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_AUTOPTR_H__ +#define __SEACAVE_AUTOPTR_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +/************************************************************************************** + * CAutoPtr template + * --------------- + * simple smart pointer + **************************************************************************************/ + +struct AutoPtrMoveCopy { + template + static inline void Copy(TYPE*& ptrLeft, TYPE*& ptrRight) { + ptrLeft = ptrRight; + ptrRight = NULL; + } +}; + +struct AutoPtrDeepCopy { + template + static inline void Copy(TYPE*& ptrLeft, TYPE*& ptrRight) { + ptrLeft = (ptrRight != NULL ? new TYPE(*ptrRight) : (TYPE*)NULL); + } +}; + +template +class CAutoPtr +{ +private: + typedef TYPE Type; + typedef TYPE* TypePtr; + +public: + inline CAutoPtr() : m_pointer(NULL) + { // construct with NULL pointer + } + + inline explicit CAutoPtr(TypePtr _Ptr) : m_pointer(_Ptr) + { // construct from object pointer + } + + inline CAutoPtr(CAutoPtr& _Right) + { // copy-construct by assuming pointer from _Right CAutoPtr + CopyPolicy::Copy(m_pointer, _Right.m_pointer); + } + + inline ~CAutoPtr() + { // destroy the object + delete m_pointer; + } + + void Swap(CAutoPtr& _Right) + { // swap compatible _Right (assume pointer) + const TypePtr tmp(m_pointer); + m_pointer = _Right.m_pointer; + _Right.m_pointer = tmp; + } + + CAutoPtr& operator=(CAutoPtr& _Right) + { // assign compatible _Right (assume pointer) + if (this != &_Right) { + delete m_pointer; + CopyPolicy::Copy(m_pointer, _Right.m_pointer); + } + return (*this); + } + + CAutoPtr& operator=(TypePtr _Ptr) + { // assign compatible _Right (assume pointer) + if (m_pointer != _Ptr) { + delete m_pointer; + m_pointer = _Ptr; + } + return (*this); + } + + inline Type& operator*() const + { // return designated value + ASSERT(m_pointer); + return (*m_pointer); + } + + inline Type* operator->() const + { // return pointer to class object + ASSERT(m_pointer); + return m_pointer; + } + + inline operator TypePtr() const + { // return pointer to class object + return m_pointer; + } + + inline operator TypePtr&() + { // return reference to class object + return m_pointer; + } + + inline bool operator==(const TypePtr _Right) const + { // return pointer to class object + return (m_pointer == _Right); + } + + inline bool operator!=(const TypePtr _Right) const + { // return pointer to class object + return (m_pointer != _Right); + } + + inline void Release() + { // release pointer + delete m_pointer; + m_pointer = NULL; + } + + inline void Reset(TypePtr _Ptr = NULL) + { // reset pointer + m_pointer = _Ptr; + } + +protected: + TypePtr m_pointer; // the wrapped object pointer + + #ifdef _USE_BOOST + // implement BOOST serialization + friend class boost::serialization::access; + template + void save(Archive& ar, const unsigned int /*version*/) const + { + ar & m_pointer; + } + template + void load(Archive& ar, const unsigned int /*version*/) + { + TypePtr newPointer; + ar & newPointer; + operator=(newPointer); + } + BOOST_SERIALIZATION_SPLIT_MEMBER() + #endif +}; + + +template +class CAutoPtrArr +{ +private: + typedef TYPE Type; + typedef TYPE* TypePtr; + +public: + inline CAutoPtrArr() : m_pointer(NULL) + { // construct with NULL pointer + } + + inline explicit CAutoPtrArr(TypePtr _Ptr) : m_pointer(_Ptr) + { // construct from object pointer + } + + inline CAutoPtrArr(CAutoPtrArr& _Right) : m_pointer(_Right.m_pointer) + { // construct by assuming pointer from _Right CAutoPtrArr + _Right.m_pointer = NULL; + } + + inline ~CAutoPtrArr() + { // destroy the object + delete[] m_pointer; + } + + CAutoPtrArr& operator=(CAutoPtrArr& _Right) + { // assign compatible _Right (assume pointer) + if (this != &_Right) + { + delete[] m_pointer; + m_pointer = _Right.m_pointer; + _Right.m_pointer = NULL; + } + return (*this); + } + + CAutoPtrArr& operator=(TypePtr _Ptr) + { // assign compatible _Right (assume pointer) + if (m_pointer != _Ptr) + { + delete[] m_pointer; + m_pointer = _Ptr; + } + return (*this); + } + + inline Type& operator*() const + { // return designated value + ASSERT(m_pointer); + return (*m_pointer); + } + + inline Type* operator->() const + { // return pointer to class object + ASSERT(m_pointer); + return m_pointer; + } + + inline operator TypePtr() const + { // return pointer to class object + return m_pointer; + } + + inline operator TypePtr&() + { // return reference to class object + return m_pointer; + } + + inline bool operator==(const TypePtr _Right) const + { // return pointer to class object + return (m_pointer == _Right); + } + + inline bool operator!=(const TypePtr _Right) const + { // return pointer to class object + return (m_pointer != _Right); + } + + inline void Release() + { // release pointer + delete[] m_pointer; + m_pointer = NULL; + } + + inline void Reset(TypePtr _Ptr = NULL) + { // reset pointer + m_pointer = _Ptr; + } + +protected: + TypePtr m_pointer; // the wrapped object pointer + + #ifdef _USE_BOOST + // implement BOOST serialization + friend class boost::serialization::access; + template + void save(Archive& ar, const unsigned int /*version*/) const + { + ar & m_pointer; + } + template + void load(Archive& ar, const unsigned int /*version*/) + { + TypePtr newPointer; + ar & newPointer; + operator=(newPointer); + } + BOOST_SERIALIZATION_SPLIT_MEMBER() + #endif +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_AUTOPTR_H__ diff --git a/libs/Common/CMakeLists.txt b/libs/Common/CMakeLists.txt new file mode 100644 index 0000000..18899af --- /dev/null +++ b/libs/Common/CMakeLists.txt @@ -0,0 +1,25 @@ +# List sources files +FILE(GLOB LIBRARY_FILES_C "*.cpp") +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_library_with_type(Common "Libs" "" "${cxx_default}" + ${LIBRARY_FILES_C} ${LIBRARY_FILES_H} +) + +# Manually set Common.h as the precompiled header +IF(CMAKE_VERSION VERSION_GREATER_EQUAL 3.16.0) + TARGET_PRECOMPILE_HEADERS(Common PRIVATE "Common.h") +endif() + +# Link its dependencies +TARGET_LINK_LIBRARIES(Common ${Boost_LIBRARIES} ${OpenCV_LIBS}) + +# Install +SET_TARGET_PROPERTIES(Common PROPERTIES + PUBLIC_HEADER "${LIBRARY_FILES_H}") +INSTALL(TARGETS Common + EXPORT OpenMVSTargets + LIBRARY DESTINATION "${INSTALL_LIB_DIR}" + ARCHIVE DESTINATION "${INSTALL_LIB_DIR}" + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" + PUBLIC_HEADER DESTINATION "${INSTALL_INCLUDE_DIR}/Common") diff --git a/libs/Common/Common.cpp b/libs/Common/Common.cpp new file mode 100644 index 0000000..54ccc8a --- /dev/null +++ b/libs/Common/Common.cpp @@ -0,0 +1,47 @@ +//////////////////////////////////////////////////////////////////// +// Common.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +// Source file that includes just the standard includes +// Common.pch will be the pre-compiled header +// Common.obj will contain the pre-compiled type information + +#include "Common.h" + +namespace SEACAVE { +#if TD_VERBOSE == TD_VERBOSE_ON +int g_nVerbosityLevel(2); +#endif +#if TD_VERBOSE == TD_VERBOSE_DEBUG +int g_nVerbosityLevel(3); +#endif + +String g_strWorkingFolder; +String g_strWorkingFolderFull; +} // namespace SEACAVE + +#ifdef _USE_BOOST +#ifdef BOOST_NO_EXCEPTIONS +#if (BOOST_VERSION / 100000) > 1 || (BOOST_VERSION / 100 % 1000) > 72 +#include +#endif +namespace boost { + void throw_exception(std::exception const & e) { + VERBOSE("exception thrown: %s", e.what()); + ASSERT("boost exception thrown" == NULL); + exit(EXIT_FAILURE); + } + #if (BOOST_VERSION / 100000) > 1 || (BOOST_VERSION / 100 % 1000) > 72 + void throw_exception(std::exception const & e, boost::source_location const & loc) { + std::ostringstream ostr; ostr << loc; + VERBOSE("exception thrown at %s: %s", ostr.str().c_str(), e.what()); + ASSERT("boost exception thrown" == NULL); + exit(EXIT_FAILURE); + } + #endif +} // namespace boost +#endif +#endif diff --git a/libs/Common/Common.h b/libs/Common/Common.h new file mode 100644 index 0000000..eba19ae --- /dev/null +++ b/libs/Common/Common.h @@ -0,0 +1,308 @@ +//////////////////////////////////////////////////////////////////// +// Common.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef _COMMON_COMMON_H_ +#define _COMMON_COMMON_H_ + + +// D E F I N E S /////////////////////////////////////////////////// + +#include "Config.h" + +// macros controlling the verbosity +#define TD_VERBOSE_OFF 0 +#define TD_VERBOSE_ON 1 +#define TD_VERBOSE_DEBUG 2 +#ifndef TD_VERBOSE +#ifdef _RELEASE +#define TD_VERBOSE TD_VERBOSE_ON +#else +#define TD_VERBOSE TD_VERBOSE_DEBUG +#endif +#endif + +#if TD_VERBOSE == TD_VERBOSE_OFF +#define VERBOSE LOG +#define DEBUG_LEVEL(n,...) +#else +#ifndef VERBOSITY_LEVEL +namespace SEACAVE { extern int g_nVerbosityLevel; } +#define VERBOSITY_LEVEL g_nVerbosityLevel +#endif +#define VERBOSE LOG +#define DEBUG_LEVEL(n,...) { if (n < VERBOSITY_LEVEL) VERBOSE(__VA_ARGS__); } +#endif +#define DEBUG(...) DEBUG_LEVEL(0, __VA_ARGS__) +#define DEBUG_EXTRA(...) DEBUG_LEVEL(1, __VA_ARGS__) +#define DEBUG_ULTIMATE(...) DEBUG_LEVEL(2, __VA_ARGS__) + + +// macros that simplify timing tasks +#define TD_TIMER_OFF 0 +#define TD_TIMER_ON 1 +#ifndef TD_TIMER +#define TD_TIMER TD_TIMER_ON +#endif + +#if TD_TIMER == TD_TIMER_OFF +#define TD_TIMER_START() +#define TD_TIMER_UPDATE() +#define TD_TIMER_GET() 0 +#define TD_TIMER_GET_INT() 0 +#define TD_TIMER_GET_FMT() String() +#define TD_TIMER_STARTD() +#define TD_TIMER_UPDATED() +#endif +#if TD_TIMER == TD_TIMER_ON +#define TD_TIMER_START() TIMER_START() +#define TD_TIMER_UPDATE() TIMER_UPDATE() +#define TD_TIMER_GET() TIMER_GET() +#define TD_TIMER_GET_INT() TIMER_GET_INT() +#define TD_TIMER_GET_FMT() TIMER_GET_FORMAT() +#if TD_VERBOSE == TD_VERBOSE_OFF +#define TD_TIMER_STARTD() +#define TD_TIMER_UPDATED() +#else +#define TD_TIMER_STARTD() TIMER_START() +#define TD_TIMER_UPDATED() TIMER_UPDATE() +#endif +#endif + + +// macros redirecting standard streams to the log +#define LOG_OUT() GET_LOG() //or std::cout +#define LOG_ERR() GET_LOG() //or std::cerr + + +// macros simplifying the task of composing file paths; +// WORKING_FOLDER and WORKING_FOLDER_FULL must be defined as strings +// containing the relative/full path to the working folder +#ifndef WORKING_FOLDER +namespace SEACAVE { +class String; +extern String g_strWorkingFolder; // empty by default (current folder) +extern String g_strWorkingFolderFull; // full path to current folder +} +#define WORKING_FOLDER g_strWorkingFolder // empty by default (current folder) +#define WORKING_FOLDER_FULL g_strWorkingFolderFull // full path to current folder +#endif +#define INIT_WORKING_FOLDER {SEACAVE::Util::ensureValidFolderPath(WORKING_FOLDER); WORKING_FOLDER_FULL = SEACAVE::Util::getFullPath(WORKING_FOLDER);} // initialize working folders +#define MAKE_PATH(str) SEACAVE::Util::getSimplifiedPath(WORKING_FOLDER+SEACAVE::String(str)) // add working directory to the given file name +#define MAKE_PATH_SAFE(str) (SEACAVE::Util::isFullPath((str).c_str()) ? SEACAVE::String(str) : MAKE_PATH(str)) // add working directory to the given file name only if not full path already +#define MAKE_PATH_FULL(p,s) (SEACAVE::Util::isFullPath((s).c_str()) ? SEACAVE::String(s) : SEACAVE::Util::getSimplifiedPath((p)+(s))) // add the given path to the given file name +#define MAKE_PATH_REL(p,s) SEACAVE::Util::getRelativePath(p,s) // remove the given path from the given file name +#define GET_PATH_FULL(str) (SEACAVE::Util::isFullPath((str).c_str()) ? SEACAVE::Util::getFilePath(str) : SEACAVE::Util::getSimplifiedPath(WORKING_FOLDER_FULL+SEACAVE::Util::getFilePath(str))) // retrieve the full path to the given file + + +// macros simplifying the task of managing options +#define DECOPT_SPACE(SPACE) namespace SPACE { \ + void init(); \ + void update(); \ + extern SEACAVE::VoidArr arrFncOpt; \ + extern SEACAVE::CConfigTable oConfig; \ +} +#define DEFOPT_SPACE(SPACE, name) namespace SPACE { \ + SEACAVE::CConfigTable oConfig(name); \ + typedef LPCTSTR (*FNCINDEX)(); \ + typedef void (*FNCINIT)(SEACAVE::IDX); \ + typedef void (*FNCUPDATE)(); \ + VoidArr arrFncOpt; \ + void init() { \ + FOREACH(i, arrFncOpt) \ + ((FNCINIT)arrFncOpt[i])(i); \ + } \ + void update() { \ + FOREACH(i, arrFncOpt) \ + ((FNCUPDATE)arrFncOpt[i])(); \ + } \ +} + +#define DEFVAR_OPTION(SPACE, flags, type, name, title, desc, ...) namespace SPACE { \ + type name; \ + LPCTSTR defval_##name(NULL); \ + void update_##name() { \ + SEACAVE::String::FromString(oConfig[title].val, name); \ + } \ + void init_##name(SEACAVE::IDX idx) { \ + LPCTSTR const vals[] = {__VA_ARGS__}; \ + arrFncOpt[idx] = (void*)update_##name; \ + SMLVALUE& val = oConfig[title]; \ + CFGITEM& opt = *((CFGITEM*)val.data); \ + opt.state = flags; \ + val.val = opt.defval = (defval_##name != NULL ? defval_##name : vals[0]); \ + opt.vals.Insert(opt.defval); \ + for (size_t i=1; i Sphere2f; +typedef TSphere Sphere3f; +typedef TAABB AABB2f; +typedef TAABB AABB3f; +typedef TOBB OBB2f; +typedef TOBB OBB3f; +typedef TRay Ray2f; +typedef TRay Ray3f; +typedef TLine Line2f; +typedef TLine Line3f; +typedef TTriangle Triangle2f; +typedef TTriangle Triangle3f; +typedef TPlane Planef; +typedef TPoint2 Point2f; +typedef TPoint3 Point3f; +typedef TMatrix Vec2f; +typedef TMatrix Vec3f; +typedef TMatrix Vec4f; +typedef TMatrix Matrix2x2f; +typedef TMatrix Matrix3x3f; +typedef TMatrix Matrix3x4f; +typedef TMatrix Matrix4x4f; + +typedef TSphere Sphere2d; +typedef TSphere Sphere3d; +typedef TAABB AABB2d; +typedef TAABB AABB3d; +typedef TOBB OBB2d; +typedef TOBB OBB3d; +typedef TRay Ray2d; +typedef TRay Ray3d; +typedef TLine Line2d; +typedef TLine Line3d; +typedef TTriangle Triangle2d; +typedef TTriangle Triangle3d; +typedef TPlane Planed; +typedef TPoint2 Point2d; +typedef TPoint3 Point3d; +typedef TMatrix Vec2d; +typedef TMatrix Vec3d; +typedef TMatrix Vec4d; +typedef TMatrix Matrix2x2d; +typedef TMatrix Matrix3x3d; +typedef TMatrix Matrix3x4d; +typedef TMatrix Matrix4x4d; + +typedef TSphere Sphere2; +typedef TSphere Sphere3; +typedef TAABB AABB2; +typedef TAABB AABB3; +typedef TOBB OBB2; +typedef TOBB OBB3; +typedef TRay Ray2; +typedef TRay Ray3; +typedef TLine Line2; +typedef TLine Line3; +typedef TTriangle Triangle2; +typedef TTriangle Triangle3; +typedef TPlane Plane; +typedef TPoint2 Point2; +typedef TPoint3 Point3; +typedef TMatrix Vec2; +typedef TMatrix Vec3; +typedef TMatrix Vec4; +typedef TMatrix Matrix2x2; +typedef TMatrix Matrix3x3; +typedef TMatrix Matrix3x4; +typedef TMatrix Matrix4x4; + +typedef TQuaternion Quaternion; +typedef TRMatrixBase RMatrixBase; + +// camera matrix types +typedef Point3 CMatrix; +typedef RMatrixBase RMatrix; +typedef Matrix3x3 KMatrix; +typedef Matrix3x4 PMatrix; + +// reconstructed 3D point type +typedef Vec3 X3D; +typedef SEACAVE::cList X3DArr; + +typedef SEACAVE::cList IndexArr; + +typedef CLISTDEF0(REAL) REALArr; +typedef CLISTDEF0(Point2f) Point2fArr; +typedef CLISTDEF0(Point3f) Point3fArr; +typedef CLISTDEF0(Point2d) Point2dArr; +typedef CLISTDEF0(Point3d) Point3dArr; +typedef CLISTDEF0(Point2) Point2Arr; +typedef CLISTDEF0(Point3) Point3Arr; +typedef CLISTDEF0(Vec4) Vec4Arr; +typedef CLISTDEF0(Matrix3x3) Matrix3x3Arr; +typedef CLISTDEF0(Matrix3x4) Matrix3x4Arr; +typedef CLISTDEF0(CMatrix) CMatrixArr; +typedef CLISTDEF0(RMatrix) RMatrixArr; +typedef CLISTDEF0(KMatrix) KMatrixArr; +typedef CLISTDEF0(PMatrix) PMatrixArr; +typedef CLISTDEF0(ImageRef) ImageRefArr; +typedef CLISTDEF0(Pixel8U) Pixel8UArr; +typedef CLISTDEF0(Pixel32F) Pixel32FArr; +typedef CLISTDEF0(Color8U) Color8UArr; +typedef CLISTDEF0(Color32F) Color32FArr; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +// define specialized cv:DataType<> +DEFINE_CVDATATYPE(SEACAVE::Vec2f) +DEFINE_CVDATATYPE(SEACAVE::Vec3f) +DEFINE_CVDATATYPE(SEACAVE::Vec4f) +DEFINE_CVDATATYPE(SEACAVE::Matrix2x2f) +DEFINE_CVDATATYPE(SEACAVE::Matrix3x3f) +DEFINE_CVDATATYPE(SEACAVE::Matrix3x4f) +DEFINE_CVDATATYPE(SEACAVE::Matrix4x4f) + +DEFINE_CVDATATYPE(SEACAVE::Vec2d) +DEFINE_CVDATATYPE(SEACAVE::Vec3d) +DEFINE_CVDATATYPE(SEACAVE::Vec4d) +DEFINE_CVDATATYPE(SEACAVE::Matrix2x2d) +DEFINE_CVDATATYPE(SEACAVE::Matrix3x3d) +DEFINE_CVDATATYPE(SEACAVE::Matrix3x4d) +DEFINE_CVDATATYPE(SEACAVE::Matrix4x4d) +/*----------------------------------------------------------------*/ + +#endif // _COMMON_COMMON_H_ diff --git a/libs/Common/Config.h b/libs/Common/Config.h new file mode 100644 index 0000000..d74b5c8 --- /dev/null +++ b/libs/Common/Config.h @@ -0,0 +1,281 @@ +//////////////////////////////////////////////////////////////////// +// Config.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_CONFIG_H__ +#define __SEACAVE_CONFIG_H__ + +// Configure everything that needs to be globally known + +#include "ConfigLocal.h" + + +// D E F I N E S /////////////////////////////////////////////////// + +#ifdef _MSC_VER + +// Modify the following defines if you have to target a platform prior to the ones specified below. +// Refer to MSDN for the latest info on corresponding values for different platforms. +#if _MSC_VER > 1400 +#ifndef NTDDI_VERSION // There's now just one symbol to specify the minimum target operating system. +#define NTDDI_VERSION NTDDI_WIN7 // All the other symbols are set automatically to the appropriate values for the target operating system. +#endif +#else +#ifndef WINVER // Allow use of features specific to Windows 95 and Windows NT 4 or later. +#define WINVER 0x0500 // Change this to the appropriate value to target Windows 98 and Windows 2000 or later. +#endif +#ifndef _WIN32_WINNT // Allow use of features specific to Windows NT 4 or later. +#define _WIN32_WINNT 0x0500 // Change this to the appropriate value to target Windows 98 and Windows 2000 or later. +#endif +#ifndef _WIN32_WINDOWS // Allow use of features specific to Windows 98 or later. +#define _WIN32_WINDOWS 0x0410 // Change this to the appropriate value to target Windows Me or later. +#endif +#ifndef _WIN32_IE // Allow use of features specific to IE 4.0 or later. +#define _WIN32_IE 0x0501 // Change this to the appropriate value to target IE 5.0 or later. +#endif +#endif + +#ifndef VC_EXTRALEAN +#define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers +#endif + +#ifndef STRICT +#define STRICT +#endif + +#define WIN32_MEAN_AND_LEAN + +#define NOMINMAX + +#define _USE_MATH_DEFINES + +#if _MSC_VER >= 1400 +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS 1 +#endif +#ifndef _CRT_SECURE_NO_DEPRECATE +#define _CRT_SECURE_NO_DEPRECATE 1 +#endif +#ifndef _ATL_SECURE_NO_DEPRECATE +#define _ATL_SECURE_NO_DEPRECATE 1 +#endif +#ifndef _CRT_NON_CONFORMING_SWPRINTFS +#define _CRT_NON_CONFORMING_SWPRINTFS 1 +#endif +#ifndef _CRT_SECURE_CPP_OVERLOAD_SECURE_NAMES +#define _CRT_SECURE_CPP_OVERLOAD_SECURE_NAMES 0 // disable automatically overloading CPP names to secure versions +#endif +#if 0 && defined(_DEBUG) && !defined(_ITERATOR_DEBUG_LEVEL) // might not build if linking statically to 3rd party libraries +#define _ITERATOR_DEBUG_LEVEL 1 // disable std iterator debugging even in Debug, as it is very slow +#endif +#endif + + +//---------------------------------------------------------------------- +// For Microsoft Visual C++, externally accessible symbols must be +// explicitly indicated with DLL_API. +// +// The following ifdef block is the standard way of creating macros +// which make exporting from a DLL simpler. All files within this DLL +// are compiled with the DLL_EXPORTS preprocessor symbol defined on the +// command line. In contrast, projects that use (or import) the DLL +// objects do not define the DLL_EXPORTS symbol. This way any other +// project whose source files include this file see DLL_API functions as +// being imported from a DLL, whereas this DLL sees symbols defined with +// this macro as being exported. +//---------------------------------------------------------------------- +#define EXPORT_API __declspec(dllexport) +#define IMPORT_API __declspec(dllimport) +/*----------------------------------------------------------------*/ +#ifdef _USRDLL + #ifdef Common_EXPORTS + #define GENERAL_API EXPORT_API + #define GENERAL_TPL + #else + #define GENERAL_API IMPORT_API + #define GENERAL_TPL extern + #endif +#else + #define GENERAL_API + #define GENERAL_TPL +#endif +/*----------------------------------------------------------------*/ + +// Define platform type +#if _WIN64 +#define _ENVIRONMENT64 +#else +#define _ENVIRONMENT32 +#endif + +#else // _MSC_VER + +#if !defined(_DEBUG) && !defined(NDEBUG) +#define _DEBUG +#endif + +//---------------------------------------------------------------------- +// DLL_API is ignored for all other systems +//---------------------------------------------------------------------- +#define EXPORT_API +#define IMPORT_API +#define GENERAL_API +#define GENERAL_TPL + +// Define platform type +#if __x86_64__ || __ppc64__ +#define _ENVIRONMENT64 +#else +#define _ENVIRONMENT32 +#endif + +#endif // _MSC_VER + + +#if __cplusplus >= 201103L || (__clang_major__ >= 4 || (__clang_major__ >= 3 && __clang_minor__ >= 3)) +#define _SUPPORT_CPP11 +#endif +#if __cplusplus >= 201402L || (__clang_major__ >= 4 || (__clang_major__ >= 3 && __clang_minor__ >= 4)) +#define _SUPPORT_CPP14 +#endif +#if __cplusplus >= 201703L || __clang_major__ >= 5 +#define _SUPPORT_CPP17 +#endif +#if __cplusplus >= 202002L || __clang_major__ >= 10 +#define _SUPPORT_CPP20 +#endif + + +#if defined(__powerpc__) +#define _PLATFORM_PPC 1 +#elif defined(__arm__) || defined (__arm64__) || defined(__aarch64__) || defined(_M_ARM) || defined(_M_ARMT) +#define _PLATFORM_ARM 1 +#else +#define _PLATFORM_X86 1 +#endif + + +#if !defined(_DEBUG) && !defined(_PROFILE) +#define _RELEASE // exclude code useful only for debug +#endif + + + +//optimization flags +#if defined(_MSC_VER) +# define ALIGN(n) __declspec(align(n)) +# define NOINITVTABLE __declspec(novtable) //disable generating code to initialize the vfptr in the constructor(s) and destructor of the class +# define DECRESTRICT __declspec(restrict) //applied to a function declaration or definition that returns a pointer type and tells the compiler that the function returns an object that will not be aliased with any other pointers +# define NOALIAS __declspec(noalias) //applied to a function declaration or definition that returns a pointer type and tells the compiler that the function call does not modify or reference visible global state and only modifies the memory pointed to directly by pointer parameters (first-level indirections) +# define RESTRICT __restrict //applied to a function parameter +# define MEMALLOC __declspec(noalias) __declspec(restrict) +# define DEPRECATED __declspec(deprecated) +# define MAYBEUNUSED +# define HOT +# define COLD +# define THREADLOCAL __declspec(thread) +# define FORCEINLINE __forceinline +#elif defined(__GNUC__) +# define ALIGN(n) __attribute__((aligned(n))) +# define NOINITVTABLE +# define DECRESTRICT +# define NOALIAS +# define RESTRICT __restrict__ +# define MEMALLOC __attribute__ ((__malloc__)) +# define DEPRECATED __attribute__ ((__deprecated__)) +# define MAYBEUNUSED __attribute__ ((unused)) +# define HOT __attribute__((hot)) __attribute__((optimize("-O3"))) __attribute__((optimize("-ffast-math"))) //optimize for speed, even in debug +# define COLD __attribute__((cold)) //optimize for size +# define THREADLOCAL __thread +# define FORCEINLINE inline //__attribute__((always_inline)) +#else +# define ALIGN(n) +# define NOINITVTABLE +# define DECRESTRICT +# define NOALIAS +# define RESTRICT +# define MEMALLOC +# define DEPRECATED +# define MAYBEUNUSED +# define HOT +# define COLD +# define THREADLOCAL __thread +# define FORCEINLINE inline +#endif + +#ifndef _SUPPORT_CPP11 +# define constexpr inline +#endif +#ifdef _SUPPORT_CPP17 +# undef MAYBEUNUSED +# define MAYBEUNUSED [[maybe_unused]] +#endif + +#define SAFE_DELETE(p) { if (p!=NULL) { delete (p); (p)=NULL; } } +#define SAFE_DELETE_ARR(p) { if (p!=NULL) { delete [] (p); (p)=NULL; } } +#define SAFE_FREE(p) { if (p!=NULL) { free(p); (p)=NULL; } } +#define SAFE_RELEASE(p) { if (p!=NULL) { (p)->Release(); (p)=NULL; } } + + +#ifdef _DEBUG + +#ifdef _MSC_VER +#define _DEBUGINFO +#define _CRTDBG_MAP_ALLOC //enable this to show also the filename (DEBUG_NEW should also be defined in each file) +#include +#include +#ifdef _INC_CRTDBG +#define ASSERT(exp) {if (!(exp) && 1 == _CrtDbgReport(_CRT_ASSERT, __FILE__, __LINE__, NULL, #exp)) _CrtDbgBreak();} +#else +#define ASSERT(exp) {if (!(exp)) __debugbreak();} +#endif // _INC_CRTDBG +#define TRACE(...) {TCHAR buffer[2048]; _sntprintf(buffer, 2048, __VA_ARGS__); OutputDebugString(buffer);} +#else // _MSC_VER +#include +#define ASSERT(exp) assert(exp) +#define TRACE(...) +#endif // _MSC_VER + +#else + +#ifdef _RELEASE +#define ASSERT(exp) +#else +#ifdef _MSC_VER +#define ASSERT(exp) {if (!(exp)) __debugbreak();} +#else // _MSC_VER +#define ASSERT(exp) {if (!(exp)) __builtin_trap();} +#endif // _MSC_VER +#endif +#define TRACE(...) + +#endif // _DEBUG + +#define ASSERTM(exp, msg) ASSERT(exp) + +namespace SEACAVE_ASSERT +{ + template struct compile_time_assert; + template <> struct compile_time_assert { enum {value=1}; }; + + template struct assert_are_same_type; + template struct assert_are_same_type { enum{value=1}; }; + + template struct assert_are_not_same_type { enum{value=1}; }; + template struct assert_are_not_same_type {}; +} + +#define STATIC_ASSERT(expression) \ + MAYBEUNUSED typedef char CTA##__LINE__[::SEACAVE_ASSERT::compile_time_assert<(bool)(expression)>::value] + +#define ASSERT_ARE_SAME_TYPE(type1, type2) \ + MAYBEUNUSED typedef char AAST##__LINE__[::SEACAVE_ASSERT::assert_are_same_type::value] + +#define ASSERT_ARE_NOT_SAME_TYPE(type1, type2) \ + MAYBEUNUSED typedef char AANST##__LINE__[::SEACAVE_ASSERT::assert_are_not_same_type::value] +/*----------------------------------------------------------------*/ + +#endif // __SEACAVE_CONFIG_H__ diff --git a/libs/Common/ConfigTable.cpp b/libs/Common/ConfigTable.cpp new file mode 100644 index 0000000..9dbe28f --- /dev/null +++ b/libs/Common/ConfigTable.cpp @@ -0,0 +1,153 @@ +//////////////////////////////////////////////////////////////////// +// ConfigTable.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" +#include "ConfigTable.h" + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +/*-----------------------------------------------------------* + * CConfigTable class implementation * + *-----------------------------------------------------------*/ + +/** + * Constructor + */ +CConfigTable::CConfigTable(const String& name) : m_oSML(name), m_pParent(NULL) +{ + m_oSML.SetFncItem(ItemInitData, ItemSaveData, ItemReleaseData); +} + +/** + * Destructor + */ +CConfigTable::~CConfigTable() +{ + if (m_pParent) + m_pParent->RemoveChild(*this); + Release(); +} +/*----------------------------------------------------------------*/ + +/** + * Released all used memory + */ +void CConfigTable::Release() +{ + m_oSML.Release(); +} +/*----------------------------------------------------------------*/ + + +/** + * Create a new child + */ +void CConfigTable::Insert(const String& name) +{ + SML* const pChildSML = new SML(name); + pChildSML->SetFncItem(ItemInitData, ItemSaveData, ItemReleaseData); + const IDX idx = m_oSML.InsertChild(pChildSML); + // if child already exists, delete created node + if (m_oSML.GetArrChildren()[idx] != pChildSML) + delete pChildSML; +} +/*----------------------------------------------------------------*/ + + +/** + * Remove an existing child + */ +void CConfigTable::Remove(const String& name) +{ + m_oSML.DestroyChild(name); +} +/*----------------------------------------------------------------*/ + + +/** + * Get the config table for the given child + */ +const SML& CConfigTable::GetConfig(const String& name) const +{ + const LPSMLARR& arrSML = m_oSML.GetArrChildren(); + const IDX idx = arrSML.FindFirst(&name, SML::CompareName); + ASSERT(idx != LPSMLARR::NO_INDEX); + return *arrSML[idx]; +} // GetConfig +SML& CConfigTable::GetConfig(const String& name) +{ + const LPSMLARR& arrSML = m_oSML.GetArrChildren(); + const IDX idx = arrSML.FindFirst(&name, SML::CompareName); + ASSERT(idx != LPSMLARR::NO_INDEX); + return *arrSML[idx]; +} // GetConfig +/*----------------------------------------------------------------*/ + + +/** + * Load the configuration from a file. + */ +bool CConfigTable::Load(const String& f) +{ + return m_oSML.Load(f); +} // Load +bool CConfigTable::Load(ISTREAM& oStream) +{ + return m_oSML.Load(oStream); +} // Load +/** + * Write to a file the values of this node and its children. + * Set to false the second parameter in order not to save the empty children. + */ +bool CConfigTable::Save(const String& f, SML::SAVEFLAG flags) const +{ + return m_oSML.Save(f, flags); +} // Save +bool CConfigTable::Save(OSTREAM& oStream, SML::SAVEFLAG flags) const +{ + return m_oSML.Save(oStream, flags); +} // Save +/*----------------------------------------------------------------*/ + + +/** + * Create and initialize a config item for the given SML entry. + */ +void STCALL CConfigTable::ItemInitData(const String& key, SMLVALUE& val, void*) +{ + CFGITEM* pItem = new CFGITEM; + pItem->name = key; + val.data = pItem; +} // ItemInitData +/*----------------------------------------------------------------*/ + +/** + * Save a config item for the given SML entry. Return false if this item should not be saved. + */ +bool STCALL CConfigTable::ItemSaveData(const SMLVALUE& val, void*) +{ + const CFGITEM& item = *((const CFGITEM*)val.data); + return !item.state.isSet(CFGITEM::TEMP); +} // ItemSaveData +/*----------------------------------------------------------------*/ + +/** + * Release and destroy the config item for the given SML entry. + */ +void STCALL CConfigTable::ItemReleaseData(SMLVALUE& val, void*) +{ + CFGITEM* pItem = (CFGITEM*)val.data; + val.data = NULL; + delete pItem; +} // ItemReleaseData +/*----------------------------------------------------------------*/ diff --git a/libs/Common/ConfigTable.h b/libs/Common/ConfigTable.h new file mode 100644 index 0000000..3375121 --- /dev/null +++ b/libs/Common/ConfigTable.h @@ -0,0 +1,82 @@ +//////////////////////////////////////////////////////////////////// +// ConfigTable.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_CONFIGTABLE_H__ +#define __SEACAVE_CONFIGTABLE_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "SML.h" + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +typedef struct CFGITEM_TYPE { + enum { + NA = 0, // no special states available + TEMP = (1 << 0), // this item lives only this instance and it will not be save + }; + Flags state; + String name; + String desc; + String defval; + StringArr vals; +} CFGITEM; + + +// P R O T O T Y P E S ///////////////////////////////////////////// + +/** + * Configuration table interface. + */ + +class GENERAL_API CConfigTable +{ +public: + CConfigTable(const String&); + ~CConfigTable(); + + void Release(); + + // main methods + void Insert(const String&); + void Remove(const String&); + const SML& GetConfig(const String&) const; + SML& GetConfig(const String&); + const SML& GetConfig() const { return m_oSML; } + SML& GetConfig() { return m_oSML; } + inline SMLVALUE& operator[] (const String& name) { return m_oSML[name]; } + inline IDX InsertChild(CConfigTable& oCfg) { oCfg.SetParent(this); return m_oSML.InsertChild(&oCfg.m_oSML); } + inline void RemoveChild(CConfigTable& oCfg) { oCfg.SetParent(NULL); m_oSML.RemoveChild(oCfg.m_oSML.GetName()); } + + // misc methods + bool Load(const String&); + bool Load(ISTREAM&); + bool Save(const String&, SML::SAVEFLAG=SML::NONE) const; + bool Save(OSTREAM&, SML::SAVEFLAG=SML::NONE) const; + inline const String& GetName() const { return m_oSML.GetName(); } + inline CConfigTable* GetParent() const { return m_pParent; } + inline void SetParent(CConfigTable* pParent) { m_pParent = pParent; } + static void STCALL ItemInitData(const String&, SMLVALUE&, void*); + static bool STCALL ItemSaveData(const SMLVALUE&, void*); + static void STCALL ItemReleaseData(SMLVALUE&, void*); + +private: + SML m_oSML; + CConfigTable* m_pParent; +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_CONFIGTABLE_H__ diff --git a/libs/Common/CriticalSection.h b/libs/Common/CriticalSection.h new file mode 100644 index 0000000..b7f03a6 --- /dev/null +++ b/libs/Common/CriticalSection.h @@ -0,0 +1,264 @@ +//////////////////////////////////////////////////////////////////// +// CriticalSection.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_CRITCALSECTION_H__ +#define __SEACAVE_CRITCALSECTION_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Thread.h" + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class CriticalSection +{ +#ifdef _MSC_VER + +public: + CriticalSection() { + InitializeCriticalSection(&cs); + } + ~CriticalSection() { + DeleteCriticalSection(&cs); + } + void Clear() { + DeleteCriticalSection(&cs); + InitializeCriticalSection(&cs); + } + void Enter() { + EnterCriticalSection(&cs); + } + bool TryEnter() { + return (TryEnterCriticalSection(&cs) != 0); + } + void Leave() { + LeaveCriticalSection(&cs); + } + +protected: + CRITICAL_SECTION cs; + +#else + +public: + CriticalSection() { + #ifdef __APPLE__ + pthread_mutex_init(&mtx, NULL); + #else + mtx = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP; + #endif + } + ~CriticalSection() { pthread_mutex_destroy(&mtx); } + void Clear() { + pthread_mutex_destroy(&mtx); + #ifdef __APPLE__ + pthread_mutex_init(&mtx, NULL); + #else + mtx = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP; + #endif + } + void Enter() { pthread_mutex_lock(&mtx); } + bool TryEnter() { return (pthread_mutex_trylock(&mtx) == 0); } + void Leave() { pthread_mutex_unlock(&mtx); } + pthread_mutex_t& getMutex() { return mtx; } + +protected: + pthread_mutex_t mtx; + +#endif + +private: + CriticalSection(const CriticalSection&); + CriticalSection& operator=(const CriticalSection&); +}; + +/** +* A fast, non-recursive and unfair implementation of the Critical Section. +* It is meant to be used in situations where the risk for lock conflict is very low, +* i e locks that are held for a very short time. The lock is _not_ recursive, i e if +* the same thread will try to grab the lock it'll hang in a never-ending loop. The lock +* is not fair, i e the first to try to enter a locked lock is not guaranteed to be the +* first to get it when it's freed... +*/ +class FastCriticalSection { +public: + FastCriticalSection() : state(0) {} + + void Clear() { + Thread::safeExchange(state, 0); + } + + void Enter() { + while (Thread::safeCompareExchange(state, 0, 1) != 0) + Thread::yield(); + } + bool TryEnter() { + return (Thread::safeCompareExchange(state, 0, 1) == 0); + } + void Leave() { + Thread::safeDec(state); + } + +protected: + volatile Thread::safe_t state; +}; + +template +class SimpleLock { +public: + SimpleLock(T& aCs) : cs(aCs) { cs.Enter(); } + ~SimpleLock() { cs.Leave(); } +protected: + T& cs; +}; + +template +class SimpleLockTry { +public: + SimpleLockTry(T& aCs) : cs(aCs) { bLocked = cs.TryEnter(); } + ~SimpleLockTry() { if (bLocked) cs.Leave(); } + bool IsLocked() const { return bLocked; } +protected: + T& cs; + bool bLocked; +}; + +typedef SimpleLock Lock; +typedef SimpleLock FastLock; +typedef SimpleLockTry LockTry; +typedef SimpleLockTry FastLockTry; + +class RWLock +{ +public: + RWLock() : cs(), readers(0) {} + ~RWLock() { ASSERT(readers==0); } + void Clear() { cs.Clear(); readers = 0; } + + // Read + void EnterRead() { + Lock l(cs); + ++readers; + } + + bool TryEnterRead() { + LockTry l(cs); + if (!l.IsLocked()) + return false; + ++readers; + return true; + } + + void LeaveRead() { + Lock l(cs); + ASSERT(readers > 0); + --readers; + } + + bool TryLeaveRead() { + LockTry l(cs); + if (!l.IsLocked()) + return false; + ASSERT(readers > 0); + --readers; + return true; + } + + // Write + void EnterWrite() { + cs.Enter(); + while (readers) { + cs.Leave(); + Thread::yield(); + cs.Enter(); + } + } + + bool TryEnterWrite() { + if (cs.TryEnter()) { + if (readers == 0) + return true; + cs.Leave(); + } + return false; + } + + void LeaveWrite() { + cs.Leave(); + } + +private: + RWLock(const RWLock&); + RWLock& operator=(const RWLock&); + +protected: + CriticalSection cs; + unsigned readers; +}; + +class RLock { +public: + RLock(RWLock& aCs) : cs(aCs) { cs.EnterRead(); } + ~RLock() { cs.LeaveRead(); } +private: + RLock(const RLock&); + RLock& operator=(const RLock&); +protected: + RWLock& cs; +}; + +class RLockTry { +public: + RLockTry(RWLock& aCs) : cs(aCs) { bLocked = cs.TryEnterRead(); } + ~RLockTry() { if (bLocked) cs.LeaveRead(); } + bool IsLocked() const { return bLocked; } + bool TryEnter() { return (bLocked = cs.TryEnterRead()); } + bool TryLeave() { return !(bLocked = !cs.TryLeaveRead()); } +private: + RLockTry(const RLockTry&); + RLockTry& operator=(const RLockTry&); +protected: + RWLock& cs; + bool bLocked; +}; + +class WLock { +public: + WLock(RWLock& aCs) : cs(aCs) { cs.EnterWrite(); } + ~WLock() { cs.LeaveWrite(); } +private: + WLock(const WLock&); + WLock& operator=(const WLock&); +protected: + RWLock& cs; +}; + +class WLockTry { +public: + WLockTry(RWLock& aCs) : cs(aCs) { bLocked = cs.TryEnterWrite(); } + ~WLockTry() { if (bLocked) cs.LeaveWrite(); } + bool IsLocked() const { return bLocked; } + bool TryEnter() { return (bLocked = cs.TryEnterWrite()); } +private: + WLockTry(const WLockTry&); + WLockTry& operator=(const WLockTry&); +protected: + RWLock& cs; + bool bLocked; +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_CRITCALSECTION_H__ diff --git a/libs/Common/EventQueue.cpp b/libs/Common/EventQueue.cpp new file mode 100644 index 0000000..82f835b --- /dev/null +++ b/libs/Common/EventQueue.cpp @@ -0,0 +1,101 @@ +//////////////////////////////////////////////////////////////////// +// EventQueue.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" +#include "EventQueue.h" + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +/*-----------------------------------------------------------* + * EventQueue class implementation * + *-----------------------------------------------------------*/ + +void EventQueue::Clear() +{ + m_cs.Clear(); + m_sem.Clear(); + m_events.Empty(); +} + +void EventQueue::AddEvent(Event* evt) +{ + Lock l(m_cs); + m_events.AddTail(evt); + m_sem.Signal(); +} +void EventQueue::AddEventFirst(Event* evt) +{ + Lock l(m_cs); + m_events.AddHead(evt); + m_sem.Signal(); +} + +Event* EventQueue::GetEvent() +{ + m_sem.Wait(); + Lock l(m_cs); + ASSERT(!m_events.IsEmpty()); + return m_events.RemoveHead(); +} +Event* EventQueue::GetEvent(uint32_t millis) +{ + if (!m_sem.Wait(millis)) + return NULL; + Lock l(m_cs); + if (m_events.IsEmpty()) + return NULL; + return m_events.RemoveHead(); +} +Event* EventQueue::GetEventLast() +{ + m_sem.Wait(); + Lock l(m_cs); + ASSERT(!m_events.IsEmpty()); + return m_events.RemoveTail(); +} +Event* EventQueue::GetEventLast(uint32_t millis) +{ + if (!m_sem.Wait(millis)) + return NULL; + Lock l(m_cs); + if (m_events.IsEmpty()) + return NULL; + return m_events.RemoveTail(); +} +/*----------------------------------------------------------------*/ + +bool EventQueue::IsEmpty() const +{ + Lock l(m_cs); + return m_events.IsEmpty(); +} + +uint_t EventQueue::GetSize() const +{ + Lock l(m_cs); + return m_events.GetSize(); +} +/*----------------------------------------------------------------*/ + + + +/*-----------------------------------------------------------* +* EventThreadPool class implementation * +*-----------------------------------------------------------*/ + +void EventThreadPool::stop() +{ + ThreadPool::stop(); + EventQueue::Clear(); +} +/*----------------------------------------------------------------*/ diff --git a/libs/Common/EventQueue.h b/libs/Common/EventQueue.h new file mode 100644 index 0000000..b698785 --- /dev/null +++ b/libs/Common/EventQueue.h @@ -0,0 +1,90 @@ +//////////////////////////////////////////////////////////////////// +// EventQueue.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_EVENTQUEUE_H__ +#define __SEACAVE_EVENTQUEUE_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Types.h" +#include "CriticalSection.h" +#include "Semaphore.h" + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class Event +{ +public: + Event(uint32_t _id) : id(_id) {} + virtual ~Event() {} + + uint32_t GetID() const { return id; } + + virtual bool Run(void* /*pArgs*/ = NULL) { return true; } + +protected: + const uint32_t id; +}; +typedef cQueue EVENTQUEUE; + + +/************************************************************************************** + * Events Queue + * -------------- + * basic eventing mechanism + * multi-thread safe + **************************************************************************************/ + +class GENERAL_API EventQueue +{ +public: + EventQueue() {} + ~EventQueue() {} + + void Clear(); // reset the state of the locks and empty the queue + + void AddEvent(Event*); //add a new event to the end of the queue + void AddEventFirst(Event*); //add a new event to the beginning of the queue + Event* GetEvent(); //block until an event arrives and get the first event pending in the queue + Event* GetEvent(uint32_t millis); //block until an event arrives or time expires and get the first event pending in the queue + Event* GetEventLast(); //block until an event arrives and get the last event pending in the queue + Event* GetEventLast(uint32_t millis); //block until an event arrives or time expires and get the last event pending in the queue + + bool IsEmpty() const; //are there any events in the queue? + uint_t GetSize() const; //number of events in the queue + +protected: + Semaphore m_sem; + mutable CriticalSection m_cs; + EVENTQUEUE m_events; +}; +/*----------------------------------------------------------------*/ + + +// basic event and thread pool +class GENERAL_API EventThreadPool : public ThreadPool, public EventQueue +{ +public: + inline EventThreadPool() {} + inline EventThreadPool(size_type nThreads) : ThreadPool(nThreads) {} + inline EventThreadPool(size_type nThreads, Thread::FncStart pfnStarter, void* pData=NULL) : ThreadPool(nThreads, pfnStarter, pData) {} + inline ~EventThreadPool() {} + + void stop(); //stop threads, reset locks state and empty event queue +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_EVENTQUEUE_H__ diff --git a/libs/Common/FastDelegate.h b/libs/Common/FastDelegate.h new file mode 100644 index 0000000..de2b8bc --- /dev/null +++ b/libs/Common/FastDelegate.h @@ -0,0 +1,2105 @@ +// FastDelegate.h +// Efficient delegates in C++ that generate only two lines of asm code! +// Documentation is found at http://www.codeproject.com/cpp/FastDelegate.asp +// +// - Don Clugston, Mar 2004. +// Major contributions were made by Jody Hagins. +// History: +// 24-Apr-04 1.0 * Submitted to CodeProject. +// 28-Apr-04 1.1 * Prevent most unsafe uses of evil static function hack. +// * Improved syntax for horrible_cast (thanks Paul Bludov). +// * Tested on Metrowerks MWCC and Intel ICL (IA32) +// * Compiled, but not run, on Comeau C++ and Intel Itanium ICL. +// 27-Jun-04 1.2 * Now works on Borland C++ Builder 5.5 +// * Now works on /clr "managed C++" code on VC7, VC7.1 +// * Comeau C++ now compiles without warnings. +// * Prevent the virtual inheritance case from being used on +// VC6 and earlier, which generate incorrect code. +// * Improved warning and error messages. Non-standard hacks +// now have compile-time checks to make them safer. +// * implicit_cast used instead of static_cast in many cases. +// * If calling a const member function, a const class pointer can be used. +// * MakeDelegate() global helper function added to simplify pass-by-value. +// * Added fastdelegate.clear() +// 16-Jul-04 1.2.1* Workaround for gcc bug (const member function pointers in templates) +// 30-Oct-04 1.3 * Support for (non-void) return values. +// * No more workarounds in client code! +// MSVC and Intel now use a clever hack invented by John Dlugosz: +// - The FASTDELEGATEDECLARE workaround is no longer necessary. +// - No more warning messages for VC6 +// * Less use of macros. Error messages should be more comprehensible. +// * Added include guards +// * Added FastDelegate::empty() to test if invocation is safe (Thanks Neville Franks). +// * Now tested on VS 2005 Express Beta, PGI C++ +// 24-Dec-04 1.4 * Added DelegateMemento, to allow collections of disparate delegates. +// * <,>,<=,>= comparison operators to allow storage in ordered containers. +// * Substantial reduction of code size, especially the 'Closure' class. +// * Standardised all the compiler-specific workarounds. +// * MFP conversion now works for CodePlay (but not yet supported in the full code). +// * Now compiles without warnings on _any_ supported compiler, including BCC 5.5.1 +// * New syntax: FastDelegate< int (char *, double) >. +// 14-Feb-05 1.4.1* Now treats =0 as equivalent to .clear(), ==0 as equivalent to .empty(). (Thanks elfric). +// * Now tested on Intel ICL for AMD64, VS2005 Beta for AMD64 and Itanium. +// 30-Mar-05 1.5 * Safebool idiom: "if (dg)" is now equivalent to "if (!dg.empty())" +// * Fully supported by CodePlay VectorC +// * Bugfix for Metrowerks: empty() was buggy because a valid MFP can be 0 on MWCC! +// * More optimal assignment,== and != operators for static function pointers. + +#ifndef FASTDELEGATE_H +#define FASTDELEGATE_H + +#include // to allow <,> comparisons + +//////////////////////////////////////////////////////////////////////////////// +// Configuration options +// +//////////////////////////////////////////////////////////////////////////////// + +// Uncomment the following #define for optimally-sized delegates. +// In this case, the generated asm code is almost identical to the code you'd get +// if the compiler had native support for delegates. +// It will not work on systems where sizeof(dataptr) < sizeof(codeptr). +// Thus, it will not work for DOS compilers using the medium model. +// It will also probably fail on some DSP systems. +#define FASTDELEGATE_USESTATICFUNCTIONHACK + +// Uncomment the next line to allow function declarator syntax. +// It is automatically enabled for those compilers where it is known to work. +//#define FASTDELEGATE_ALLOW_FUNCTION_TYPE_SYNTAX + +//////////////////////////////////////////////////////////////////////////////// +// Compiler identification for workarounds +// +//////////////////////////////////////////////////////////////////////////////// + +// Compiler identification. It's not easy to identify Visual C++ because +// many vendors fraudulently define Microsoft's identifiers. +#if defined(_MSC_VER) && !defined(__MWERKS__) && !defined(__VECTOR_C) && !defined(__ICL) && !defined(__BORLANDC__) +#define FASTDLGT_ISMSVC + +#if (_MSC_VER <1300) // Many workarounds are required for VC6. +#define FASTDLGT_VC6 +#pragma warning(disable:4786) // disable this ridiculous warning +#endif + +#endif + +// Does the compiler uses Microsoft's member function pointer structure? +// If so, it needs special treatment. +// Metrowerks CodeWarrior, Intel, and CodePlay fraudulently define Microsoft's +// identifier, _MSC_VER. We need to filter Metrowerks out. +#if defined(_MSC_VER) && !defined(__MWERKS__) +#define FASTDLGT_MICROSOFT_MFP + +#if !defined(__VECTOR_C) +// CodePlay doesn't have the __single/multi/virtual_inheritance keywords +#define FASTDLGT_HASINHERITANCE_KEYWORDS +#endif +#endif + +// Does it allow function declarator syntax? The following compilers are known to work: +#if defined(FASTDLGT_ISMSVC) && (_MSC_VER >=1310) // VC 7.1 +#define FASTDELEGATE_ALLOW_FUNCTION_TYPE_SYNTAX +#endif + +// Gcc(2.95+), and versions of Digital Mars, Intel and Comeau in common use. +#if defined (__DMC__) || defined(__GNUC__) || defined(__ICL) || defined(__COMO__) +#define FASTDELEGATE_ALLOW_FUNCTION_TYPE_SYNTAX +#endif + +// It works on Metrowerks MWCC 3.2.2. From boost.Config it should work on earlier ones too. +#if defined (__MWERKS__) +#define FASTDELEGATE_ALLOW_FUNCTION_TYPE_SYNTAX +#endif + +#ifdef __GNUC__ // Workaround GCC bug #8271 + // At present, GCC doesn't recognize constness of MFPs in templates +#define FASTDELEGATE_GCC_BUG_8271 +#endif + + + +//////////////////////////////////////////////////////////////////////////////// +// General tricks used in this code +// +// (a) Error messages are generated by typdefing an array of negative size to +// generate compile-time errors. +// (b) Warning messages on MSVC are generated by declaring unused variables, and +// enabling the "variable XXX is never used" warning. +// (c) Unions are used in a few compiler-specific cases to perform illegal casts. +// (d) For Microsoft and Intel, when adjusting the 'this' pointer, it's cast to +// (char *) first to ensure that the correct number of *bytes* are added. +// +//////////////////////////////////////////////////////////////////////////////// +// Helper templates +// +//////////////////////////////////////////////////////////////////////////////// + + +namespace fastdelegate { +namespace detail { // we'll hide the implementation details in a nested namespace. + +// implicit_cast< > +// I believe this was originally going to be in the C++ standard but +// was left out by accident. It's even milder than static_cast. +// I use it instead of static_cast<> to emphasize that I'm not doing +// anything nasty. +// Usage is identical to static_cast<> +template +inline OutputClass implicit_cast(InputClass input){ + return input; +} + +// horrible_cast< > +// This is truly evil. It completely subverts C++'s type system, allowing you +// to cast from any class to any other class. Technically, using a union +// to perform the cast is undefined behaviour (even in C). But we can see if +// it is OK by checking that the union is the same size as each of its members. +// horrible_cast<> should only be used for compiler-specific workarounds. +// Usage is identical to reinterpret_cast<>. + +// This union is declared outside the horrible_cast because BCC 5.5.1 +// can't inline a function with a nested class, and gives a warning. +template +union horrible_union{ + OutputClass out; + InputClass in; +}; + +template +inline OutputClass horrible_cast(const InputClass input){ + horrible_union u; + // Cause a compile-time error if in, out and u are not the same size. + // If the compile fails here, it means the compiler has peculiar + // unions which would prevent the cast from working. + typedef int ERROR_CantUseHorrible_cast[sizeof(InputClass)==sizeof(u) + && sizeof(InputClass)==sizeof(OutputClass) ? 1 : -1]; + u.in = input; + return u.out; +} + +//////////////////////////////////////////////////////////////////////////////// +// Workarounds +// +//////////////////////////////////////////////////////////////////////////////// + +// Backwards compatibility: This macro used to be necessary in the virtual inheritance +// case for Intel and Microsoft. Now it just forward-declares the class. +#define FASTDELEGATEDECLARE(CLASSNAME) class CLASSNAME; + +// Prevent use of the static function hack with the DOS medium model. +#ifdef __MEDIUM__ +#undef FASTDELEGATE_USESTATICFUNCTIONHACK +#endif + +// DefaultVoid - a workaround for 'void' templates in VC6. +// +// (1) VC6 and earlier do not allow 'void' as a default template argument. +// (2) They also doesn't allow you to return 'void' from a function. +// +// Workaround for (1): Declare a dummy type 'DefaultVoid' which we use +// when we'd like to use 'void'. We convert it into 'void' and back +// using the templates DefaultVoidToVoid<> and VoidToDefaultVoid<>. +// Workaround for (2): On VC6, the code for calling a void function is +// identical to the code for calling a non-void function in which the +// return value is never used, provided the return value is returned +// in the EAX register, rather than on the stack. +// This is true for most fundamental types such as int, enum, void *. +// Const void * is the safest option since it doesn't participate +// in any automatic conversions. But on a 16-bit compiler it might +// cause extra code to be generated, so we disable it for all compilers +// except for VC6 (and VC5). +#ifdef FASTDLGT_VC6 +// VC6 workaround +typedef const void * DefaultVoid; +#else +// On any other compiler, just use a normal void. +typedef void DefaultVoid; +#endif + +// Translate from 'DefaultVoid' to 'void'. +// Everything else is unchanged +template +struct DefaultVoidToVoid { typedef T type; }; + +template <> +struct DefaultVoidToVoid { typedef void type; }; + +// Translate from 'void' into 'DefaultVoid' +// Everything else is unchanged +template +struct VoidToDefaultVoid { typedef T type; }; + +template <> +struct VoidToDefaultVoid { typedef DefaultVoid type; }; + + + +//////////////////////////////////////////////////////////////////////////////// +// Fast Delegates, part 1: +// +// Conversion of member function pointer to a standard form +// +//////////////////////////////////////////////////////////////////////////////// + +// GenericClass is a fake class, ONLY used to provide a type. +// It is vitally important that it is never defined, so that the compiler doesn't +// think it can optimize the invocation. For example, Borland generates simpler +// code if it knows the class only uses single inheritance. + +// Compilers using Microsoft's structure need to be treated as a special case. +#ifdef FASTDLGT_MICROSOFT_MFP + +#ifdef FASTDLGT_HASINHERITANCE_KEYWORDS + // For Microsoft and Intel, we want to ensure that it's the most efficient type of MFP + // (4 bytes), even when the /vmg option is used. Declaring an empty class + // would give 16 byte pointers in this case.... + class __single_inheritance GenericClass; +#endif + // ...but for Codeplay, an empty class *always* gives 4 byte pointers. + // If compiled with the /clr option ("managed C++"), the JIT compiler thinks + // it needs to load GenericClass before it can call any of its functions, + // (compiles OK but crashes at runtime!), so we need to declare an + // empty class to make it happy. + // Codeplay and VC4 can't cope with the unknown_inheritance case either. + class GenericClass {}; +#else + class GenericClass; +#endif + +// The size of a single inheritance member function pointer. +const int SINGLE_MEMFUNCPTR_SIZE = sizeof(void (GenericClass::*)()); + +// SimplifyMemFunc< >::Convert() +// +// A template function that converts an arbitrary member function pointer into the +// simplest possible form of member function pointer, using a supplied 'this' pointer. +// According to the standard, this can be done legally with reinterpret_cast<>. +// For (non-standard) compilers which use member function pointers which vary in size +// depending on the class, we need to use knowledge of the internal structure of a +// member function pointer, as used by the compiler. Template specialization is used +// to distinguish between the sizes. Because some compilers don't support partial +// template specialisation, I use full specialisation of a wrapper struct. + +// general case -- don't know how to convert it. Force a compile failure +template +struct SimplifyMemFunc { + template + inline static GenericClass *Convert(X *pthis, XFuncType function_to_bind, + GenericMemFuncType &bound_func) { + // Unsupported member function type -- force a compile failure. + // (it's illegal to have a array with negative size). + typedef char ERROR_Unsupported_member_function_pointer_on_this_compiler[N-100]; + return 0; + } +}; + +// For compilers where all member func ptrs are the same size, everything goes here. +// For non-standard compilers, only single_inheritance classes go here. +template <> +struct SimplifyMemFunc { + template + inline static GenericClass *Convert(X *pthis, XFuncType function_to_bind, + GenericMemFuncType &bound_func) { +#if defined __DMC__ + // Digital Mars doesn't allow you to cast between arbitrary PMF's, + // even though the standard says you can. The 32-bit compiler lets you + // static_cast through an int, but the DOS compiler doesn't. + bound_func = horrible_cast(function_to_bind); +#else + bound_func = reinterpret_cast(function_to_bind); +#endif + return reinterpret_cast(pthis); + } +}; + +//////////////////////////////////////////////////////////////////////////////// +// Fast Delegates, part 1b: +// +// Workarounds for Microsoft and Intel +// +//////////////////////////////////////////////////////////////////////////////// + + +// Compilers with member function pointers which violate the standard (MSVC, Intel, Codeplay), +// need to be treated as a special case. +#ifdef FASTDLGT_MICROSOFT_MFP + +// We use unions to perform horrible_casts. I would like to use #pragma pack(push, 1) +// at the start of each function for extra safety, but VC6 seems to ICE +// intermittently if you do this inside a template. + +// __multiple_inheritance classes go here +// Nasty hack for Microsoft and Intel (IA32 and Itanium) +template<> +struct SimplifyMemFunc< SINGLE_MEMFUNCPTR_SIZE + sizeof(int) > { + template + inline static GenericClass *Convert(X *pthis, XFuncType function_to_bind, + GenericMemFuncType &bound_func) { + // We need to use a horrible_cast to do this conversion. + // In MSVC, a multiple inheritance member pointer is internally defined as: + union { + XFuncType func; + struct { + GenericMemFuncType funcaddress; // points to the actual member function + int delta; // #BYTES to be added to the 'this' pointer + }s; + } u; + // Check that the horrible_cast will work + typedef int ERROR_CantUsehorrible_cast[sizeof(function_to_bind)==sizeof(u.s)? 1 : -1]; + u.func = function_to_bind; + bound_func = u.s.funcaddress; + return reinterpret_cast(reinterpret_cast(pthis) + u.s.delta); + } +}; + +// virtual inheritance is a real nuisance. It's inefficient and complicated. +// On MSVC and Intel, there isn't enough information in the pointer itself to +// enable conversion to a closure pointer. Earlier versions of this code didn't +// work for all cases, and generated a compile-time error instead. +// But a very clever hack invented by John M. Dlugosz solves this problem. +// My code is somewhat different to his: I have no asm code, and I make no +// assumptions about the calling convention that is used. + +// In VC++ and ICL, a virtual_inheritance member pointer +// is internally defined as: +struct MicrosoftVirtualMFP { + void (GenericClass::*codeptr)(); // points to the actual member function + int delta; // #bytes to be added to the 'this' pointer + int vtable_index; // or 0 if no virtual inheritance +}; +// The CRUCIAL feature of Microsoft/Intel MFPs which we exploit is that the +// m_codeptr member is *always* called, regardless of the values of the other +// members. (This is *not* true for other compilers, eg GCC, which obtain the +// function address from the vtable if a virtual function is being called). +// Dlugosz's trick is to make the codeptr point to a probe function which +// returns the 'this' pointer that was used. + +// Define a generic class that uses virtual inheritance. +// It has a trivial member function that returns the value of the 'this' pointer. +struct GenericVirtualClass : virtual public GenericClass +{ + typedef GenericVirtualClass * (GenericVirtualClass::*ProbePtrType)(); + GenericVirtualClass * GetThis() { return this; } +}; + +// __virtual_inheritance classes go here +template <> +struct SimplifyMemFunc +{ + + template + inline static GenericClass *Convert(X *pthis, XFuncType function_to_bind, + GenericMemFuncType &bound_func) { + union { + XFuncType func; + GenericClass* (X::*ProbeFunc)(); + MicrosoftVirtualMFP s; + } u; + u.func = function_to_bind; + bound_func = reinterpret_cast(u.s.codeptr); + union { + GenericVirtualClass::ProbePtrType virtfunc; + MicrosoftVirtualMFP s; + } u2; + // Check that the horrible_cast<>s will work + typedef int ERROR_CantUsehorrible_cast[sizeof(function_to_bind)==sizeof(u.s) + && sizeof(function_to_bind)==sizeof(u.ProbeFunc) + && sizeof(u2.virtfunc)==sizeof(u2.s) ? 1 : -1]; + // Unfortunately, taking the address of a MF prevents it from being inlined, so + // this next line can't be completely optimised away by the compiler. + u2.virtfunc = &GenericVirtualClass::GetThis; + u.s.codeptr = u2.s.codeptr; + return (pthis->*u.ProbeFunc)(); + } +}; + +#if (_MSC_VER <1300) + +// Nasty hack for Microsoft Visual C++ 6.0 +// unknown_inheritance classes go here +// There is a compiler bug in MSVC6 which generates incorrect code in this case!! +template <> +struct SimplifyMemFunc +{ + template + inline static GenericClass *Convert(X *pthis, XFuncType function_to_bind, + GenericMemFuncType &bound_func) { + // There is an apalling but obscure compiler bug in MSVC6 and earlier: + // vtable_index and 'vtordisp' are always set to 0 in the + // unknown_inheritance case! + // This means that an incorrect function could be called!!! + // Compiling with the /vmg option leads to potentially incorrect code. + // This is probably the reason that the IDE has a user interface for specifying + // the /vmg option, but it is disabled - you can only specify /vmg on + // the command line. In VC1.5 and earlier, the compiler would ICE if it ever + // encountered this situation. + // It is OK to use the /vmg option if /vmm or /vms is specified. + + // Fortunately, the wrong function is only called in very obscure cases. + // It only occurs when a derived class overrides a virtual function declared + // in a virtual base class, and the member function + // points to the *Derived* version of that function. The problem can be + // completely averted in 100% of cases by using the *Base class* for the + // member fpointer. Ie, if you use the base class as an interface, you'll + // stay out of trouble. + // Occasionally, you might want to point directly to a derived class function + // that isn't an override of a base class. In this case, both vtable_index + // and 'vtordisp' are zero, but a virtual_inheritance pointer will be generated. + // We can generate correct code in this case. To prevent an incorrect call from + // ever being made, on MSVC6 we generate a warning, and call a function to + // make the program crash instantly. + typedef char ERROR_VC6CompilerBug[-100]; + return 0; + } +}; + + +#else + +// Nasty hack for Microsoft and Intel (IA32 and Itanium) +// unknown_inheritance classes go here +// This is probably the ugliest bit of code I've ever written. Look at the casts! +// There is a compiler bug in MSVC6 which prevents it from using this code. +template <> +struct SimplifyMemFunc +{ + template + inline static GenericClass *Convert(X *pthis, XFuncType function_to_bind, + GenericMemFuncType &bound_func) { + // The member function pointer is 16 bytes long. We can't use a normal cast, but + // we can use a union to do the conversion. + union { + XFuncType func; + // In VC++ and ICL, an unknown_inheritance member pointer + // is internally defined as: + struct { + GenericMemFuncType m_funcaddress; // points to the actual member function + int delta; // #bytes to be added to the 'this' pointer + int vtordisp; // #bytes to add to 'this' to find the vtable + int vtable_index; // or 0 if no virtual inheritance + } s; + } u; + // Check that the horrible_cast will work + typedef int ERROR_CantUsehorrible_cast[sizeof(XFuncType)==sizeof(u.s)? 1 : -1]; + u.func = function_to_bind; + bound_func = u.s.funcaddress; + int virtual_delta = 0; + if (u.s.vtable_index) { // Virtual inheritance is used + // First, get to the vtable. + // It is 'vtordisp' bytes from the start of the class. + const int * vtable = *reinterpret_cast( + reinterpret_cast(pthis) + u.s.vtordisp ); + + // 'vtable_index' tells us where in the table we should be looking. + virtual_delta = u.s.vtordisp + *reinterpret_cast( + reinterpret_cast(vtable) + u.s.vtable_index); + } + // The int at 'virtual_delta' gives us the amount to add to 'this'. + // Finally we can add the three components together. Phew! + return reinterpret_cast( + reinterpret_cast(pthis) + u.s.delta + virtual_delta); + }; +}; +#endif // MSVC 7 and greater + +#endif // MS/Intel hacks + +} // namespace detail + +//////////////////////////////////////////////////////////////////////////////// +// Fast Delegates, part 2: +// +// Define the delegate storage, and cope with static functions +// +//////////////////////////////////////////////////////////////////////////////// + +// DelegateMemento -- an opaque structure which can hold an arbitrary delegate. +// It knows nothing about the calling convention or number of arguments used by +// the function pointed to. +// It supplies comparison operators so that it can be stored in STL collections. +// It cannot be set to anything other than null, nor invoked directly: +// it must be converted to a specific delegate. + +// Implementation: +// There are two possible implementations: the Safe method and the Evil method. +// DelegateMemento - Safe version +// +// This implementation is standard-compliant, but a bit tricky. +// A static function pointer is stored inside the class. +// Here are the valid values: +// +-- Static pointer --+--pThis --+-- pMemFunc-+-- Meaning------+ +// | 0 | 0 | 0 | Empty | +// | !=0 |(dontcare)| Invoker | Static function| +// | 0 | !=0 | !=0* | Method call | +// +--------------------+----------+------------+----------------+ +// * For Metrowerks, this can be 0. (first virtual function in a +// single_inheritance class). +// When stored stored inside a specific delegate, the 'dontcare' entries are replaced +// with a reference to the delegate itself. This complicates the = and == operators +// for the delegate class. + +// DelegateMemento - Evil version +// +// For compilers where data pointers are at least as big as code pointers, it is +// possible to store the function pointer in the this pointer, using another +// horrible_cast. In this case the DelegateMemento implementation is simple: +// +--pThis --+-- pMemFunc-+-- Meaning---------------------+ +// | 0 | 0 | Empty | +// | !=0 | !=0* | Static function or method call| +// +----------+------------+-------------------------------+ +// * For Metrowerks, this can be 0. (first virtual function in a +// single_inheritance class). +// Note that the Sun C++ and MSVC documentation explicitly state that they +// support static_cast between void * and function pointers. + +class DelegateMemento { +protected: + // the data is protected, not private, because many + // compilers have problems with template friends. + typedef void (detail::GenericClass::*GenericMemFuncType)(); // arbitrary MFP. + detail::GenericClass *m_pthis; + GenericMemFuncType m_pFunction; + +#if !defined(FASTDELEGATE_USESTATICFUNCTIONHACK) + typedef void (*GenericFuncPtr)(); // arbitrary code pointer + GenericFuncPtr m_pStaticFunction; +#endif + +public: +#if !defined(FASTDELEGATE_USESTATICFUNCTIONHACK) + DelegateMemento() : m_pthis(0), m_pFunction(0), m_pStaticFunction(0) {}; + void clear() { + m_pthis=0; m_pFunction=0; m_pStaticFunction=0; + } +#else + DelegateMemento() : m_pthis(0), m_pFunction(0) {}; + void clear() { m_pthis=0; m_pFunction=0; } +#endif +public: +#if !defined(FASTDELEGATE_USESTATICFUNCTIONHACK) + inline bool IsEqual (const DelegateMemento &x) const{ + // We have to cope with the static function pointers as a special case + if (m_pFunction!=x.m_pFunction) return false; + // the static function ptrs must either both be equal, or both be 0. + if (m_pStaticFunction!=x.m_pStaticFunction) return false; + if (m_pStaticFunction!=0) return m_pthis==x.m_pthis; + else return true; + } +#else // Evil Method + inline bool IsEqual (const DelegateMemento &x) const{ + return m_pthis==x.m_pthis && m_pFunction==x.m_pFunction; + } +#endif + // Provide a strict weak ordering for DelegateMementos. + inline bool IsLess(const DelegateMemento &right) const { + // deal with static function pointers first +#if !defined(FASTDELEGATE_USESTATICFUNCTIONHACK) + if (m_pStaticFunction !=0 || right.m_pStaticFunction!=0) + return m_pStaticFunction < right.m_pStaticFunction; +#endif + if (m_pthis !=right.m_pthis) return m_pthis < right.m_pthis; + // There are no ordering operators for member function pointers, + // but we can fake one by comparing each byte. The resulting ordering is + // arbitrary (and compiler-dependent), but it permits storage in ordered STL containers. + return memcmp(&m_pFunction, &right.m_pFunction, sizeof(m_pFunction)) < 0; + + } + // BUGFIX (Mar 2005): + // We can't just compare m_pFunction because on Metrowerks, + // m_pFunction can be zero even if the delegate is not empty! + inline bool operator ! () const // Is it bound to anything? + { return m_pthis==0 && m_pFunction==0; } + inline bool empty() const // Is it bound to anything? + { return m_pthis==0 && m_pFunction==0; } +public: + DelegateMemento & operator = (const DelegateMemento &right) { + SetMementoFrom(right); + return *this; + } + inline bool operator <(const DelegateMemento &right) { + return IsLess(right); + } + inline bool operator >(const DelegateMemento &right) { + return right.IsLess(*this); + } + DelegateMemento (const DelegateMemento &right) : + m_pFunction(right.m_pFunction), m_pthis(right.m_pthis) +#if !defined(FASTDELEGATE_USESTATICFUNCTIONHACK) + , m_pStaticFunction (right.m_pStaticFunction) +#endif + {} +protected: + void SetMementoFrom(const DelegateMemento &right) { + m_pFunction = right.m_pFunction; + m_pthis = right.m_pthis; +#if !defined(FASTDELEGATE_USESTATICFUNCTIONHACK) + m_pStaticFunction = right.m_pStaticFunction; +#endif + } +}; + + +// ClosurePtr<> +// +// A private wrapper class that adds function signatures to DelegateMemento. +// It's the class that does most of the actual work. +// The signatures are specified by: +// GenericMemFunc: must be a type of GenericClass member function pointer. +// StaticFuncPtr: must be a type of function pointer with the same signature +// as GenericMemFunc. +// UnvoidStaticFuncPtr: is the same as StaticFuncPtr, except on VC6 +// where it never returns void (returns DefaultVoid instead). + +// An outer class, FastDelegateN<>, handles the invoking and creates the +// necessary typedefs. +// This class does everything else. + +namespace detail { + +template < class GenericMemFunc, class StaticFuncPtr, class UnvoidStaticFuncPtr> +class ClosurePtr : public DelegateMemento { +public: + // These functions are for setting the delegate to a member function. + + // Here's the clever bit: we convert an arbitrary member function into a + // standard form. XMemFunc should be a member function of class X, but I can't + // enforce that here. It needs to be enforced by the wrapper class. + template < class X, class XMemFunc > + inline void bindmemfunc(X *pthis, XMemFunc function_to_bind ) { + m_pthis = SimplifyMemFunc< sizeof(function_to_bind) > + ::Convert(pthis, function_to_bind, m_pFunction); +#if !defined(FASTDELEGATE_USESTATICFUNCTIONHACK) + m_pStaticFunction = 0; +#endif + } + // For const member functions, we only need a const class pointer. + // Since we know that the member function is const, it's safe to + // remove the const qualifier from the 'this' pointer with a const_cast. + // VC6 has problems if we just overload 'bindmemfunc', so we give it a different name. + template < class X, class XMemFunc> + inline void bindconstmemfunc(const X *pthis, XMemFunc function_to_bind) { + m_pthis= SimplifyMemFunc< sizeof(function_to_bind) > + ::Convert(const_cast(pthis), function_to_bind, m_pFunction); +#if !defined(FASTDELEGATE_USESTATICFUNCTIONHACK) + m_pStaticFunction = 0; +#endif + } +#ifdef FASTDELEGATE_GCC_BUG_8271 // At present, GCC doesn't recognize constness of MFPs in templates + template < class X, class XMemFunc> + inline void bindmemfunc(const X *pthis, XMemFunc function_to_bind) { + bindconstmemfunc(pthis, function_to_bind); +#if !defined(FASTDELEGATE_USESTATICFUNCTIONHACK) + m_pStaticFunction = 0; +#endif + } +#endif + // These functions are required for invoking the stored function + inline GenericClass *GetClosureThis() const { return m_pthis; } + inline GenericMemFunc GetClosureMemPtr() const { return reinterpret_cast(m_pFunction); } + +// There are a few ways of dealing with static function pointers. +// There's a standard-compliant, but tricky method. +// There's also a straightforward hack, that won't work on DOS compilers using the +// medium memory model. It's so evil that I can't recommend it, but I've +// implemented it anyway because it produces very nice asm code. + +#if !defined(FASTDELEGATE_USESTATICFUNCTIONHACK) + +// ClosurePtr<> - Safe version +// +// This implementation is standard-compliant, but a bit tricky. +// I store the function pointer inside the class, and the delegate then +// points to itself. Whenever the delegate is copied, these self-references +// must be transformed, and this complicates the = and == operators. +public: + // The next two functions are for operator ==, =, and the copy constructor. + // We may need to convert the m_pthis pointers, so that + // they remain as self-references. + template< class DerivedClass > + inline void CopyFrom (DerivedClass *pParent, const DelegateMemento &x) { + SetMementoFrom(x); + if (m_pStaticFunction!=0) { + // transform self references... + m_pthis=reinterpret_cast(pParent); + } + } + // For static functions, the 'static_function_invoker' class in the parent + // will be called. The parent then needs to call GetStaticFunction() to find out + // the actual function to invoke. + template < class DerivedClass, class ParentInvokerSig > + inline void bindstaticfunc(DerivedClass *pParent, ParentInvokerSig static_function_invoker, + StaticFuncPtr function_to_bind ) { + if (function_to_bind==0) { // cope with assignment to 0 + m_pFunction=0; + } else { + bindmemfunc(pParent, static_function_invoker); + } + m_pStaticFunction=reinterpret_cast(function_to_bind); + } + inline UnvoidStaticFuncPtr GetStaticFunction() const { + return reinterpret_cast(m_pStaticFunction); + } +#else + +// ClosurePtr<> - Evil version +// +// For compilers where data pointers are at least as big as code pointers, it is +// possible to store the function pointer in the this pointer, using another +// horrible_cast. Invocation isn't any faster, but it saves 4 bytes, and +// speeds up comparison and assignment. If C++ provided direct language support +// for delegates, they would produce asm code that was almost identical to this. +// Note that the Sun C++ and MSVC documentation explicitly state that they +// support static_cast between void * and function pointers. + + template< class DerivedClass > + inline void CopyFrom (DerivedClass *pParent, const DelegateMemento &right) { + SetMementoFrom(right); + } + // For static functions, the 'static_function_invoker' class in the parent + // will be called. The parent then needs to call GetStaticFunction() to find out + // the actual function to invoke. + // ******** EVIL, EVIL CODE! ******* + template < class DerivedClass, class ParentInvokerSig> + inline void bindstaticfunc(DerivedClass *pParent, ParentInvokerSig static_function_invoker, + StaticFuncPtr function_to_bind) { + if (function_to_bind==0) { // cope with assignment to 0 + m_pFunction=0; + } else { + // We'll be ignoring the 'this' pointer, but we need to make sure we pass + // a valid value to bindmemfunc(). + bindmemfunc(pParent, static_function_invoker); + } + + // WARNING! Evil hack. We store the function in the 'this' pointer! + // Ensure that there's a compilation failure if function pointers + // and data pointers have different sizes. + // If you get this error, you need to #undef FASTDELEGATE_USESTATICFUNCTIONHACK. + typedef int ERROR_CantUseEvilMethod[sizeof(GenericClass *)==sizeof(function_to_bind) ? 1 : -1]; + m_pthis = horrible_cast(function_to_bind); + // MSVC, SunC++ and DMC accept the following (non-standard) code: +// m_pthis = static_cast(static_cast(function_to_bind)); + // BCC32, Comeau and DMC accept this method. MSVC7.1 needs __int64 instead of long +// m_pthis = reinterpret_cast(reinterpret_cast(function_to_bind)); + } + // ******** EVIL, EVIL CODE! ******* + // This function will be called with an invalid 'this' pointer!! + // We're just returning the 'this' pointer, converted into + // a function pointer! + inline UnvoidStaticFuncPtr GetStaticFunction() const { + // Ensure that there's a compilation failure if function pointers + // and data pointers have different sizes. + // If you get this error, you need to #undef FASTDELEGATE_USESTATICFUNCTIONHACK. + typedef int ERROR_CantUseEvilMethod[sizeof(UnvoidStaticFuncPtr)==sizeof(this) ? 1 : -1]; + return horrible_cast(this); + } +#endif // !defined(FASTDELEGATE_USESTATICFUNCTIONHACK) + + // Does the closure contain this static function? + inline bool IsEqualToStaticFuncPtr(StaticFuncPtr funcptr){ + if (funcptr==0) return empty(); + // For the Evil method, if it doesn't actually contain a static function, this will return an arbitrary + // value that is not equal to any valid function pointer. + else return funcptr==reinterpret_cast(GetStaticFunction()); + } +}; + + +} // namespace detail + +//////////////////////////////////////////////////////////////////////////////// +// Fast Delegates, part 3: +// +// Wrapper classes to ensure type safety +// +//////////////////////////////////////////////////////////////////////////////// + + +// Once we have the member function conversion templates, it's easy to make the +// wrapper classes. So that they will work with as many compilers as possible, +// the classes are of the form +// FastDelegate3 +// They can cope with any combination of parameters. The max number of parameters +// allowed is 8, but it is trivial to increase this limit. +// Note that we need to treat const member functions separately. +// All this class does is to enforce type safety, and invoke the delegate with +// the correct list of parameters. + +// Because of the weird rule about the class of derived member function pointers, +// you sometimes need to apply a downcast to the 'this' pointer. +// This is the reason for the use of "implicit_cast(pthis)" in the code below. +// If CDerivedClass is derived from CBaseClass, but doesn't override SimpleVirtualFunction, +// without this trick you'd need to write: +// MyDelegate(static_cast(&d), &CDerivedClass::SimpleVirtualFunction); +// but with the trick you can write +// MyDelegate(&d, &CDerivedClass::SimpleVirtualFunction); + +// RetType is the type the compiler uses in compiling the template. For VC6, +// it cannot be void. DesiredRetType is the real type which is returned from +// all of the functions. It can be void. + +// Implicit conversion to "bool" is achieved using the safe_bool idiom, +// using member data pointers (MDP). This allows "if (dg)..." syntax +// Because some compilers (eg codeplay) don't have a unique value for a zero +// MDP, an extra padding member is added to the SafeBool struct. +// Some compilers (eg VC6) won't implicitly convert from 0 to an MDP, so +// in that case the static function constructor is not made explicit; this +// allows "if (dg==0) ..." to compile. + +//N=0 +template +class FastDelegate0 { +private: + typedef typename detail::DefaultVoidToVoid::type DesiredRetType; + typedef DesiredRetType (*StaticFunctionPtr)(); + typedef RetType (*UnvoidStaticFunctionPtr)(); + typedef RetType (detail::GenericClass::*GenericMemFn)(); + typedef detail::ClosurePtr ClosureType; + ClosureType m_Closure; +public: + // Typedefs to aid generic programming + typedef FastDelegate0 type; + + // Construction and comparison functions + FastDelegate0() { clear(); } + FastDelegate0(const FastDelegate0 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + void operator = (const FastDelegate0 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + bool operator ==(const FastDelegate0 &x) const { + return m_Closure.IsEqual(x.m_Closure); } + bool operator !=(const FastDelegate0 &x) const { + return !m_Closure.IsEqual(x.m_Closure); } + bool operator <(const FastDelegate0 &x) const { + return m_Closure.IsLess(x.m_Closure); } + bool operator >(const FastDelegate0 &x) const { + return x.m_Closure.IsLess(m_Closure); } + // Binding to non-const member functions + template < class X, class Y > + FastDelegate0(Y *pthis, DesiredRetType (X::* function_to_bind)() ) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(Y *pthis, DesiredRetType (X::* function_to_bind)()) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Binding to const member functions. + template < class X, class Y > + FastDelegate0(const Y *pthis, DesiredRetType (X::* function_to_bind)() const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(const Y *pthis, DesiredRetType (X::* function_to_bind)() const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Static functions. We convert them into a member function call. + // This constructor also provides implicit conversion + FastDelegate0(DesiredRetType (*function_to_bind)() ) { + bind(function_to_bind); } + // for efficiency, prevent creation of a temporary + void operator = (DesiredRetType (*function_to_bind)() ) { + bind(function_to_bind); } + inline void bind(DesiredRetType (*function_to_bind)()) { + m_Closure.bindstaticfunc(this, &FastDelegate0::InvokeStaticFunction, + function_to_bind); } + // Invoke the delegate + RetType operator() () const { + return (m_Closure.GetClosureThis()->*(m_Closure.GetClosureMemPtr()))(); } + // Implicit conversion to "bool" using the safe_bool idiom +private: + typedef struct SafeBoolStruct { + int a_data_pointer_to_this_is_0_on_buggy_compilers; + StaticFunctionPtr m_nonzero; + } UselessTypedef; + typedef StaticFunctionPtr SafeBoolStruct::*unspecified_bool_type; +public: + operator unspecified_bool_type() const { + return empty()? 0: &SafeBoolStruct::m_nonzero; + } + // necessary to allow ==0 to work despite the safe_bool idiom + inline bool operator==(StaticFunctionPtr funcptr) { + return m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator!=(StaticFunctionPtr funcptr) { + return !m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator ! () const { // Is it bound to anything? + return !m_Closure; } + inline bool empty() const { + return !m_Closure; } + void clear() { m_Closure.clear();} + // Conversion to and from the DelegateMemento storage class + const DelegateMemento & GetMemento() { return m_Closure; } + void SetMemento(const DelegateMemento &any) { m_Closure.CopyFrom(this, any); } + +private: // Invoker for static functions + RetType InvokeStaticFunction() const { + return (*(m_Closure.GetStaticFunction()))(); } +}; + +//N=1 +template +class FastDelegate1 { +private: + typedef typename detail::DefaultVoidToVoid::type DesiredRetType; + typedef DesiredRetType (*StaticFunctionPtr)(Param1 p1); + typedef RetType (*UnvoidStaticFunctionPtr)(Param1 p1); + typedef RetType (detail::GenericClass::*GenericMemFn)(Param1 p1); + typedef detail::ClosurePtr ClosureType; + ClosureType m_Closure; +public: + // Typedefs to aid generic programming + typedef FastDelegate1 type; + + // Construction and comparison functions + FastDelegate1() { clear(); } + FastDelegate1(const FastDelegate1 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + void operator = (const FastDelegate1 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + bool operator ==(const FastDelegate1 &x) const { + return m_Closure.IsEqual(x.m_Closure); } + bool operator !=(const FastDelegate1 &x) const { + return !m_Closure.IsEqual(x.m_Closure); } + bool operator <(const FastDelegate1 &x) const { + return m_Closure.IsLess(x.m_Closure); } + bool operator >(const FastDelegate1 &x) const { + return x.m_Closure.IsLess(m_Closure); } + // Binding to non-const member functions + template < class X, class Y > + FastDelegate1(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1) ) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1)) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Binding to const member functions. + template < class X, class Y > + FastDelegate1(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Static functions. We convert them into a member function call. + // This constructor also provides implicit conversion + FastDelegate1(DesiredRetType (*function_to_bind)(Param1 p1) ) { + bind(function_to_bind); } + // for efficiency, prevent creation of a temporary + void operator = (DesiredRetType (*function_to_bind)(Param1 p1) ) { + bind(function_to_bind); } + inline void bind(DesiredRetType (*function_to_bind)(Param1 p1)) { + m_Closure.bindstaticfunc(this, &FastDelegate1::InvokeStaticFunction, + function_to_bind); } + // Invoke the delegate + RetType operator() (Param1 p1) const { + return (m_Closure.GetClosureThis()->*(m_Closure.GetClosureMemPtr()))(p1); } + // Implicit conversion to "bool" using the safe_bool idiom +private: + typedef struct SafeBoolStruct { + int a_data_pointer_to_this_is_0_on_buggy_compilers; + StaticFunctionPtr m_nonzero; + } UselessTypedef; + typedef StaticFunctionPtr SafeBoolStruct::*unspecified_bool_type; +public: + operator unspecified_bool_type() const { + return empty()? 0: &SafeBoolStruct::m_nonzero; + } + // necessary to allow ==0 to work despite the safe_bool idiom + inline bool operator==(StaticFunctionPtr funcptr) { + return m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator!=(StaticFunctionPtr funcptr) { + return !m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator ! () const { // Is it bound to anything? + return !m_Closure; } + inline bool empty() const { + return !m_Closure; } + void clear() { m_Closure.clear();} + // Conversion to and from the DelegateMemento storage class + const DelegateMemento & GetMemento() { return m_Closure; } + void SetMemento(const DelegateMemento &any) { m_Closure.CopyFrom(this, any); } + +private: // Invoker for static functions + RetType InvokeStaticFunction(Param1 p1) const { + return (*(m_Closure.GetStaticFunction()))(p1); } +}; + +//N=2 +template +class FastDelegate2 { +private: + typedef typename detail::DefaultVoidToVoid::type DesiredRetType; + typedef DesiredRetType (*StaticFunctionPtr)(Param1 p1, Param2 p2); + typedef RetType (*UnvoidStaticFunctionPtr)(Param1 p1, Param2 p2); + typedef RetType (detail::GenericClass::*GenericMemFn)(Param1 p1, Param2 p2); + typedef detail::ClosurePtr ClosureType; + ClosureType m_Closure; +public: + // Typedefs to aid generic programming + typedef FastDelegate2 type; + + // Construction and comparison functions + FastDelegate2() { clear(); } + FastDelegate2(const FastDelegate2 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + void operator = (const FastDelegate2 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + bool operator ==(const FastDelegate2 &x) const { + return m_Closure.IsEqual(x.m_Closure); } + bool operator !=(const FastDelegate2 &x) const { + return !m_Closure.IsEqual(x.m_Closure); } + bool operator <(const FastDelegate2 &x) const { + return m_Closure.IsLess(x.m_Closure); } + bool operator >(const FastDelegate2 &x) const { + return x.m_Closure.IsLess(m_Closure); } + // Binding to non-const member functions + template < class X, class Y > + FastDelegate2(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2) ) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2)) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Binding to const member functions. + template < class X, class Y > + FastDelegate2(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Static functions. We convert them into a member function call. + // This constructor also provides implicit conversion + FastDelegate2(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2) ) { + bind(function_to_bind); } + // for efficiency, prevent creation of a temporary + void operator = (DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2) ) { + bind(function_to_bind); } + inline void bind(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2)) { + m_Closure.bindstaticfunc(this, &FastDelegate2::InvokeStaticFunction, + function_to_bind); } + // Invoke the delegate + RetType operator() (Param1 p1, Param2 p2) const { + return (m_Closure.GetClosureThis()->*(m_Closure.GetClosureMemPtr()))(p1, p2); } + // Implicit conversion to "bool" using the safe_bool idiom +private: + typedef struct SafeBoolStruct { + int a_data_pointer_to_this_is_0_on_buggy_compilers; + StaticFunctionPtr m_nonzero; + } UselessTypedef; + typedef StaticFunctionPtr SafeBoolStruct::*unspecified_bool_type; +public: + operator unspecified_bool_type() const { + return empty()? 0: &SafeBoolStruct::m_nonzero; + } + // necessary to allow ==0 to work despite the safe_bool idiom + inline bool operator==(StaticFunctionPtr funcptr) { + return m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator!=(StaticFunctionPtr funcptr) { + return !m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator ! () const { // Is it bound to anything? + return !m_Closure; } + inline bool empty() const { + return !m_Closure; } + void clear() { m_Closure.clear();} + // Conversion to and from the DelegateMemento storage class + const DelegateMemento & GetMemento() { return m_Closure; } + void SetMemento(const DelegateMemento &any) { m_Closure.CopyFrom(this, any); } + +private: // Invoker for static functions + RetType InvokeStaticFunction(Param1 p1, Param2 p2) const { + return (*(m_Closure.GetStaticFunction()))(p1, p2); } +}; + +//N=3 +template +class FastDelegate3 { +private: + typedef typename detail::DefaultVoidToVoid::type DesiredRetType; + typedef DesiredRetType (*StaticFunctionPtr)(Param1 p1, Param2 p2, Param3 p3); + typedef RetType (*UnvoidStaticFunctionPtr)(Param1 p1, Param2 p2, Param3 p3); + typedef RetType (detail::GenericClass::*GenericMemFn)(Param1 p1, Param2 p2, Param3 p3); + typedef detail::ClosurePtr ClosureType; + ClosureType m_Closure; +public: + // Typedefs to aid generic programming + typedef FastDelegate3 type; + + // Construction and comparison functions + FastDelegate3() { clear(); } + FastDelegate3(const FastDelegate3 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + void operator = (const FastDelegate3 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + bool operator ==(const FastDelegate3 &x) const { + return m_Closure.IsEqual(x.m_Closure); } + bool operator !=(const FastDelegate3 &x) const { + return !m_Closure.IsEqual(x.m_Closure); } + bool operator <(const FastDelegate3 &x) const { + return m_Closure.IsLess(x.m_Closure); } + bool operator >(const FastDelegate3 &x) const { + return x.m_Closure.IsLess(m_Closure); } + // Binding to non-const member functions + template < class X, class Y > + FastDelegate3(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3) ) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3)) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Binding to const member functions. + template < class X, class Y > + FastDelegate3(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Static functions. We convert them into a member function call. + // This constructor also provides implicit conversion + FastDelegate3(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3) ) { + bind(function_to_bind); } + // for efficiency, prevent creation of a temporary + void operator = (DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3) ) { + bind(function_to_bind); } + inline void bind(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3)) { + m_Closure.bindstaticfunc(this, &FastDelegate3::InvokeStaticFunction, + function_to_bind); } + // Invoke the delegate + RetType operator() (Param1 p1, Param2 p2, Param3 p3) const { + return (m_Closure.GetClosureThis()->*(m_Closure.GetClosureMemPtr()))(p1, p2, p3); } + // Implicit conversion to "bool" using the safe_bool idiom +private: + typedef struct SafeBoolStruct { + int a_data_pointer_to_this_is_0_on_buggy_compilers; + StaticFunctionPtr m_nonzero; + } UselessTypedef; + typedef StaticFunctionPtr SafeBoolStruct::*unspecified_bool_type; +public: + operator unspecified_bool_type() const { + return empty()? 0: &SafeBoolStruct::m_nonzero; + } + // necessary to allow ==0 to work despite the safe_bool idiom + inline bool operator==(StaticFunctionPtr funcptr) { + return m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator!=(StaticFunctionPtr funcptr) { + return !m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator ! () const { // Is it bound to anything? + return !m_Closure; } + inline bool empty() const { + return !m_Closure; } + void clear() { m_Closure.clear();} + // Conversion to and from the DelegateMemento storage class + const DelegateMemento & GetMemento() { return m_Closure; } + void SetMemento(const DelegateMemento &any) { m_Closure.CopyFrom(this, any); } + +private: // Invoker for static functions + RetType InvokeStaticFunction(Param1 p1, Param2 p2, Param3 p3) const { + return (*(m_Closure.GetStaticFunction()))(p1, p2, p3); } +}; + +//N=4 +template +class FastDelegate4 { +private: + typedef typename detail::DefaultVoidToVoid::type DesiredRetType; + typedef DesiredRetType (*StaticFunctionPtr)(Param1 p1, Param2 p2, Param3 p3, Param4 p4); + typedef RetType (*UnvoidStaticFunctionPtr)(Param1 p1, Param2 p2, Param3 p3, Param4 p4); + typedef RetType (detail::GenericClass::*GenericMemFn)(Param1 p1, Param2 p2, Param3 p3, Param4 p4); + typedef detail::ClosurePtr ClosureType; + ClosureType m_Closure; +public: + // Typedefs to aid generic programming + typedef FastDelegate4 type; + + // Construction and comparison functions + FastDelegate4() { clear(); } + FastDelegate4(const FastDelegate4 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + void operator = (const FastDelegate4 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + bool operator ==(const FastDelegate4 &x) const { + return m_Closure.IsEqual(x.m_Closure); } + bool operator !=(const FastDelegate4 &x) const { + return !m_Closure.IsEqual(x.m_Closure); } + bool operator <(const FastDelegate4 &x) const { + return m_Closure.IsLess(x.m_Closure); } + bool operator >(const FastDelegate4 &x) const { + return x.m_Closure.IsLess(m_Closure); } + // Binding to non-const member functions + template < class X, class Y > + FastDelegate4(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4) ) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4)) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Binding to const member functions. + template < class X, class Y > + FastDelegate4(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Static functions. We convert them into a member function call. + // This constructor also provides implicit conversion + FastDelegate4(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4) ) { + bind(function_to_bind); } + // for efficiency, prevent creation of a temporary + void operator = (DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4) ) { + bind(function_to_bind); } + inline void bind(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4)) { + m_Closure.bindstaticfunc(this, &FastDelegate4::InvokeStaticFunction, + function_to_bind); } + // Invoke the delegate + RetType operator() (Param1 p1, Param2 p2, Param3 p3, Param4 p4) const { + return (m_Closure.GetClosureThis()->*(m_Closure.GetClosureMemPtr()))(p1, p2, p3, p4); } + // Implicit conversion to "bool" using the safe_bool idiom +private: + typedef struct SafeBoolStruct { + int a_data_pointer_to_this_is_0_on_buggy_compilers; + StaticFunctionPtr m_nonzero; + } UselessTypedef; + typedef StaticFunctionPtr SafeBoolStruct::*unspecified_bool_type; +public: + operator unspecified_bool_type() const { + return empty()? 0: &SafeBoolStruct::m_nonzero; + } + // necessary to allow ==0 to work despite the safe_bool idiom + inline bool operator==(StaticFunctionPtr funcptr) { + return m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator!=(StaticFunctionPtr funcptr) { + return !m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator ! () const { // Is it bound to anything? + return !m_Closure; } + inline bool empty() const { + return !m_Closure; } + void clear() { m_Closure.clear();} + // Conversion to and from the DelegateMemento storage class + const DelegateMemento & GetMemento() { return m_Closure; } + void SetMemento(const DelegateMemento &any) { m_Closure.CopyFrom(this, any); } + +private: // Invoker for static functions + RetType InvokeStaticFunction(Param1 p1, Param2 p2, Param3 p3, Param4 p4) const { + return (*(m_Closure.GetStaticFunction()))(p1, p2, p3, p4); } +}; + +//N=5 +template +class FastDelegate5 { +private: + typedef typename detail::DefaultVoidToVoid::type DesiredRetType; + typedef DesiredRetType (*StaticFunctionPtr)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5); + typedef RetType (*UnvoidStaticFunctionPtr)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5); + typedef RetType (detail::GenericClass::*GenericMemFn)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5); + typedef detail::ClosurePtr ClosureType; + ClosureType m_Closure; +public: + // Typedefs to aid generic programming + typedef FastDelegate5 type; + + // Construction and comparison functions + FastDelegate5() { clear(); } + FastDelegate5(const FastDelegate5 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + void operator = (const FastDelegate5 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + bool operator ==(const FastDelegate5 &x) const { + return m_Closure.IsEqual(x.m_Closure); } + bool operator !=(const FastDelegate5 &x) const { + return !m_Closure.IsEqual(x.m_Closure); } + bool operator <(const FastDelegate5 &x) const { + return m_Closure.IsLess(x.m_Closure); } + bool operator >(const FastDelegate5 &x) const { + return x.m_Closure.IsLess(m_Closure); } + // Binding to non-const member functions + template < class X, class Y > + FastDelegate5(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5) ) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5)) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Binding to const member functions. + template < class X, class Y > + FastDelegate5(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Static functions. We convert them into a member function call. + // This constructor also provides implicit conversion + FastDelegate5(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5) ) { + bind(function_to_bind); } + // for efficiency, prevent creation of a temporary + void operator = (DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5) ) { + bind(function_to_bind); } + inline void bind(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5)) { + m_Closure.bindstaticfunc(this, &FastDelegate5::InvokeStaticFunction, + function_to_bind); } + // Invoke the delegate + RetType operator() (Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5) const { + return (m_Closure.GetClosureThis()->*(m_Closure.GetClosureMemPtr()))(p1, p2, p3, p4, p5); } + // Implicit conversion to "bool" using the safe_bool idiom +private: + typedef struct SafeBoolStruct { + int a_data_pointer_to_this_is_0_on_buggy_compilers; + StaticFunctionPtr m_nonzero; + } UselessTypedef; + typedef StaticFunctionPtr SafeBoolStruct::*unspecified_bool_type; +public: + operator unspecified_bool_type() const { + return empty()? 0: &SafeBoolStruct::m_nonzero; + } + // necessary to allow ==0 to work despite the safe_bool idiom + inline bool operator==(StaticFunctionPtr funcptr) { + return m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator!=(StaticFunctionPtr funcptr) { + return !m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator ! () const { // Is it bound to anything? + return !m_Closure; } + inline bool empty() const { + return !m_Closure; } + void clear() { m_Closure.clear();} + // Conversion to and from the DelegateMemento storage class + const DelegateMemento & GetMemento() { return m_Closure; } + void SetMemento(const DelegateMemento &any) { m_Closure.CopyFrom(this, any); } + +private: // Invoker for static functions + RetType InvokeStaticFunction(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5) const { + return (*(m_Closure.GetStaticFunction()))(p1, p2, p3, p4, p5); } +}; + +//N=6 +template +class FastDelegate6 { +private: + typedef typename detail::DefaultVoidToVoid::type DesiredRetType; + typedef DesiredRetType (*StaticFunctionPtr)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6); + typedef RetType (*UnvoidStaticFunctionPtr)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6); + typedef RetType (detail::GenericClass::*GenericMemFn)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6); + typedef detail::ClosurePtr ClosureType; + ClosureType m_Closure; +public: + // Typedefs to aid generic programming + typedef FastDelegate6 type; + + // Construction and comparison functions + FastDelegate6() { clear(); } + FastDelegate6(const FastDelegate6 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + void operator = (const FastDelegate6 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + bool operator ==(const FastDelegate6 &x) const { + return m_Closure.IsEqual(x.m_Closure); } + bool operator !=(const FastDelegate6 &x) const { + return !m_Closure.IsEqual(x.m_Closure); } + bool operator <(const FastDelegate6 &x) const { + return m_Closure.IsLess(x.m_Closure); } + bool operator >(const FastDelegate6 &x) const { + return x.m_Closure.IsLess(m_Closure); } + // Binding to non-const member functions + template < class X, class Y > + FastDelegate6(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6) ) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6)) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Binding to const member functions. + template < class X, class Y > + FastDelegate6(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Static functions. We convert them into a member function call. + // This constructor also provides implicit conversion + FastDelegate6(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6) ) { + bind(function_to_bind); } + // for efficiency, prevent creation of a temporary + void operator = (DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6) ) { + bind(function_to_bind); } + inline void bind(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6)) { + m_Closure.bindstaticfunc(this, &FastDelegate6::InvokeStaticFunction, + function_to_bind); } + // Invoke the delegate + RetType operator() (Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6) const { + return (m_Closure.GetClosureThis()->*(m_Closure.GetClosureMemPtr()))(p1, p2, p3, p4, p5, p6); } + // Implicit conversion to "bool" using the safe_bool idiom +private: + typedef struct SafeBoolStruct { + int a_data_pointer_to_this_is_0_on_buggy_compilers; + StaticFunctionPtr m_nonzero; + } UselessTypedef; + typedef StaticFunctionPtr SafeBoolStruct::*unspecified_bool_type; +public: + operator unspecified_bool_type() const { + return empty()? 0: &SafeBoolStruct::m_nonzero; + } + // necessary to allow ==0 to work despite the safe_bool idiom + inline bool operator==(StaticFunctionPtr funcptr) { + return m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator!=(StaticFunctionPtr funcptr) { + return !m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator ! () const { // Is it bound to anything? + return !m_Closure; } + inline bool empty() const { + return !m_Closure; } + void clear() { m_Closure.clear();} + // Conversion to and from the DelegateMemento storage class + const DelegateMemento & GetMemento() { return m_Closure; } + void SetMemento(const DelegateMemento &any) { m_Closure.CopyFrom(this, any); } + +private: // Invoker for static functions + RetType InvokeStaticFunction(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6) const { + return (*(m_Closure.GetStaticFunction()))(p1, p2, p3, p4, p5, p6); } +}; + +//N=7 +template +class FastDelegate7 { +private: + typedef typename detail::DefaultVoidToVoid::type DesiredRetType; + typedef DesiredRetType (*StaticFunctionPtr)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7); + typedef RetType (*UnvoidStaticFunctionPtr)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7); + typedef RetType (detail::GenericClass::*GenericMemFn)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7); + typedef detail::ClosurePtr ClosureType; + ClosureType m_Closure; +public: + // Typedefs to aid generic programming + typedef FastDelegate7 type; + + // Construction and comparison functions + FastDelegate7() { clear(); } + FastDelegate7(const FastDelegate7 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + void operator = (const FastDelegate7 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + bool operator ==(const FastDelegate7 &x) const { + return m_Closure.IsEqual(x.m_Closure); } + bool operator !=(const FastDelegate7 &x) const { + return !m_Closure.IsEqual(x.m_Closure); } + bool operator <(const FastDelegate7 &x) const { + return m_Closure.IsLess(x.m_Closure); } + bool operator >(const FastDelegate7 &x) const { + return x.m_Closure.IsLess(m_Closure); } + // Binding to non-const member functions + template < class X, class Y > + FastDelegate7(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7) ) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7)) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Binding to const member functions. + template < class X, class Y > + FastDelegate7(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Static functions. We convert them into a member function call. + // This constructor also provides implicit conversion + FastDelegate7(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7) ) { + bind(function_to_bind); } + // for efficiency, prevent creation of a temporary + void operator = (DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7) ) { + bind(function_to_bind); } + inline void bind(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7)) { + m_Closure.bindstaticfunc(this, &FastDelegate7::InvokeStaticFunction, + function_to_bind); } + // Invoke the delegate + RetType operator() (Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7) const { + return (m_Closure.GetClosureThis()->*(m_Closure.GetClosureMemPtr()))(p1, p2, p3, p4, p5, p6, p7); } + // Implicit conversion to "bool" using the safe_bool idiom +private: + typedef struct SafeBoolStruct { + int a_data_pointer_to_this_is_0_on_buggy_compilers; + StaticFunctionPtr m_nonzero; + } UselessTypedef; + typedef StaticFunctionPtr SafeBoolStruct::*unspecified_bool_type; +public: + operator unspecified_bool_type() const { + return empty()? 0: &SafeBoolStruct::m_nonzero; + } + // necessary to allow ==0 to work despite the safe_bool idiom + inline bool operator==(StaticFunctionPtr funcptr) { + return m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator!=(StaticFunctionPtr funcptr) { + return !m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator ! () const { // Is it bound to anything? + return !m_Closure; } + inline bool empty() const { + return !m_Closure; } + void clear() { m_Closure.clear();} + // Conversion to and from the DelegateMemento storage class + const DelegateMemento & GetMemento() { return m_Closure; } + void SetMemento(const DelegateMemento &any) { m_Closure.CopyFrom(this, any); } + +private: // Invoker for static functions + RetType InvokeStaticFunction(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7) const { + return (*(m_Closure.GetStaticFunction()))(p1, p2, p3, p4, p5, p6, p7); } +}; + +//N=8 +template +class FastDelegate8 { +private: + typedef typename detail::DefaultVoidToVoid::type DesiredRetType; + typedef DesiredRetType (*StaticFunctionPtr)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8); + typedef RetType (*UnvoidStaticFunctionPtr)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8); + typedef RetType (detail::GenericClass::*GenericMemFn)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8); + typedef detail::ClosurePtr ClosureType; + ClosureType m_Closure; +public: + // Typedefs to aid generic programming + typedef FastDelegate8 type; + + // Construction and comparison functions + FastDelegate8() { clear(); } + FastDelegate8(const FastDelegate8 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + void operator = (const FastDelegate8 &x) { + m_Closure.CopyFrom(this, x.m_Closure); } + bool operator ==(const FastDelegate8 &x) const { + return m_Closure.IsEqual(x.m_Closure); } + bool operator !=(const FastDelegate8 &x) const { + return !m_Closure.IsEqual(x.m_Closure); } + bool operator <(const FastDelegate8 &x) const { + return m_Closure.IsLess(x.m_Closure); } + bool operator >(const FastDelegate8 &x) const { + return x.m_Closure.IsLess(m_Closure); } + // Binding to non-const member functions + template < class X, class Y > + FastDelegate8(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8) ) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8)) { + m_Closure.bindmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Binding to const member functions. + template < class X, class Y > + FastDelegate8(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + template < class X, class Y > + inline void bind(const Y *pthis, DesiredRetType (X::* function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8) const) { + m_Closure.bindconstmemfunc(detail::implicit_cast(pthis), function_to_bind); } + // Static functions. We convert them into a member function call. + // This constructor also provides implicit conversion + FastDelegate8(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8) ) { + bind(function_to_bind); } + // for efficiency, prevent creation of a temporary + void operator = (DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8) ) { + bind(function_to_bind); } + inline void bind(DesiredRetType (*function_to_bind)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8)) { + m_Closure.bindstaticfunc(this, &FastDelegate8::InvokeStaticFunction, + function_to_bind); } + // Invoke the delegate + RetType operator() (Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8) const { + return (m_Closure.GetClosureThis()->*(m_Closure.GetClosureMemPtr()))(p1, p2, p3, p4, p5, p6, p7, p8); } + // Implicit conversion to "bool" using the safe_bool idiom +private: + typedef struct SafeBoolStruct { + int a_data_pointer_to_this_is_0_on_buggy_compilers; + StaticFunctionPtr m_nonzero; + } UselessTypedef; + typedef StaticFunctionPtr SafeBoolStruct::*unspecified_bool_type; +public: + operator unspecified_bool_type() const { + return empty()? 0: &SafeBoolStruct::m_nonzero; + } + // necessary to allow ==0 to work despite the safe_bool idiom + inline bool operator==(StaticFunctionPtr funcptr) { + return m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator!=(StaticFunctionPtr funcptr) { + return !m_Closure.IsEqualToStaticFuncPtr(funcptr); } + inline bool operator ! () const { // Is it bound to anything? + return !m_Closure; } + inline bool empty() const { + return !m_Closure; } + void clear() { m_Closure.clear();} + // Conversion to and from the DelegateMemento storage class + const DelegateMemento & GetMemento() { return m_Closure; } + void SetMemento(const DelegateMemento &any) { m_Closure.CopyFrom(this, any); } + +private: // Invoker for static functions + RetType InvokeStaticFunction(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8) const { + return (*(m_Closure.GetStaticFunction()))(p1, p2, p3, p4, p5, p6, p7, p8); } +}; + + +//////////////////////////////////////////////////////////////////////////////// +// Fast Delegates, part 4: +// +// FastDelegate<> class (Original author: Jody Hagins) +// Allows boost::function style syntax like: +// FastDelegate< double (int, long) > +// instead of: +// FastDelegate2< int, long, double > +// +//////////////////////////////////////////////////////////////////////////////// + +#ifdef FASTDELEGATE_ALLOW_FUNCTION_TYPE_SYNTAX + +// Declare FastDelegate as a class template. It will be specialized +// later for all number of arguments. +template +class FastDelegate; + +//N=0 +// Specialization to allow use of +// FastDelegate< R ( ) > +// instead of +// FastDelegate0 < R > +template +class FastDelegate< R ( ) > + // Inherit from FastDelegate0 so that it can be treated just like a FastDelegate0 + : public FastDelegate0 < R > +{ +public: + // Make using the base type a bit easier via typedef. + typedef FastDelegate0 < R > BaseType; + + // Allow users access to the specific type of this delegate. + typedef FastDelegate SelfType; + + // Mimic the base class constructors. + FastDelegate() : BaseType() { } + + template < class X, class Y > + FastDelegate(Y * pthis, + R (X::* function_to_bind)( )) + : BaseType(pthis, function_to_bind) { } + + template < class X, class Y > + FastDelegate(const Y *pthis, + R (X::* function_to_bind)( ) const) + : BaseType(pthis, function_to_bind) + { } + + FastDelegate(R (*function_to_bind)( )) + : BaseType(function_to_bind) { } + void operator = (const BaseType &x) { + *static_cast(this) = x; } +}; + +//N=1 +// Specialization to allow use of +// FastDelegate< R ( Param1 ) > +// instead of +// FastDelegate1 < Param1, R > +template +class FastDelegate< R ( Param1 ) > + // Inherit from FastDelegate1 so that it can be treated just like a FastDelegate1 + : public FastDelegate1 < Param1, R > +{ +public: + // Make using the base type a bit easier via typedef. + typedef FastDelegate1 < Param1, R > BaseType; + + // Allow users access to the specific type of this delegate. + typedef FastDelegate SelfType; + + // Mimic the base class constructors. + FastDelegate() : BaseType() { } + + template < class X, class Y > + FastDelegate(Y * pthis, + R (X::* function_to_bind)( Param1 p1 )) + : BaseType(pthis, function_to_bind) { } + + template < class X, class Y > + FastDelegate(const Y *pthis, + R (X::* function_to_bind)( Param1 p1 ) const) + : BaseType(pthis, function_to_bind) + { } + + FastDelegate(R (*function_to_bind)( Param1 p1 )) + : BaseType(function_to_bind) { } + void operator = (const BaseType &x) { + *static_cast(this) = x; } +}; + +//N=2 +// Specialization to allow use of +// FastDelegate< R ( Param1, Param2 ) > +// instead of +// FastDelegate2 < Param1, Param2, R > +template +class FastDelegate< R ( Param1, Param2 ) > + // Inherit from FastDelegate2 so that it can be treated just like a FastDelegate2 + : public FastDelegate2 < Param1, Param2, R > +{ +public: + // Make using the base type a bit easier via typedef. + typedef FastDelegate2 < Param1, Param2, R > BaseType; + + // Allow users access to the specific type of this delegate. + typedef FastDelegate SelfType; + + // Mimic the base class constructors. + FastDelegate() : BaseType() { } + + template < class X, class Y > + FastDelegate(Y * pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2 )) + : BaseType(pthis, function_to_bind) { } + + template < class X, class Y > + FastDelegate(const Y *pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2 ) const) + : BaseType(pthis, function_to_bind) + { } + + FastDelegate(R (*function_to_bind)( Param1 p1, Param2 p2 )) + : BaseType(function_to_bind) { } + void operator = (const BaseType &x) { + *static_cast(this) = x; } +}; + +//N=3 +// Specialization to allow use of +// FastDelegate< R ( Param1, Param2, Param3 ) > +// instead of +// FastDelegate3 < Param1, Param2, Param3, R > +template +class FastDelegate< R ( Param1, Param2, Param3 ) > + // Inherit from FastDelegate3 so that it can be treated just like a FastDelegate3 + : public FastDelegate3 < Param1, Param2, Param3, R > +{ +public: + // Make using the base type a bit easier via typedef. + typedef FastDelegate3 < Param1, Param2, Param3, R > BaseType; + + // Allow users access to the specific type of this delegate. + typedef FastDelegate SelfType; + + // Mimic the base class constructors. + FastDelegate() : BaseType() { } + + template < class X, class Y > + FastDelegate(Y * pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2, Param3 p3 )) + : BaseType(pthis, function_to_bind) { } + + template < class X, class Y > + FastDelegate(const Y *pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2, Param3 p3 ) const) + : BaseType(pthis, function_to_bind) + { } + + FastDelegate(R (*function_to_bind)( Param1 p1, Param2 p2, Param3 p3 )) + : BaseType(function_to_bind) { } + void operator = (const BaseType &x) { + *static_cast(this) = x; } +}; + +//N=4 +// Specialization to allow use of +// FastDelegate< R ( Param1, Param2, Param3, Param4 ) > +// instead of +// FastDelegate4 < Param1, Param2, Param3, Param4, R > +template +class FastDelegate< R ( Param1, Param2, Param3, Param4 ) > + // Inherit from FastDelegate4 so that it can be treated just like a FastDelegate4 + : public FastDelegate4 < Param1, Param2, Param3, Param4, R > +{ +public: + // Make using the base type a bit easier via typedef. + typedef FastDelegate4 < Param1, Param2, Param3, Param4, R > BaseType; + + // Allow users access to the specific type of this delegate. + typedef FastDelegate SelfType; + + // Mimic the base class constructors. + FastDelegate() : BaseType() { } + + template < class X, class Y > + FastDelegate(Y * pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4 )) + : BaseType(pthis, function_to_bind) { } + + template < class X, class Y > + FastDelegate(const Y *pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4 ) const) + : BaseType(pthis, function_to_bind) + { } + + FastDelegate(R (*function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4 )) + : BaseType(function_to_bind) { } + void operator = (const BaseType &x) { + *static_cast(this) = x; } +}; + +//N=5 +// Specialization to allow use of +// FastDelegate< R ( Param1, Param2, Param3, Param4, Param5 ) > +// instead of +// FastDelegate5 < Param1, Param2, Param3, Param4, Param5, R > +template +class FastDelegate< R ( Param1, Param2, Param3, Param4, Param5 ) > + // Inherit from FastDelegate5 so that it can be treated just like a FastDelegate5 + : public FastDelegate5 < Param1, Param2, Param3, Param4, Param5, R > +{ +public: + // Make using the base type a bit easier via typedef. + typedef FastDelegate5 < Param1, Param2, Param3, Param4, Param5, R > BaseType; + + // Allow users access to the specific type of this delegate. + typedef FastDelegate SelfType; + + // Mimic the base class constructors. + FastDelegate() : BaseType() { } + + template < class X, class Y > + FastDelegate(Y * pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5 )) + : BaseType(pthis, function_to_bind) { } + + template < class X, class Y > + FastDelegate(const Y *pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5 ) const) + : BaseType(pthis, function_to_bind) + { } + + FastDelegate(R (*function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5 )) + : BaseType(function_to_bind) { } + void operator = (const BaseType &x) { + *static_cast(this) = x; } +}; + +//N=6 +// Specialization to allow use of +// FastDelegate< R ( Param1, Param2, Param3, Param4, Param5, Param6 ) > +// instead of +// FastDelegate6 < Param1, Param2, Param3, Param4, Param5, Param6, R > +template +class FastDelegate< R ( Param1, Param2, Param3, Param4, Param5, Param6 ) > + // Inherit from FastDelegate6 so that it can be treated just like a FastDelegate6 + : public FastDelegate6 < Param1, Param2, Param3, Param4, Param5, Param6, R > +{ +public: + // Make using the base type a bit easier via typedef. + typedef FastDelegate6 < Param1, Param2, Param3, Param4, Param5, Param6, R > BaseType; + + // Allow users access to the specific type of this delegate. + typedef FastDelegate SelfType; + + // Mimic the base class constructors. + FastDelegate() : BaseType() { } + + template < class X, class Y > + FastDelegate(Y * pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6 )) + : BaseType(pthis, function_to_bind) { } + + template < class X, class Y > + FastDelegate(const Y *pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6 ) const) + : BaseType(pthis, function_to_bind) + { } + + FastDelegate(R (*function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6 )) + : BaseType(function_to_bind) { } + void operator = (const BaseType &x) { + *static_cast(this) = x; } +}; + +//N=7 +// Specialization to allow use of +// FastDelegate< R ( Param1, Param2, Param3, Param4, Param5, Param6, Param7 ) > +// instead of +// FastDelegate7 < Param1, Param2, Param3, Param4, Param5, Param6, Param7, R > +template +class FastDelegate< R ( Param1, Param2, Param3, Param4, Param5, Param6, Param7 ) > + // Inherit from FastDelegate7 so that it can be treated just like a FastDelegate7 + : public FastDelegate7 < Param1, Param2, Param3, Param4, Param5, Param6, Param7, R > +{ +public: + // Make using the base type a bit easier via typedef. + typedef FastDelegate7 < Param1, Param2, Param3, Param4, Param5, Param6, Param7, R > BaseType; + + // Allow users access to the specific type of this delegate. + typedef FastDelegate SelfType; + + // Mimic the base class constructors. + FastDelegate() : BaseType() { } + + template < class X, class Y > + FastDelegate(Y * pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7 )) + : BaseType(pthis, function_to_bind) { } + + template < class X, class Y > + FastDelegate(const Y *pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7 ) const) + : BaseType(pthis, function_to_bind) + { } + + FastDelegate(R (*function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7 )) + : BaseType(function_to_bind) { } + void operator = (const BaseType &x) { + *static_cast(this) = x; } +}; + +//N=8 +// Specialization to allow use of +// FastDelegate< R ( Param1, Param2, Param3, Param4, Param5, Param6, Param7, Param8 ) > +// instead of +// FastDelegate8 < Param1, Param2, Param3, Param4, Param5, Param6, Param7, Param8, R > +template +class FastDelegate< R ( Param1, Param2, Param3, Param4, Param5, Param6, Param7, Param8 ) > + // Inherit from FastDelegate8 so that it can be treated just like a FastDelegate8 + : public FastDelegate8 < Param1, Param2, Param3, Param4, Param5, Param6, Param7, Param8, R > +{ +public: + // Make using the base type a bit easier via typedef. + typedef FastDelegate8 < Param1, Param2, Param3, Param4, Param5, Param6, Param7, Param8, R > BaseType; + + // Allow users access to the specific type of this delegate. + typedef FastDelegate SelfType; + + // Mimic the base class constructors. + FastDelegate() : BaseType() { } + + template < class X, class Y > + FastDelegate(Y * pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8 )) + : BaseType(pthis, function_to_bind) { } + + template < class X, class Y > + FastDelegate(const Y *pthis, + R (X::* function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8 ) const) + : BaseType(pthis, function_to_bind) + { } + + FastDelegate(R (*function_to_bind)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8 )) + : BaseType(function_to_bind) { } + void operator = (const BaseType &x) { + *static_cast(this) = x; } +}; + + +#endif //FASTDELEGATE_ALLOW_FUNCTION_TYPE_SYNTAX + +//////////////////////////////////////////////////////////////////////////////// +// Fast Delegates, part 5: +// +// MakeDelegate() helper function +// +// MakeDelegate(&x, &X::func) returns a fastdelegate of the type +// necessary for calling x.func() with the correct number of arguments. +// This makes it possible to eliminate many typedefs from user code. +// +//////////////////////////////////////////////////////////////////////////////// + +// Also declare overloads of a MakeDelegate() global function to +// reduce the need for typedefs. +// We need separate overloads for const and non-const member functions. +// Also, because of the weird rule about the class of derived member function pointers, +// implicit downcasts may need to be applied later to the 'this' pointer. +// That's why two classes (X and Y) appear in the definitions. Y must be implicitly +// castable to X. + +// Workaround for VC6. VC6 needs void return types converted into DefaultVoid. +// GCC 3.2 and later won't compile this unless it's preceded by 'typename', +// but VC6 doesn't allow 'typename' in this context. +// So, I have to use a macro. + +#ifdef FASTDLGT_VC6 +#define FASTDLGT_RETTYPE detail::VoidToDefaultVoid::type +#else +#define FASTDLGT_RETTYPE RetType +#endif + +//N=0 +template +FastDelegate0 MakeDelegate(Y* x, RetType (X::*func)()) { + return FastDelegate0(x, func); +} + +template +FastDelegate0 MakeDelegate(Y* x, RetType (X::*func)() const) { + return FastDelegate0(x, func); +} + +//N=1 +template +FastDelegate1 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1)) { + return FastDelegate1(x, func); +} + +template +FastDelegate1 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1) const) { + return FastDelegate1(x, func); +} + +//N=2 +template +FastDelegate2 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2)) { + return FastDelegate2(x, func); +} + +template +FastDelegate2 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2) const) { + return FastDelegate2(x, func); +} + +//N=3 +template +FastDelegate3 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2, Param3 p3)) { + return FastDelegate3(x, func); +} + +template +FastDelegate3 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2, Param3 p3) const) { + return FastDelegate3(x, func); +} + +//N=4 +template +FastDelegate4 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2, Param3 p3, Param4 p4)) { + return FastDelegate4(x, func); +} + +template +FastDelegate4 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2, Param3 p3, Param4 p4) const) { + return FastDelegate4(x, func); +} + +//N=5 +template +FastDelegate5 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5)) { + return FastDelegate5(x, func); +} + +template +FastDelegate5 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5) const) { + return FastDelegate5(x, func); +} + +//N=6 +template +FastDelegate6 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6)) { + return FastDelegate6(x, func); +} + +template +FastDelegate6 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6) const) { + return FastDelegate6(x, func); +} + +//N=7 +template +FastDelegate7 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7)) { + return FastDelegate7(x, func); +} + +template +FastDelegate7 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7) const) { + return FastDelegate7(x, func); +} + +//N=8 +template +FastDelegate8 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8)) { + return FastDelegate8(x, func); +} + +template +FastDelegate8 MakeDelegate(Y* x, RetType (X::*func)(Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8) const) { + return FastDelegate8(x, func); +} + + + // clean up after ourselves... +#undef FASTDLGT_RETTYPE + +} // namespace fastdelegate + +#endif // !defined(FASTDELEGATE_H) + diff --git a/libs/Common/FastDelegateBind.h b/libs/Common/FastDelegateBind.h new file mode 100644 index 0000000..95b017d --- /dev/null +++ b/libs/Common/FastDelegateBind.h @@ -0,0 +1,240 @@ +// FastDelegateBind.h +// Helper file for FastDelegates. Provides bind() function, enabling +// FastDelegates to be rapidly compared to programs using boost::function and boost::bind. +// +// Documentation is found at http://www.codeproject.com/cpp/FastDelegate.asp +// +// Original author: Jody Hagins. +// Minor changes by Don Clugston. +// +// Warning: The arguments to 'bind' are ignored! No actual binding is performed. +// The behaviour is equivalent to boost::bind only when the basic placeholder +// arguments _1, _2, _3, etc are used in order. +// +// HISTORY: +// 1.4 Dec 2004. Initial release as part of FastDelegate 1.4. + + +#ifndef FASTDELEGATEBIND_H +#define FASTDELEGATEBIND_H + +//////////////////////////////////////////////////////////////////////////////// +// FastDelegate bind() +// +// bind() helper function for boost compatibility. +// (Original author: Jody Hagins). +// +// Add another helper, so FastDelegate can be a dropin replacement +// for boost::bind (in a fair number of cases). +// Note the ellipsis, because boost::bind() takes place holders +// but FastDelegate does not care about them. Getting the place holder +// mechanism to work, and play well with boost is a bit tricky, so +// we do the "easy" thing... +// Assume we have the following code... +// using boost::bind; +// bind(&Foo:func, &foo, _1, _2); +// we should be able to replace the "using" with... +// using fastdelegate::bind; +// and everything should work fine... +//////////////////////////////////////////////////////////////////////////////// + +#ifdef FASTDELEGATE_ALLOW_FUNCTION_TYPE_SYNTAX + +namespace fastdelegate { + +//N=0 +template +FastDelegate< RetType ( ) > +bind( + RetType (X::*func)( ), + Y * y, + ...) +{ + return FastDelegate< RetType ( ) >(y, func); +} + +template +FastDelegate< RetType ( ) > +bind( + RetType (X::*func)( ) const, + Y * y, + ...) +{ + return FastDelegate< RetType ( ) >(y, func); +} + +//N=1 +template +FastDelegate< RetType ( Param1 p1 ) > +bind( + RetType (X::*func)( Param1 p1 ), + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1 ) >(y, func); +} + +template +FastDelegate< RetType ( Param1 p1 ) > +bind( + RetType (X::*func)( Param1 p1 ) const, + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1 ) >(y, func); +} + +//N=2 +template +FastDelegate< RetType ( Param1 p1, Param2 p2 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2 ), + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2 ) >(y, func); +} + +template +FastDelegate< RetType ( Param1 p1, Param2 p2 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2 ) const, + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2 ) >(y, func); +} + +//N=3 +template +FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2, Param3 p3 ), + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3 ) >(y, func); +} + +template +FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2, Param3 p3 ) const, + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3 ) >(y, func); +} + +//N=4 +template +FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2, Param3 p3, Param4 p4 ), + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4 ) >(y, func); +} + +template +FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2, Param3 p3, Param4 p4 ) const, + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4 ) >(y, func); +} + +//N=5 +template +FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5 ), + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5 ) >(y, func); +} + +template +FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5 ) const, + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5 ) >(y, func); +} + +//N=6 +template +FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6 ), + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6 ) >(y, func); +} + +template +FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6 ) const, + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6 ) >(y, func); +} + +//N=7 +template +FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7 ), + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7 ) >(y, func); +} + +template +FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7 ) const, + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7 ) >(y, func); +} + +//N=8 +template +FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8 ), + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8 ) >(y, func); +} + +template +FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8 ) > +bind( + RetType (X::*func)( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8 ) const, + Y * y, + ...) +{ + return FastDelegate< RetType ( Param1 p1, Param2 p2, Param3 p3, Param4 p4, Param5 p5, Param6 p6, Param7 p7, Param8 p8 ) >(y, func); +} + + +#endif //FASTDELEGATE_ALLOW_FUNCTION_TYPE_SYNTAX + +} // namespace fastdelegate + +#endif // !defined(FASTDELEGATEBIND_H) + diff --git a/libs/Common/FastDelegateCPP11.h b/libs/Common/FastDelegateCPP11.h new file mode 100644 index 0000000..26a565b --- /dev/null +++ b/libs/Common/FastDelegateCPP11.h @@ -0,0 +1,379 @@ +/** \file SRDelegate.hpp + * + * This is a C++11 implementation by janezz55(code.google) for the original "The Impossibly Fast C++ Delegates" authored by Sergey Ryazanov. + * + * This is a copy checkouted from https://code.google.com/p/cpppractice/source/browse/trunk/delegate.hpp on 2014/06/07. + * Last change in the chunk was r370 on Feb 9, 2014. + * + * The following modifications were added by Benjamin YanXiang Huang + * - replace light_ptr with std::shared_ptr + * - renamed src file + * + * Reference: + * - http://codereview.stackexchange.com/questions/14730/impossibly-fast-delegate-in-c11 + * - http://www.codeproject.com/Articles/11015/The-Impossibly-Fast-C-Delegates + * - https://code.google.com/p/cpppractice/source/browse/trunk/delegate.hpp +*/ + +#pragma once +#ifndef SRDELEGATE_HPP +#define SRDELEGATE_HPP + +#include +#include +#include +#include +#include +#include + +// VC work around for constexpr and noexcept: VC2013 and below do not support these 2 keywords +#if defined(_MSC_VER) && (_MSC_VER <= 1800) +#define constexpr const +#define noexcept throw() +#endif + +namespace fastdelegate +{ + +template class delegate; + +template +class delegate +{ + using stub_ptr_type = R(*)(void *, A&&...); + + delegate(void * const o, stub_ptr_type const m) noexcept : object_ptr_(o), stub_ptr_(m) {} + +public: + delegate(void) = default; + + delegate(delegate const &) = default; + + delegate(delegate && d) + : object_ptr_(d.object_ptr_), stub_ptr_(d.stub_ptr_), deleter_(d.deleter_), store_(d.store_), store_size_(d.store_size_) + { + d.object_ptr_ = nullptr; + d.stub_ptr_ = nullptr; + d.deleter_ = nullptr; + d.store_ = nullptr; + d.store_size_ = 0; + } + + delegate(::std::nullptr_t const) noexcept : delegate() { } + + template ::value, C>::type> + explicit delegate(C const * const o) noexcept : + object_ptr_(const_cast(o)) + {} + + template {}>::type> + explicit delegate(C const & o) noexcept : + object_ptr_(const_cast(&o)) + {} + + template + delegate(C * const object_ptr, R(C::* const method_ptr)(A...)) + { + *this = from(object_ptr, method_ptr); + } + + template + delegate(C * const object_ptr, R(C::* const method_ptr)(A...) const) + { + *this = from(object_ptr, method_ptr); + } + + template + delegate(C & object, R(C::* const method_ptr)(A...)) + { + *this = from(object, method_ptr); + } + + template + delegate(C const & object, R(C::* const method_ptr)(A...) const) + { + *this = from(object, method_ptr); + } + + template < + typename T, + typename = typename ::std::enable_if::type>::value>::type + > + delegate(T&& f) + : store_(operator new(sizeof(typename ::std::decay::type)) + , functor_deleter::type>) + , store_size_(sizeof(typename ::std::decay::type)) + { + using functor_type = typename ::std::decay::type; + + new(store_.get()) functor_type(::std::forward(f)); + object_ptr_ = store_.get(); + + stub_ptr_ = functor_stub; + deleter_ = deleter_stub; + } + + delegate & operator=(delegate const &) = default; + + delegate & operator=(delegate&& d) + { + object_ptr_ = d.object_ptr_; + stub_ptr_ = d.stub_ptr_; + deleter_ = d.deleter_; + store_ = d.store_; + store_size_ = d.store_size_; + + d.object_ptr_ = nullptr; + d.stub_ptr_ = nullptr; + d.deleter_ = nullptr; + d.store_ = nullptr; + d.store_size_ = 0; + + return *this; + } + + template + delegate & operator=(R(C::* const rhs)(A...)) + { + return *this = from(static_cast(object_ptr_), rhs); + } + + template + delegate & operator=(R(C::* const rhs)(A...) const) + { + return *this = from(static_cast(object_ptr_), rhs); + } + + template < + typename T + , typename = typename ::std::enable_if::type>::value>::type + > + delegate & operator=(T&& f) + { + using functor_type = typename ::std::decay::type; + + if ((sizeof(functor_type) > store_size_) || !store_.unique()) + { + store_.reset(operator new(sizeof(functor_type)), functor_deleter); + store_size_ = sizeof(functor_type); + } + else + deleter_(store_.get()); + + new(store_.get()) functor_type(::std::forward(f)); + object_ptr_ = store_.get(); + + stub_ptr_ = functor_stub; + deleter_ = deleter_stub; + + return *this; + } + + template + static delegate from(void) noexcept + { + return { nullptr, function_stub }; + } + + template + static delegate from(C * const object_ptr) noexcept + { + return { object_ptr, method_stub }; + } + + template + static delegate from(C const * const object_ptr) noexcept + { + return { const_cast(object_ptr), const_method_stub }; + } + + template + static delegate from(C & object) noexcept + { + return { &object, method_stub }; + } + + template + static delegate from(C const & object) noexcept + { + return { const_cast(&object), const_method_stub }; + } + + template + static delegate from(T && f) + { + return ::std::forward(f); + } + + static delegate from(R(* const function_ptr)(A...)) + { + return function_ptr; + } + + template + using member_pair = ::std::pair; + + template + using const_member_pair = ::std::pair; + + template + static delegate from(C * const object_ptr, R(C::* const method_ptr)(A...)) + { + return member_pair(object_ptr, method_ptr); + } + + template + static delegate from(C const * const object_ptr, R(C::* const method_ptr)(A...) const) + { + return const_member_pair(object_ptr, method_ptr); + } + + template + static delegate from(C & object, R(C::* const method_ptr)(A...)) + { + return member_pair(&object, method_ptr); + } + + template + static delegate from(C const & object, R(C::* const method_ptr)(A...) const) + { + return const_member_pair(&object, method_ptr); + } + + void reset(void) + { + stub_ptr_ = nullptr; + store_.reset(); + } + + void reset_stub(void) noexcept { stub_ptr_ = nullptr; } + + void swap(delegate & other) noexcept { ::std::swap(*this, other); } + + bool operator==(delegate const & rhs) const noexcept + { + // comparison between functor and non-functor is left as undefined at the moment. + if (store_size_ && rhs.store_size_) // both functors + return (std::memcmp(store_.get(), rhs.store_.get(), store_size_) == 0) && (stub_ptr_ == rhs.stub_ptr_); + return (object_ptr_ == rhs.object_ptr_) && (stub_ptr_ == rhs.stub_ptr_); + } + + bool operator!=(delegate const & rhs) const noexcept + { + return !operator==(rhs); + } + + bool operator<(delegate const & rhs) const noexcept + { + return (object_ptr_ < rhs.object_ptr_) || + ((object_ptr_ == rhs.object_ptr_) && (stub_ptr_ < rhs.stub_ptr_)); + } + + bool operator==(::std::nullptr_t const) const noexcept + { + return !stub_ptr_; + } + + bool operator!=(::std::nullptr_t const) const noexcept + { + return stub_ptr_; + } + + explicit operator bool() const noexcept + { + return stub_ptr_; + } + + R operator()(A... args) const + { + // assert(stub_ptr); + return stub_ptr_(object_ptr_, ::std::forward(args)...); + } + +private: + friend struct ::std::hash; + + using deleter_type = void (*)(void *); + + void * object_ptr_ = nullptr; + stub_ptr_type stub_ptr_ {}; + + deleter_type deleter_ = nullptr; + + ::std::shared_ptr store_ = nullptr; + ::std::size_t store_size_ = 0; + + template + static void functor_deleter(void * const p) + { + static_cast(p)->~T(); + operator delete(p); + } + + template + static void deleter_stub(void * const p) + { + static_cast(p)->~T(); + } + + template + static R function_stub(void * const, A && ... args) + { + return function_ptr(::std::forward(args)...); + } + + template + static R method_stub(void * const object_ptr, A && ... args) + { + return (static_cast(object_ptr)->*method_ptr)(::std::forward(args)...); + } + + template + static R const_method_stub(void * const object_ptr, A && ... args) + { + return (static_cast(object_ptr)->*method_ptr)(::std::forward(args)...); + } + + template + struct is_member_pair : ::std::false_type { }; + + template + struct is_member_pair< ::std::pair > : ::std::true_type {}; + + template + struct is_const_member_pair : ::std::false_type { }; + + template + struct is_const_member_pair< ::std::pair > : ::std::true_type {}; + + template + static typename ::std::enable_if::value || is_const_member_pair::value), R>::type + functor_stub(void * const object_ptr, A && ... args) + { + return (*static_cast(object_ptr))(::std::forward(args)...); + } + + template + static typename ::std::enable_if::value || is_const_member_pair::value, R>::type + functor_stub(void * const object_ptr, A && ... args) + { + return (static_cast(object_ptr)->first->*static_cast(object_ptr)->second)(::std::forward(args)...); + } +}; + +} + +namespace std +{ +template +struct hash<::fastdelegate::delegate > +{ + size_t operator()(::fastdelegate::delegate const & d) const noexcept + { + auto const seed(hash()(d.object_ptr_)); + return hash()(d.stub_ptr_) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + } +}; +} + +#endif // SRDELEGATE_HPP diff --git a/libs/Common/File.h b/libs/Common/File.h new file mode 100644 index 0000000..2e431bc --- /dev/null +++ b/libs/Common/File.h @@ -0,0 +1,686 @@ +//////////////////////////////////////////////////////////////////// +// File.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_FILE_H__ +#define __SEACAVE_FILE_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Streams.h" + +// Under both Windows and Unix, the stat function is used for classification + +// Under Gnu/Linux, the following classifications are defined +// source: Gnu/Linux man page for stat(2) http://linux.die.net/man/2/stat +// S_IFMT 0170000 bitmask for the file type bitfields +// S_IFSOCK 0140000 socket (Note this overlaps with S_IFDIR) +// S_IFLNK 0120000 symbolic link +// S_IFREG 0100000 regular file +// S_IFBLK 0060000 block device +// S_IFDIR 0040000 directory +// S_IFCHR 0020000 character device +// S_IFIFO 0010000 FIFO +// There are also some Posix-standard macros: +// S_ISREG(m) is it a regular file? +// S_ISDIR(m) directory? +// S_ISCHR(m) character device? +// S_ISBLK(m) block device? +// S_ISFIFO(m) FIFO (named pipe)? +// S_ISLNK(m) symbolic link? (Not in POSIX.1-1996.) +// S_ISSOCK(m) socket? (Not in POSIX.1-1996.) +// Under Windows, the following are defined: +// source: Header file sys/stat.h distributed with Visual Studio 10 +// _S_IFMT (S_IFMT) 0xF000 file type mask +// _S_IFREG (S_IFREG) 0x8000 regular +// _S_IFDIR (S_IFDIR) 0x4000 directory +// _S_IFCHR (S_IFCHR) 0x2000 character special +// _S_IFIFO 0x1000 pipe + +#ifdef _MSC_VER +#include +// file type tests are not defined for some reason on Windows despite them providing the stat() function! +#define F_OK 0 +#define X_OK 1 +#define W_OK 2 +#define R_OK 4 +// Posix-style macros for Windows +#ifndef S_ISREG +#define S_ISREG(mode) ((mode & _S_IFMT) == _S_IFREG) +#endif +#ifndef S_ISDIR +#define S_ISDIR(mode) ((mode & _S_IFMT) == _S_IFDIR) +#endif +#ifndef S_ISCHR +#define S_ISCHR(mode) ((mode & _S_IFMT) == _S_IFCHR) +#endif +#ifndef S_ISBLK +#define S_ISBLK(mode) (false) +#endif +#ifndef S_ISFIFO +#define S_ISFIFO(mode) ((mode & _S_IFMT) == _S_IFIFO) +#endif +#ifndef S_ISLNK +#define S_ISLNK(mode) (false) +#endif +#ifndef S_ISSOCK +#define S_ISSOCK(mode) (false) +#endif +#else +#include +#include +#define _taccess access +#endif +#ifdef __APPLE__ +#define fdatasync fsync +#endif + + +// D E F I N E S /////////////////////////////////////////////////// + +// size of the stored file size variable +#ifdef LARGEFILESIZE +#define FILESIZE size_f_t +#else +#define FILESIZE size_t +#endif + +// invalid file handle +#ifdef _MSC_VER +#define FILE_INVALID_HANDLE INVALID_HANDLE_VALUE +#else +#define FILE_INVALID_HANDLE int(-1) +#endif + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class GENERAL_API File : public IOStream { +public: + typedef struct FILEINFO_TYPE { + String path; + FILESIZE size; + DWORD attrib; + } FILEINFO; + typedef cList FileInfoArr; + + typedef enum FMCREATE_TYPE { + OPEN = 0x01, + CREATE = 0x02, + TRUNCATE = 0x04 + } FMCREATE; + + typedef enum FMFLAGS_TYPE { + SYNC = 0x01, + NOBUFFER = 0x02, + RANDOM = 0x04, + SEQUENTIAL = 0x08 + } FMFLAGS; + + inline File() : h(FILE_INVALID_HANDLE) { + #ifndef _RELEASE + breakRead = -1; + breakWrite = -1; + #endif + } + inline File(LPCTSTR aFileName, int access, int mode, int flags=0) : h(FILE_INVALID_HANDLE) { + #ifndef _RELEASE + breakRead = -1; + breakWrite = -1; + #endif + File::open(aFileName, access, mode, flags); + } + + virtual ~File() { + File::close(); + } + + #ifdef _SUPPORT_CPP11 + inline File(File&& rhs) : h(rhs.h) { + #ifndef _RELEASE + breakRead = rhs.breakRead; + breakWrite = rhs.breakWrite; + #endif + rhs.h = FILE_INVALID_HANDLE; + } + + inline File& operator=(File&& rhs) { + h = rhs.h; + #ifndef _RELEASE + breakRead = rhs.breakRead; + breakWrite = rhs.breakWrite; + #endif + rhs.h = FILE_INVALID_HANDLE; + return *this; + } + #endif + + bool isOpen() const { + return h != FILE_INVALID_HANDLE; + } + +#ifdef _MSC_VER + typedef enum FMACCESS_TYPE { + READ = GENERIC_READ, + WRITE = GENERIC_WRITE, + RW = READ | WRITE + } FMACCESS; + + typedef enum FMCHECKACCESS_TYPE { + CA_EXIST = F_OK, // existence + CA_WRITE = W_OK, // write + CA_READ = R_OK, // read + CA_RW = R_OK | W_OK, + } FMCHECKACCESS; + + /** + * Open the file specified. + * If there are errors, h is set to FILE_INVALID_HANDLE. + * Use isOpen() to check. + */ + virtual void open(LPCTSTR aFileName, int access, int mode, int flags=0) { + ASSERT(access == WRITE || access == READ || access == (READ | WRITE)); + + close(); + + DWORD m = 0; + if (mode & OPEN) { + if (mode & CREATE) { + m = (mode & TRUNCATE) ? CREATE_ALWAYS : OPEN_ALWAYS; + } else { + m = (mode & TRUNCATE) ? TRUNCATE_EXISTING : OPEN_EXISTING; + } + } else { + ASSERT(mode & CREATE); + m = (mode & TRUNCATE) ? CREATE_ALWAYS : CREATE_NEW; + } + + DWORD f = 0; + if (flags & SYNC) + f |= FILE_FLAG_WRITE_THROUGH; + if (flags & NOBUFFER) + f |= FILE_FLAG_NO_BUFFERING; + if (flags & RANDOM) + f |= FILE_FLAG_RANDOM_ACCESS; + if (flags & SEQUENTIAL) + f |= FILE_FLAG_SEQUENTIAL_SCAN; + + h = ::CreateFile(aFileName, access, FILE_SHARE_READ, NULL, m, f, NULL); + } + + virtual void close() { + if (isOpen()) { + FlushFileBuffers(h); + CloseHandle(h); + h = FILE_INVALID_HANDLE; + } + } + + uint32_t getLastModified() { + ASSERT(isOpen()); + FILETIME f = {0}; + ::GetFileTime(h, NULL, NULL, &f); + return convertTime(&f); + } + + static uint32_t convertTime(FILETIME* f) { + SYSTEMTIME s = { 1970, 1, 0, 1, 0, 0, 0, 0 }; + FILETIME f2 = {0}; + if (::SystemTimeToFileTime(&s, &f2)) { + uint64_t* a = (uint64_t*)f; + uint64_t* b = (uint64_t*)&f2; + *a -= *b; + *a /= (1000LL*1000LL*1000LL/100LL); // 100ns > s + return (uint32_t)*a; + } + return 0; + } + + size_f_t getSize() const override { + ASSERT(isOpen()); + DWORD x; + DWORD l = ::GetFileSize(h, &x); + if ((l == INVALID_FILE_SIZE) && (GetLastError() != NO_ERROR)) + return SIZE_NA; + return (size_f_t)l | ((size_f_t)x)<<32; + } + + virtual bool setSize(size_f_t newSize) { + const size_f_t pos = getPos(); + if (pos == SIZE_NA) + return false; + if (!setPos(newSize)) + return false; + if (!setEOF()) + return false; + if (!setPos(pos)) + return false; + return true; + } + + size_f_t getPos() const override { + ASSERT(isOpen()); + LONG x = 0; + const DWORD l = ::SetFilePointer(h, 0, &x, FILE_CURRENT); + if (l == INVALID_SET_FILE_POINTER) + return SIZE_NA; + return (size_f_t)l | ((size_f_t)x)<<32; + } + + bool setPos(size_f_t pos) override { + ASSERT(isOpen()); + LONG x = (LONG) (pos>>32); + return (::SetFilePointer(h, (DWORD)(pos & 0xffffffff), &x, FILE_BEGIN) != INVALID_SET_FILE_POINTER); + } + + virtual bool setEndPos(size_f_t pos) { + ASSERT(isOpen()); + LONG x = (LONG) (pos>>32); + return (::SetFilePointer(h, (DWORD)(pos & 0xffffffff), &x, FILE_END) != INVALID_SET_FILE_POINTER); + } + + virtual bool movePos(size_f_t pos) { + ASSERT(isOpen()); + LONG x = (LONG) (pos>>32); + return (::SetFilePointer(h, (DWORD)(pos & 0xffffffff), &x, FILE_CURRENT) != INVALID_SET_FILE_POINTER); + } + + size_t read(void* buf, size_t len) override { + ASSERT(isOpen()); + #ifndef _RELEASE + if (breakRead != (size_t)(-1)) { + if (breakRead <= len) { + ASSERT("FILE::read() break" == NULL); + breakRead = -1; + } else { + breakRead -= len; + } + } + #endif + DWORD x; + if (!::ReadFile(h, buf, (DWORD)len, &x, NULL)) + return STREAM_ERROR; + return x; + } + + size_t write(const void* buf, size_t len) override { + ASSERT(isOpen()); + #ifndef _RELEASE + if (breakWrite != (size_t)(-1)) { + if (breakWrite <= len) { + ASSERT("FILE::write() break" == NULL); + breakWrite = -1; + } else { + breakWrite -= len; + } + } + #endif + DWORD x; + if (!::WriteFile(h, buf, (DWORD)len, &x, NULL)) + return STREAM_ERROR; + ASSERT(x == len); + return x; + } + virtual bool setEOF() { + ASSERT(isOpen()); + return (SetEndOfFile(h) != FALSE); + } + + size_t flush() override { + ASSERT(isOpen()); + return (FlushFileBuffers(h) ? 0 : STREAM_ERROR); + } + + virtual bool getInfo(BY_HANDLE_FILE_INFORMATION* fileInfo) { + ASSERT(isOpen()); + return (GetFileInformationByHandle(h, fileInfo) != FALSE); + } + + static uint32_t getAttrib(LPCTSTR aFileName) { + return GetFileAttributes(aFileName); + } + + static bool setAttrib(LPCTSTR aFileName, uint32_t attribs) { + return (SetFileAttributes(aFileName, attribs) != FALSE); + } + + static void deleteFile(LPCTSTR aFileName) { ::DeleteFile(aFileName); } + static bool renameFile(LPCTSTR source, LPCTSTR target) { + if (!::MoveFile(source, target)) { + // Can't move, try copy/delete... + if (!::CopyFile(source, target, FALSE)) + return false; + deleteFile(source); + } + return true; + } + static bool copyFile(LPCTSTR source, LPCTSTR target) { return ::CopyFile(source, target, FALSE) == TRUE; } + + static size_f_t getSize(LPCTSTR aFileName) { + const HANDLE fh = ::CreateFile(aFileName, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, NULL); + if (fh == FILE_INVALID_HANDLE) + return SIZE_NA; + DWORD x; + DWORD l = ::GetFileSize(fh, &x); + CloseHandle(fh); + if ((l == INVALID_FILE_SIZE) && (GetLastError() != NO_ERROR)) + return SIZE_NA; + return (((size_f_t)l) | (((size_f_t)x)<<32)); + } + + + static size_f_t findFiles(const String& _strPath, const String& strMask, bool bProcessSubdir, FileInfoArr& arrFiles) { + // List all the files. + WIN32_FIND_DATA fd; + HANDLE hFind; + size_f_t totalSize = 0; + String strPath(_strPath); + Util::ensureFolderSlash(strPath); + //Find all the files in this folder. + hFind = FindFirstFile((strPath + strMask).c_str(), &fd); + if (hFind != FILE_INVALID_HANDLE) { + do { + // this is a file that can be used + if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + continue; + // Store the file name. + FILEINFO& fileInfo = arrFiles.AddEmpty(); + fileInfo.path = strPath + fd.cFileName; + #ifdef LARGEFILESIZE + fileInfo.size = (((size_f_t)fd.nFileSizeLow) | (((size_f_t)fd.nFileSizeHigh)<<32)); + #else + fileInfo.size = fd.nFileSizeLow; + #endif + fileInfo.attrib = fd.dwFileAttributes; + totalSize += fileInfo.size; + } while (FindNextFile(hFind, &fd)); + FindClose(hFind); + } + //Process the subfolders also... + if (!bProcessSubdir) + return totalSize; + hFind = FindFirstFile((strPath + '*').c_str(), &fd); + if (hFind != FILE_INVALID_HANDLE) { + do { + // if SUBDIR then process that too + if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + continue; + if (!_tcscmp(fd.cFileName, _T("."))) + continue; + if (!_tcscmp(fd.cFileName, _T(".."))) + continue; + // Process all subfolders recursively + totalSize += findFiles(strPath + fd.cFileName + PATH_SEPARATOR, strMask, true, arrFiles); + } while (FindNextFile(hFind, &fd)); + FindClose(hFind); + } + return totalSize; + } + +#else // _MSC_VER + + typedef enum FMACCESS_TYPE { + READ = 0x01, + WRITE = 0x02, + RW = READ | WRITE, + } FMACCESS; + + typedef enum FMCHECKACCESS_TYPE { + CA_EXIST = F_OK, // existence + CA_WRITE = W_OK, // write + CA_READ = R_OK, // read + CA_RW = R_OK | W_OK, + CA_EXEC = X_OK, // execute + } FMCHECKACCESS; + + /** + * Open the file specified. + * If there are errors, h is set to FILE_INVALID_HANDLE. + * Use isOpen() to check. + */ + virtual void open(LPCTSTR aFileName, int access, int mode, int flags=0) { + ASSERT(access == WRITE || access == READ || access == (READ | WRITE)); + + close(); + + int m = 0; + if (access == READ) + m |= O_RDONLY; + else if (access == WRITE) + m |= O_WRONLY; + else + m |= O_RDWR; + + if (mode & CREATE) + m |= O_CREAT; + if (mode & TRUNCATE) + m |= O_TRUNC; + + if (flags & SYNC) + m |= O_DSYNC; + #ifndef __APPLE__ + if (flags & NOBUFFER) + m |= O_DIRECT; + #endif + h = ::open(aFileName, m, S_IRUSR | S_IWUSR); + } + + virtual void close() { + if (h != FILE_INVALID_HANDLE) { + ::close(h); + h = FILE_INVALID_HANDLE; + } + } + + uint32_t getLastModified() { + ASSERT(isOpen()); + struct stat s; + if (::fstat(h, &s) == -1) + return 0; + return (uint32_t)s.st_mtime; + } + + size_f_t getSize() const override { + ASSERT(isOpen()); + struct stat s; + if (::fstat(h, &s) == -1) + return SIZE_NA; + return (size_f_t)s.st_size; + } + + size_f_t getPos() const override { + ASSERT(isOpen()); + return (size_f_t)lseek(h, 0, SEEK_CUR); + } + + bool setPos(size_f_t pos) override { ASSERT(isOpen()); return lseek(h, (off_t)pos, SEEK_SET) != (off_t)-1; } + virtual bool setEndPos(size_f_t pos) { ASSERT(isOpen()); return lseek(h, (off_t)pos, SEEK_END) != (off_t)-1; } + virtual bool movePos(size_f_t pos) { ASSERT(isOpen()); return lseek(h, (off_t)pos, SEEK_CUR) != (off_t)-1; } + + size_t read(void* buf, size_t len) override { + ASSERT(isOpen()); + #ifndef _RELEASE + if (breakRead != (size_t)(-1)) { + if (breakRead <= len) { + ASSERT("FILE::read() break" == NULL); + breakRead = -1; + } else { + breakRead -= len; + } + } + #endif + ssize_t x = ::read(h, buf, len); + if (x == -1) + return STREAM_ERROR; + return (size_t)x; + } + + size_t write(const void* buf, size_t len) override { + ASSERT(isOpen()); + #ifndef _RELEASE + if (breakWrite != (size_t)(-1)) { + if (breakWrite <= len) { + ASSERT("FILE::write() break" == NULL); + breakWrite = -1; + } else { + breakWrite -= len; + } + } + #endif + ssize_t x = ::write(h, buf, len); + if (x == -1) + return STREAM_ERROR; + if (x < (ssize_t)len) + return STREAM_ERROR; + return x; + } + + virtual bool setEOF() { + ASSERT(isOpen()); + return (ftruncate(h, (off_t)getPos()) != -1); + } + virtual bool setSize(size_f_t newSize) { + ASSERT(isOpen()); + return (ftruncate(h, (off_t)newSize) != -1); + } + + size_t flush() override { + ASSERT(isOpen()); + return fdatasync(h); + } + + static void deleteFile(LPCTSTR aFileName) { ::remove(aFileName); } + static bool renameFile(LPCTSTR source, LPCTSTR target) { return ::rename(source, target) == 0; } + static bool copyFile(LPCTSTR source, LPCTSTR target) { + std::ifstream src(source, std::ios::binary); + if (!src.is_open()) + return false; + std::ofstream dst(target, std::ios::binary); + if (!dst.is_open()) + return false; + dst << src.rdbuf(); + return true; + } + + static size_f_t getSize(LPCTSTR aFileName) { + struct stat buf; + if (stat(aFileName, &buf) != 0) + return SIZE_NA; + return buf.st_size; + } + +#endif // _MSC_VER + + // test for whether there's something (i.e. folder or file) with this name + // and what access mode is supported + static bool isPresent(LPCTSTR path) { + struct stat buf; + return stat(path, &buf) == 0; + } + static bool access(LPCTSTR path, int mode=CA_EXIST) { + return ::_taccess(path, mode) == 0; + } + // test for whether there's something present and its a folder + static bool isFolder(LPCTSTR path) { + struct stat buf; + if (!(stat(path, &buf) == 0)) + return false; + // If the object is present, see if it is a directory + // this is the Posix-approved way of testing + return S_ISDIR(buf.st_mode); + } + // test for whether there's something present and its a file + // a file can be a regular file, a symbolic link, a FIFO or a socket, but not a device + static bool isFile(LPCTSTR path) { + struct stat buf; + if (!(stat(path, &buf) == 0)) + return false; + // If the object is present, see if it is a file or file-like object + // Note that devices are neither folders nor files + // this is the Posix-approved way of testing + return S_ISREG(buf.st_mode) || S_ISLNK(buf.st_mode) || S_ISSOCK(buf.st_mode) || S_ISFIFO(buf.st_mode); + } + + // time the file was originally created + static time_t getCreated(LPCTSTR path) { + struct stat buf; + if (stat(path, &buf) != 0) return 0; + return buf.st_ctime; + } + // time the file was last modified + static time_t getModified(LPCTSTR path) { + struct stat buf; + if (stat(path, &buf) != 0) return 0; + return buf.st_mtime; + } + // time the file was accessed + static time_t getAccessed(LPCTSTR path) { + struct stat buf; + if (stat(path, &buf) != 0) return 0; + return buf.st_atime; + } + + // set the current folder + static bool setCurrentFolder(LPCTSTR path) { + if (!isFolder(path)) + return false; + #ifdef _MSC_VER + // Windows implementation - this returns non-zero for success + return (SetCurrentDirectory(path) != 0); + #else + // Unix implementation - this returns zero for success + return (chdir(path) == 0); + #endif + } + + template + inline size_t write(const VECTOR& arr) { + const typename VECTOR::IDX nSize(arr.GetSize()); + size_t nBytes(write(&nSize, sizeof(typename VECTOR::IDX))); + nBytes += write(arr.GetData(), arr.GetDataSize()); + return nBytes; + } + + template + inline size_t read(VECTOR& arr) { + typename VECTOR::IDX nSize; + size_t nBytes(read(&nSize, sizeof(typename VECTOR::IDX))); + arr.Resize(nSize); + nBytes += read(arr.GetData(), arr.GetDataSize()); + return nBytes; + } + + enum { LAYER_ID_IN=3 }; + InputStream* getInputStream(int typ=InputStream::LAYER_ID_IN) override { return (typ == LAYER_ID_IN ? static_cast(this) : IOStream::getInputStream(typ)); } + enum { LAYER_ID_OUT=3 }; + OutputStream* getOutputStream(int typ=OutputStream::LAYER_ID_OUT) override { return (typ == LAYER_ID_OUT ? static_cast(this) : IOStream::getOutputStream(typ)); } + +protected: + #ifdef _MSC_VER + HANDLE h; + #else + int h; + #endif + +public: + #ifndef _RELEASE + size_t breakRead; + size_t breakWrite; + #endif + +private: + File(const File&); + File& operator=(const File&); +}; +typedef File* LPFILE; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_FILE_H__ diff --git a/libs/Common/Filters.h b/libs/Common/Filters.h new file mode 100644 index 0000000..ae3d385 --- /dev/null +++ b/libs/Common/Filters.h @@ -0,0 +1,612 @@ +//////////////////////////////////////////////////////////////////// +// Filters.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_FILTERS_H__ +#define __SEACAVE_FILTERS_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Streams.h" + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +template +class BufferedInputStream : public LayerInputStream { +public: + typedef LayerInputStream Base; + + BufferedInputStream(InputStream* aStream, size_t aBufSize) + : Base(aStream), buf(new uint8_t[aBufSize]), bufSize(aBufSize), cache(0), pos(0) { ASSERT(aBufSize > 0); } + virtual ~BufferedInputStream() { + delete[] buf; + } + + size_t read(void* wbuf, size_t len) override { + uint8_t* b = (uint8_t*)wbuf; + const size_t l2 = len; + do { + ASSERT(pos <= cache); + if (pos == cache) { + if (len >= bufSize) { + const size_t r = Base::read(b, len); + if (r == STREAM_ERROR) + return STREAM_ERROR; + return l2 - len + r; + } + pos = 0; + switch (cache = Base::read(buf, bufSize)) { + case 0: + return l2 - len; + case STREAM_ERROR: + return STREAM_ERROR; + } + } + const size_t n = MINF(cache - pos, len); + memcpy(b, buf + pos, n); + b += n; + pos += n; + len -= n; + } while (len > 0); + return l2; + } + + bool setPos(size_f_t wpos) override { + pos = cache = 0; + return Base::setPos(wpos); + } + + size_f_t getPos() const override { + const size_f_t r = Base::getPos(); + if (r == SIZE_NA) + return SIZE_NA; + return r-(cache-pos); + } + + enum { LAYER_ID_IN=1 }; + InputStream* getInputStream(int typ=InputStream::LAYER_ID_IN) override { return (typ == LAYER_ID_IN ? static_cast(this) : Base::getInputStream(typ)); } + +private: + uint8_t* const buf; + const size_t bufSize; + size_t cache; + size_t pos; +}; + + +template +class BufferedOutputStream : public LayerOutputStream { +public: + typedef LayerOutputStream Base; + + BufferedOutputStream(OutputStream* aStream, size_t aBufSize) + : Base(aStream), buf(new uint8_t[aBufSize]), bufSize(aBufSize), pos(0) { } + virtual ~BufferedOutputStream() { + flush(); + delete[] buf; + } + + size_t write(const void* wbuf, size_t len) override { + if (len < bufSize - pos) { + memcpy(buf + pos, (const uint8_t*)wbuf, len); + pos += len; + return len; + } + uint8_t* b = (uint8_t*)wbuf; + const size_t l2 = len; + do { + if (pos == bufSize) { + if (Base::write(buf, bufSize) == STREAM_ERROR) + return STREAM_ERROR; + pos = 0; + if (len < bufSize) { + if (Base::write(b, len) == STREAM_ERROR) + return STREAM_ERROR; + break; + } + } + const size_t n = MINF(bufSize - pos, len); + memcpy(buf + pos, b, n); + b += n; + pos += n; + len -= n; + } while (len > 0); + return l2; + } + + size_f_t getSize() const override { + const size_f_t r = Base::getSize(); + if (r == SIZE_NA) + return SIZE_NA; + return r + pos; + } + + bool setPos(size_f_t wpos) override { + if (pos > 0) { + const size_t ret = Base::write(buf, pos); + pos = 0; + if (ret == STREAM_ERROR) + return false; + } + return Base::setPos(wpos); + } + + size_f_t getPos() const override { + const size_f_t r = Base::getPos(); + if (r == SIZE_NA) + return SIZE_NA; + return r + pos; + } + + size_t flush() override { + size_t ret = 0; + if (pos > 0) { + ret = Base::write(buf, pos); + pos = 0; + if (ret == STREAM_ERROR) + return STREAM_ERROR; + } + return ret + Base::flush(); + } + + enum { LAYER_ID_OUT=1 }; + OutputStream* getOutputStream(int typ=OutputStream::LAYER_ID_OUT) override { return (typ == LAYER_ID_OUT ? static_cast(this) : Base::getOutputStream(typ)); } + +private: + uint8_t* const buf; + const size_t bufSize; + size_t pos; +}; +/*----------------------------------------------------------------*/ + + + +template +class FilteredInputStream : public LayerInputStream { +public: + typedef LayerInputStream Base; + + FilteredInputStream(InputStream* pFile, size_t nFilteredSize, size_t nBufSize, void* pData) + : Base(pFile), buf(new uint8_t[nBufSize]), filteredSize(nFilteredSize), bufSize(nBufSize), valid(0), pos(0), filter(pData), more(true) {} + virtual ~FilteredInputStream() { + delete[] buf; + } + + /** + * Read data through filter, keep calling until len returns 0. + * @param rbuf Data buffer + * @param len Buffer size on entry, bytes actually read on exit + * @return Length of data in buffer + */ + size_t read(void* rbuf, size_t len) override { + uint8_t* rb = (uint8_t*)rbuf; + + const size_t l2 = len; + while (more && len) { + if (pos == valid) { + valid = Base::read(buf, bufSize); + if (valid == STREAM_ERROR) + return STREAM_ERROR; + pos = 0; + } + size_t m = valid - pos; + size_t n = len; + more = filter(buf + pos, m, rb, n); + pos += m; + rb += n; + len -= n; + } + return l2-len; + } + + size_f_t getSize() const override { + return filteredSize; + } + + bool setPos(size_f_t wpos) override { + valid = pos = 0; + return Base::setPos(wpos); + } + + size_f_t getPos() const override { + const size_f_t r = Base::getPos(); + if (r == SIZE_NA) + return SIZE_NA; + return r-(valid-pos); + } + + enum { LAYER_ID_IN=2 }; + InputStream* getInputStream(int typ=InputStream::LAYER_ID_IN) override { return (typ == LAYER_ID_IN ? static_cast(this) : Base::getInputStream(typ)); } + +private: + uint8_t* const buf; + const size_t filteredSize; + const size_t bufSize; + size_t valid; + size_t pos; + Filter filter; + bool more; +}; + + +template +class FilteredOutputStream : public LayerOutputStream { +public: + typedef LayerOutputStream Base; + + FilteredOutputStream(OutputStream* aFile, size_t aBufSize, void* pData) + : Base(aFile), buf(new uint8_t[aBufSize]), bufSize(aBufSize), filter(pData), flushed(false) {} + virtual ~FilteredOutputStream() { + flush(); + delete[] buf; + } + + size_t write(const void* wbuf, size_t len) override { + if (flushed) + return STREAM_ERROR; + + const uint8_t* wb = (const uint8_t*)wbuf; + size_t written = 0; + while (len > 0) { + size_t n = bufSize; + size_t m = len; + + const bool more = filter(wb, m, buf, n); + wb += m; + len -= m; + + const size_t r = Base::write(buf, n); + if (r == STREAM_ERROR) + return STREAM_ERROR; + written += r; + + if (!more) { + if (len > 0) + return STREAM_ERROR; + flushed = true; + return written; + } + } + return written; + } + + size_t flush() override { + if (flushed) + return Base::flush(); + + flushed = true; + size_t written = 0; + + while (true) { + size_t n = bufSize; + size_t zero = 0; + bool more = filter(NULL, zero, buf, n); + + written += Base::write(buf, n); + + if (!more) + break; + } + const size_t r = Base::flush(); + if (r == STREAM_ERROR) + return STREAM_ERROR; + return written + r; + } + + enum { LAYER_ID_OUT=2 }; + OutputStream* getOutputStream(int typ=OutputStream::LAYER_ID_OUT) override { return (typ == LAYER_ID_OUT ? static_cast(this) : Base::getOutputStream(typ)); } + +private: + uint8_t* const buf; + const size_t bufSize; + Filter filter; + bool flushed; +}; +/*----------------------------------------------------------------*/ + + + +#ifdef _UNICODE +#define TOKEN_SIZE 2 //in bytes +#else +#define TOKEN_SIZE 1 //in bytes +#endif +#define TOKEN_MAXBUF (2048*TOKEN_SIZE) //in bytes +#define TOKEN_MAXIGN 32 //in TCHARs + +template +class TokenInputStream : public LayerInputStream { +public: + typedef LayerInputStream Base; + + TokenInputStream(InputStream* aStream, TCHAR aToken=_T('\n')) + : Base(aStream), pos(TOKEN_MAXBUF), eos(false), token(aToken) { arrIgnore[0] = _T('\0'); } + virtual ~TokenInputStream() { + } + + void emptyBuffer() { + setPos(getPos()); + } + + TCHAR getToken() const { + return token; + } + + void setToken(TCHAR aToken) { + token = aToken; + } + + LPCTSTR getTrimTokens() const { + return arrIgnore; + } + + size_t setTrimTokens(LPCTSTR szIgnore) { + const size_t len = _tcslen(szIgnore); + if (len >= TOKEN_MAXIGN) + return 0; + _tcscpy(arrIgnore, szIgnore); + return len; + } + + LPTSTR trimFrontLine(LPTSTR line) { + while (line[0] != _T('\0')) { + if (_tcschr(arrIgnore, line[0]) == NULL) + return line; + ++line; + } + return line; + } + + LPTSTR trimFrontLine(MemFile& memFile) { + memFile.ensureSize(1); + LPTSTR line = (LPTSTR)memFile.getData(); + line[memFile.getSizeLeft()] = _T('\0'); + LPTSTR newLine = trimFrontLine(line); + memFile.movePos(newLine-line); + return newLine; + } + + size_t trimBackLine(LPTSTR line) { + const size_t len = _tcslen(line); + size_t i = len; + while (i > 0 && line[--i] != _T('\0')) { + if (_tcschr(arrIgnore, line[i]) == NULL) + return len-(i+1); + line[i] = _T('\0'); + } + return len; + } + + size_t trimBackLine(MemFile& memFile) { + memFile.ensureSize(1); + LPTSTR line = (LPTSTR)memFile.getData(); + line[memFile.getSizeLeft()] = _T('\0'); + const size_t trimedSize = trimBackLine(line); + memFile.moveSize(-((size_f_t)trimedSize)); + return trimedSize; + } + + // revert the deleted token during the last readLine() + void restoreToken() { + LPTSTR line = (LPTSTR)buf; + ASSERT(pos > 0); + line[--pos] = token; + } + + size_t readLine(LPTSTR wbuf, size_t len) { + if (eos) + return 0; + uint8_t* b = (uint8_t*)wbuf; + const size_t l2 = len; + // process chunks of TOKEN_MAXBUF bytes + while (len >= TOKEN_MAXBUF / TOKEN_SIZE) { + const size_t n = read(b, TOKEN_MAXBUF); + if (n == STREAM_ERROR) + return STREAM_ERROR; + *((TCHAR*)(b+n)) = _T('\0'); + LPTSTR t = _tcschr((TCHAR*)b, token); + // if token found... + if (t != NULL) { + // ... set the end of line and return it + t[0] = _T('\0'); + if (n == TOKEN_SIZE && len != 1) { + eos = true; + return l2-len; + } + ++t; + const size_t bytesParsed = (uint8_t*)t-b; + const size_t bytesNotParsed = n - bytesParsed; + pos = TOKEN_MAXBUF - bytesNotParsed; + // store the unprocessed data + memcpy(buf+pos, (uint8_t*)t, bytesNotParsed); + len -= (bytesParsed - TOKEN_SIZE) / TOKEN_SIZE; + return l2-len; + } + len -= n / TOKEN_SIZE; + // if end of stream return + if (n < TOKEN_MAXBUF) { + eos = true; + return l2-len; + } + b += n; + // if we reached the end of the buffer, signal it + if (len == 0) + return l2+1; + } + // process the last sub-chunk part + const size_t n = read(b, len*TOKEN_SIZE); + if (n == STREAM_ERROR) + return STREAM_ERROR; + *((TCHAR*)(b+n)) = _T('\0'); + LPTSTR t = _tcschr((TCHAR*)b, token); + // if token found... + if (t != NULL) { + // ... set the end of line and return it + t[0] = _T('\0'); + if (n == TOKEN_SIZE && len != 1) { + eos = true; + return l2-len; + } + ++t; + const size_t bytesParsed = (uint8_t*)t-b; + const size_t bytesNotParsed = n - bytesParsed; + pos = TOKEN_MAXBUF - bytesNotParsed; + // store the unprocessed data + memcpy(buf+pos, (uint8_t*)t, bytesNotParsed); + len -= (bytesParsed - TOKEN_SIZE) / TOKEN_SIZE; + return l2-len; + } + // if end of stream return + if (n < len * TOKEN_SIZE) { + eos = true; + return n / TOKEN_SIZE; + } + // we reached the end of the buffer, signal it + return l2+1; + } + + size_t readLine(MemFile& memFile) + { + if (eos) + return 0; + // make sure we read one full line + const size_f_t oldSize = memFile.getSize(); + while (true) { + memFile.ensureSize((4096+1)*TOKEN_SIZE); + const size_t ret = readLine((LPTSTR)(memFile.getBuffer()+memFile.getSize()), 4096); + if (ret == STREAM_ERROR) + return STREAM_ERROR; + if (ret <= 4096) { + memFile.growSize(ret*TOKEN_SIZE); + break; + } + memFile.growSize(4096*TOKEN_SIZE); + } + return size_t(memFile.getSize()-oldSize); + } + + // read all to the memfile + size_t read(MemFile& memFile) + { + if (eos) + return 0; + const size_t len = (size_t)(Base::getSize() - getPos()); + memFile.ensureSize(len); + const size_t ret = read(memFile.getBuffer()+memFile.getSize(), len); + if (ret == STREAM_ERROR) + return STREAM_ERROR; + memFile.growSize(ret); + return ret; + } + + size_t read(void* wbuf, size_t len) override { + size_t n = 0; + if (pos < TOKEN_MAXBUF) { + n = TOKEN_MAXBUF-pos; + if (n > len) + n = len; + memcpy(wbuf, buf+pos, n); + len -= n; + pos += n; + } + if (len == 0) + return n; + const size_t r = Base::read((uint8_t*)wbuf+n, len); + if (r == STREAM_ERROR) + return STREAM_ERROR; + return n+r; + } + + bool setPos(size_f_t wpos) override { + eos = false; + pos = TOKEN_MAXBUF; + return Base::setPos(wpos); + } + + size_f_t getPos() const override { + const size_f_t r = Base::getPos(); + if (r == SIZE_NA) + return SIZE_NA; + return r-(TOKEN_MAXBUF-pos); + } + + bool isEOS() const { + return eos; + } + + enum { LAYER_ID_IN=5 }; + InputStream* getInputStream(int typ=InputStream::LAYER_ID_IN) override { return (typ == LAYER_ID_IN ? static_cast(this) : Base::getInputStream(typ)); } + +private: + uint8_t buf[TOKEN_MAXBUF+TOKEN_SIZE]; + size_t pos; + bool eos; + TCHAR token; + TCHAR arrIgnore[TOKEN_MAXIGN]; +}; +/*----------------------------------------------------------------*/ + + + +template +class MaskInputStream : public LayerInputStream { +public: + typedef LayerInputStream Base; + + MaskInputStream(InputStream* aStream, size_f_t nPos, size_f_t nSize) + : Base(aStream), startPos(nPos), size(nSize), pos(0) { } + virtual ~MaskInputStream() { } + + size_t read(void* wbuf, size_t len) override { + if (pos >= size) + return 0; + if (!Base::setPos(startPos + pos)) + return STREAM_ERROR; + if (pos+(size_f_t)len > size) + len = (size_t)(size - pos); + const size_t r = Base::read(wbuf, len); + if (r == STREAM_ERROR) + return STREAM_ERROR; + pos += r; + return r; + } + + size_f_t getSize() const override { + return size; + } + + bool setPos(size_f_t wpos) override { + if (wpos > size) + pos = size; + else + pos = wpos; + return true; + } + + size_f_t getPos() const override { + return pos; + } + + enum { LAYER_ID_IN=6 }; + InputStream* getInputStream(int typ=InputStream::LAYER_ID_IN) override { return (typ == LAYER_ID_IN ? static_cast(this) : Base::getInputStream(typ)); } + +private: + const size_f_t size; + const size_f_t startPos; + size_f_t pos; +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_FILTERS_H__ diff --git a/libs/Common/HTMLDoc.h b/libs/Common/HTMLDoc.h new file mode 100644 index 0000000..8fd21b5 --- /dev/null +++ b/libs/Common/HTMLDoc.h @@ -0,0 +1,457 @@ +/* + * Modified version of: + * + * @file htmlDoc.h + * @brief Simple HTML document writer and SVG drawer + * @author Pierre MOULON + * + * Copyright (c) 2011, 2012, 2013 Pierre MOULON + * All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef _HTMLDOC_H_ +#define _HTMLDOC_H_ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + +#define JSXCHART_BORDER 0.2f + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace HTML { + +inline const std::string htmlMarkup(const std::string& markup, const std::string& text) { + std::ostringstream os; + os << '<'<< markup<<'>' << text << "' <<"\n"; + return os.str(); +} +inline const std::string htmlMarkup(const std::string& markup, const std::string& attributes, const std::string& text) { + std::ostringstream os; + os << '<'<' << text << "' <<"\n"; + return os.str(); +} +inline const std::string htmlOpenMarkup(const std::string& markup, const std::string& attributes) { + std::ostringstream os; + os << '<'<" <<"\n"; + return os.str(); +} + +inline const std::string htmlComment(const std::string& text) { + std::ostringstream os; + os << "" << "\n"; + return os.str(); +} + +/// Return a chain in the form attributes="val" +template +inline const std::string quotedAttributes(const std::string& attributes, const T & val) { + std::ostringstream os; + os << attributes << "='" << val << '\''; + return os.str(); +} + +/// Return a chain of the value T +template +inline const std::string toString(const T& val) { + std::ostringstream os; + os << val; + return os.str(); +} + +class htmlDocumentStream +{ +public: + htmlDocumentStream(const std::string& title) { + htmlStream << "\n"; + htmlStream << htmlMarkup("head", + "\n" + "\n" + "\n"); + htmlStream << htmlMarkup("title",title); + } + + htmlDocumentStream(const std::string& title, + const std::vector& vec_css, + const std::vector& vec_js) + { + htmlStream << "\n\n"; + htmlStream << htmlMarkup("title",title); + // CSS and JS resources + for (std::vector::const_iterator iter = vec_css.begin(); iter != vec_css.end(); ++iter) + htmlStream << "\n"; + for (std::vector::const_iterator iter = vec_js.begin(); iter != vec_js.end(); ++iter) + htmlStream << "\n"; + htmlStream << "\n"; + } + + void pushInfo(const std::string& text) { + htmlStream << text; + } + + std::string getDoc() { + return htmlMarkup("html", htmlStream.str()); + } + + std::ostringstream htmlStream; +}; + + +/// Class to draw with the JSXGraph library in HTML page. +class JSXGraphWrapper +{ +public: + typedef float TRANGE; + typedef std::pair< std::pair, std::pair > RANGE; + + JSXGraphWrapper() { + cpt = 0; + } + + void reset() { + stream.str(""); + stream.precision(4); + //stream.setf(std::ios::fixed,std::ios::floatfield); + cpt = 0; + } + + void init(unsigned W, unsigned H, LPCTSTR szGraphName=NULL) { + reset(); + std::string strGraphName; + if (szGraphName == NULL) { + strGraphName = SEACAVE::Util::getUniqueName(); + szGraphName = strGraphName.c_str(); + } + stream << + "\n" + "
\n" + "\n"; + } + + std::string toStr() const { + return stream.str(); + } + + template + static inline RANGE autoViewport(TX maxValX, TY maxValY, TX minValX, TY minValY) { + //Use the value with a little margin + const TX rangeX = maxValX-minValX; + const TY rangeY = maxValY-minValY; + return std::make_pair( + std::make_pair(-JSXCHART_BORDER*rangeX+minValX,JSXCHART_BORDER*rangeX+maxValX), + std::make_pair(-JSXCHART_BORDER*rangeY+minValY,JSXCHART_BORDER*rangeY+maxValY)); + } + template + static RANGE autoViewport(const VECTX& vec_x, const VECTY& vec_y) { + typedef typename VECTX::value_type TX; + typedef typename VECTY::value_type TY; + if (vec_x.empty() || vec_y.empty() || vec_x.size() != vec_y.size()) + return RANGE(); + //For X values + const TX minValX = *std::min_element(vec_x.begin(), vec_x.end()); + const TX maxValX = *std::max_element(vec_x.begin(), vec_x.end()); + //For Y values + const TY minValY = *std::min_element(vec_y.begin(), vec_y.end()); + const TY maxValY = *std::max_element(vec_y.begin(), vec_y.end()); + return autoViewport(maxValX, maxValY, minValX, minValY); + } + template + static RANGE autoViewport(const VECTY& vec_y, bool bForceY0=true) { + typedef T TX; + typedef typename VECTY::value_type TY; + if (vec_y.empty()) + return RANGE(); + //For X values + const TX minValX = TX(0); + const TX maxValX = static_cast(vec_y.size()); + //For Y values + const TY minValY = (bForceY0 ? TY(0) : *std::min_element(vec_y.begin(), vec_y.end())); + const TY maxValY = *std::max_element(vec_y.begin(), vec_y.end()); + return autoViewport(maxValX, maxValY, minValX, minValY); + } + + std::ostringstream stream; + size_t cpt; //increment for variable +}; +/*----------------------------------------------------------------*/ + +} // namespace HTML + + + +namespace SVG { + +/// Basic SVG style +class svgStyle +{ +public: + svgStyle():_sFillCol(""), _sStrokeCol("black"), _sToolTip(""), _fillOpacity(1.f), _strokeW(1.f), _strokeOpacity(1.f) {} + + // Configure fill color + svgStyle& fill(const std::string& col, float opacity = 1.f) + { _sFillCol = col; _fillOpacity = opacity; return *this; } + + // Configure stroke color and width + svgStyle& stroke(const std::string& col, float witdh = 1.f, float opacity = 1.f) + { _sStrokeCol = col; _strokeW = witdh; _strokeOpacity = opacity; return *this; } + + // Configure with no stroke + svgStyle& noStroke() + { _sStrokeCol = ""; _strokeW = 0.f; _strokeOpacity = 0.f; return *this; } + + // Configure tooltip + svgStyle& tooltip(const std::string& sTooltip) + { _sToolTip = sTooltip; return *this; } + + const std::string getSvgStream() const { + std::ostringstream os; + if (!_sStrokeCol.empty()) { + os << " stroke='" << _sStrokeCol << "' stroke-width='" << _strokeW << "'"; + if (_strokeOpacity < 1) + os << " stroke-opacity='" << _strokeOpacity << "'"; + } + if (!_sFillCol.empty()) { + os << " fill='" << _sFillCol << "'"; + if (_fillOpacity < 1) + os << " fill-opacity='" << _fillOpacity << "'"; + } else { + os << " fill='none'"; + } + if (!_sToolTip.empty()) { + os << " tooltip='enable'>" << "" << _sToolTip << ""; + } + return os.str(); + } + + bool bTooltip() const { return !_sToolTip.empty();} + + std::string _sFillCol, _sStrokeCol, _sToolTip; + float _fillOpacity, _strokeW, _strokeOpacity; +}; + + +/// Basic class to handle simple SVG draw. +/// You can draw line, square, rectangle, text and image (xlink) +class svgDrawer +{ +public: + ///Constructor + svgDrawer(size_t W = 0, size_t H = 0) { + svgStream << + "\n" + "\n" + " 0 && H > 0) + svgStream << + " width='" << W << "px' height='"<< H << "px'" + " preserveAspectRatio='xMinYMin meet'" + " viewBox='0 0 " << W << ' ' << H <<"'"; + + svgStream << + " xmlns='http://www.w3.org/2000/svg'" + " xmlns:xlink='http://www.w3.org/1999/xlink'" + " version='1.1'>\n"; + } + ///Circle draw -> x,y position and radius + void drawCircle(float cx, float cy, float r, const svgStyle& style) { + svgStream + << "\n" : "/>\n"); + } + ///Line draw -> start and end point + void drawLine(float ax, float ay, float bx, float by, const svgStyle& style) { + svgStream + << "\n" : "/>\n"); + } + + ///Reference to an image (path must be relative to the SVG file) + void drawImage(const std::string& simagePath, int W, int H, + int posx = 0, int posy = 0, float opacity =1.f) + { + svgStream << + "\n"; + } + + ///Square draw -> x,y position and size + void drawSquare(float cx, float cy, float W, const svgStyle& style) { + drawRectangle(cx, cy, W, W, style); + } + + ///Circle draw -> x,y position and width and height + void drawRectangle(float cx, float cy, float W, float H, const svgStyle& style) { + svgStream + << "\n" : "/>\n"); + } + + ///Text display -> x,y position, font size + void drawText(float cx, float cy, const std::string& stext, const std::string& scol = "", const std::string& sattr = "", float fontSize = 1.f) { + svgStream << "" << stext << "\n"; + } + template< typename DataInputIteratorX, typename DataInputIteratorY> + void drawPolyline(DataInputIteratorX xStart, DataInputIteratorX xEnd, + DataInputIteratorY yStart, DataInputIteratorY /*yEnd*/, + const svgStyle& style) + { + svgStream << "\n" : "/>\n"); + } + + ///Close the svg tag. + std::ostringstream& closeSvgFile() { + svgStream << ""; + return svgStream; + } + + std::ostringstream svgStream; +}; + +/// Helper to draw a SVG histogram +/// ____ +/// | | ___ | +/// | |__| | | +/// | | | | | +/// -----------| +struct svgHisto +{ + template + std::string draw(const VECT& vec_value, + const std::pair& range, + float W, float H) + { + if (vec_value.empty()) + return ""; + + //-- Max value + typedef typename VECT::value_type T; + const T maxi = *max_element(vec_value.begin(), vec_value.end()); + const size_t n = vec_value.size(); + + const float scaleFactorY = H / static_cast(maxi); + const float scaleFactorX = W / static_cast(n); + + svgDrawer svgStream; + + for (typename VECT::const_iterator iter = vec_value.begin(); iter != vec_value.end(); ++iter) + { + const T dist = std::distance(vec_value.begin(), iter); + const T& val = *iter; + std::ostringstream os; + os << '(' << range.first + dist/float(n) * (range.second-range.first) << ',' << val << ')'; + svgStyle style = svgStyle().fill("blue").stroke("black", 1.0).tooltip(os.str()); + svgStream.drawRectangle( + scaleFactorX * dist, H-val * scaleFactorY, + scaleFactorX, val * scaleFactorY, + style); + //_________ + //| |_________ + //| || | + //| || | + //| || | + //0 sFactorX 2*sFactorX + } + svgStyle styleAxis = svgStyle().stroke("black", 1.0f); + // Draw X Axis + svgStream.drawText(.05f*W, 1.2f*H, HTML::toString(range.first), "black", "", .1f*H); + svgStream.drawText(W, 1.2*H, HTML::toString(range.second), "black", "", .1f*H); + svgStream.drawLine(0, 1.1f*H, W, 1.1f*H, styleAxis); + // Draw Y Axis + svgStream.drawText(1.2f*W, .1f*H, HTML::toString(maxi), "black", "", .1f*H); + svgStream.drawText(1.2f*W, H, "0", "black", "", .1f*H); + svgStream.drawLine(1.1f*W, 0, 1.1f*W, H, styleAxis); + + return svgStream.closeSvgFile().str(); + } +}; +/*----------------------------------------------------------------*/ + +} // namespace SVG + +#endif // _HTMLDOC_H_ diff --git a/libs/Common/HalfFloat.h b/libs/Common/HalfFloat.h new file mode 100644 index 0000000..3f38c83 --- /dev/null +++ b/libs/Common/HalfFloat.h @@ -0,0 +1,159 @@ +//////////////////////////////////////////////////////////////////// +// HalfFoat.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_HALFFLOAT_H__ +#define __SEACAVE_HALFFLOAT_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + + +// hfloat - a 16-bit floating point number class +// +// Can represent positive and negative numbers whose magnitude is between +// roughly 6.1e-5 and 6.5e+4 with a relative error of 9.8e-4; numbers +// smaller than 6.1e-5 can be represented with an absolute error of +// 6.0e-8. All integers from -2048 to +2048 can be represented exactly. +// Supports Denormals-as-zero (DAZ), but does not support infinities or NaN. +// +// Only conversions from half to float are lossless. +class GENERAL_API hfloat +{ +public: + typedef short Type; + + inline hfloat() : val(fromFloat(float())) {} + explicit inline hfloat(float v) : val(fromFloat(v)) {} + template + inline hfloat(T v) : val(fromFloat(static_cast(v))) {} + + inline hfloat& operator = (float v) { + val = fromFloat(v); + return *this; + } + template + inline hfloat& operator = (T v) { + val = fromFloat(static_cast(v)); + return *this; + } + + inline operator float() const { + return toFloat(val); + } + inline hfloat operator - () const { + return fromFloat2HFloat(-toFloat(val)); + } + inline hfloat operator + (hfloat v) { + return fromFloat2HFloat(toFloat(val) + toFloat(v.val)); + } + inline hfloat operator + (float v) { + return fromFloat2HFloat(toFloat(val) + v); + } + inline hfloat& operator += (hfloat v) { + return *this = *this + v; + } + inline hfloat& operator += (float v) { + return *this = *this + v; + } + inline hfloat operator - (hfloat v) { + return fromFloat2HFloat(toFloat(val) - toFloat(v.val)); + } + inline hfloat operator - (float v) { + return fromFloat2HFloat(toFloat(val) - v); + } + inline hfloat& operator -= (hfloat v) { + return *this = *this - v; + } + inline hfloat& operator -= (float v) { + return *this = *this - v; + } + inline hfloat operator * (hfloat v) { + return fromFloat2HFloat(toFloat(val) * toFloat(v.val)); + } + inline hfloat operator * (float v) { + return fromFloat2HFloat(toFloat(val) * v); + } + inline hfloat& operator *= (hfloat v) { + return *this = *this * v; + } + inline hfloat& operator *= (float v) { + return *this = *this * v; + } + inline hfloat operator / (hfloat v) { + return fromFloat2HFloat(toFloat(val) / toFloat(v.val)); + } + inline hfloat operator / (float v) { + return fromFloat2HFloat(toFloat(val) / v); + } + inline hfloat& operator /= (hfloat v) { + return *this = *this / v; + } + inline hfloat& operator /= (float v) { + return *this = *this / v; + } + + static float min() { return 6.1e-5f; } + static float max() { return 6.5e+4f; } + + static inline short fromFloat(float f) { + ASSERT(ISFINITE(f) && (ABS(f) == 0.f || ABS(f) >= min()) && ABS(f) <= max()); + // ~8 clock cycles on modern x86-64 + const CastF2I ufi(f); + int32_t t1 = ufi.i & 0x7fffffff; // Non-sign bits + int32_t t2 = ufi.i & 0x80000000; // Sign bit + int32_t t3 = ufi.i & 0x7f800000; // Exponent + t1 >>= 13; // Align mantissa on MSB + t2 >>= 16; // Shift sign bit into position + t1 -= 0x1c000; // Adjust bias + t1 = (t3 < 0x38800000) ? 0 : t1; // Flush-to-zero + t1 = (t3 > 0x47000000) ? 0x7bff : t1; // Clamp-to-max + t1 = (t3 == 0 ? 0 : t1); // Denormals-as-zero + return (short)(t1 | t2); // Re-insert sign bit + } + static inline float toFloat(short h) { + // ~6 clock cycles on modern x86-64 + CastF2I ufi; + int32_t t1 = h & 0x7fff; // Non-sign bits + int32_t t2 = h & 0x8000; // Sign bit + int32_t t3 = h & 0x7c00; // Exponent + t1 <<= 13; // Align mantissa on MSB + t2 <<= 16; // Shift sign bit into position + t1 += 0x38000000; // Adjust bias + t1 = (t3 == 0 ? 0 : t1); // Denormals-as-zero + ufi.i = t1 | t2; // Re-insert sign bit + return ufi.f; + } + + #ifdef _USE_BOOST + // serialize + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & val; + } + #endif + +protected: + static inline hfloat fromFloat2HFloat(float f) { + return TAliasCast(fromFloat(f)).i; + } + +protected: + Type val; +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_HALFFLOAT_H__ diff --git a/libs/Common/Hash.h b/libs/Common/Hash.h new file mode 100644 index 0000000..ccb9163 --- /dev/null +++ b/libs/Common/Hash.h @@ -0,0 +1,303 @@ +//////////////////////////////////////////////////////////////////// +// Hash.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_HASH_H__ +#define __SEACAVE_HASH_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +/************************************************************************************** + * StringHash template + * --------------- + * compile-time string hash template + **************************************************************************************/ + +class StringHash +{ +private: + uint32_t m_val; + + template inline uint32_t _Hash(const char (&str)[N]) const + { + typedef const char (&truncated_str)[N-2]; + return str[N-1] + 65599 * (str[N-2] + 65599 * _Hash((truncated_str)str)); + } + inline uint32_t _Hash(const char (&str)[3]) const { return str[2] + 65599 * (str[1] + 65599 * str[0]); } + inline uint32_t _Hash(const char (&str)[2]) const { return str[1] + 65599 * str[0]; } + +public: + template StringHash(const char (&str)[N]) { m_val = _Hash(str); } + inline operator uint32_t() const { return m_val; } +}; + + +/************************************************************************************** + * cHashTable template + * --------------- + * simple hash template (performs relatively well for less than 300 elements) + **************************************************************************************/ + +#define KEY_NA 0xFFFF +#define KEY_MAXSIZE 0x1000000 + +template +class cHashTable +{ +public: + typedef uint32_t Key; + typedef uint16_t Idx; + typedef uint32_t Size; + typedef cList Values; + typedef cList Keys; + typedef cList Indices; + +protected: + Keys m_keys; // Array of unique keys that have been set + Values m_values; // Matching array of unique values that have been set + Indices m_indices; // 1-based index array referencing the two arrays above + +public: + cHashTable() : m_indices(32) { m_indices.Memset(0); } + ~cHashTable() { Release(); } + + inline void Release() + { + m_keys.Release(); + m_values.Release(); + m_indices.Release(); + } + + inline bool IsEmpty() const { return m_indices.IsEmpty(); } + inline size_t GetSize() const { return m_keys.GetSize(); } + inline const Type* GetBegin() const { return m_values.GetData(); } + inline const Type* GetEnd() const { return m_values.GetData()+m_values.GetSize(); } + inline Type* GetBegin() { return m_values.GetData(); } + inline Type* GetEnd() { return m_values.GetData()+m_values.GetSize(); } + + inline const Indices& GetArrIndices() const { return m_indices; } + inline const Keys& GetArrKeys() const { return m_keys; } + inline const Values& GetArrValues() const { return m_values; } + inline Values& GetArrValues() { return m_values; } + +private: + inline Size _StaticKeyToID(Key key, Size size) const { return key & (size-1); } + inline Size _KeyToID(Key key) const { return _StaticKeyToID(key, (Size)m_indices.GetSize()); } + inline Idx _IDToIndex(Size id) const { return m_indices[id] - 1; } + inline Idx _KeyToIndex(Key key) const { return (m_indices.IsEmpty() ? KEY_NA : _IDToIndex(_KeyToID(key))); } + + // Completely discards then rebuilds all indices based on the current set of keys + void _RebuildIndices() + { + // Clear all current memory + m_indices.Memset(0); + // Run through all keys and recreate the indices + for (Idx i=0; i0; ) + { + if (m_indices[--i] > index) + { + --m_indices[i]; + } + } + } + } + + // Retrieves a value from the hash -- but it will only be valid if it exists, so be careful. In most + // cases you will either want to use an Exists() check first, or simply use the Find function. + inline const Type& operator [] (Key key) const + { + const Idx index = _KeyToIndex(key); + return m_values[index]; + } + + // Retrieves a value from the hash, inserting a new one if necessary + Type& operator [] (Key key) + { + // Get the index for this key + const Idx index = _KeyToIndex(key); + + if (index != KEY_NA) + { + // If we found a valid entry, we need to match the actual key + const Key oldKey = m_keys[index]; + + // If the key matches, return the value + if (oldKey == key) + { + return m_values[index]; + } + else + { + // Setting the key was unsuccessful due to another entry colliding with our key; + // we must expand the indices until we find a set of keys that will match. + Size newSize = m_indices.GetSize(); + while (newSize < (KEY_MAXSIZE>>1)) + { + newSize = newSize << 1; + // Find the next best size for the hash that would make both keys unique + if (_StaticKeyToID(key, newSize) != _StaticKeyToID(oldKey, newSize)) + { + m_indices.ResizeExact(newSize); + _RebuildIndices(); + break; + } + } + // Critical error if we didn't find a working size + ASSERT(_StaticKeyToID(key, newSize) != _StaticKeyToID(oldKey, newSize)); + } + } + + // assert the total number of values stored is in Idx range + ASSERT(m_keys.GetSize() < (Size)((Idx)-1)); + + // Append the new key to the end + m_keys.Insert(key); + + // Add this new entry to the index list using the current array size as the 1-based index + m_indices[_KeyToID(key)] = (Idx)m_keys.GetSize(); + + // Return the value + return m_values.AddEmpty(); + } + +public: + // Fowler / Noll / Vo (FNV) Hash + // magic numbers from http://www.isthe.com/chongo/tech/comp/fnv/ + static inline Key HashKeyFNV(const uint8_t* data, Size size) + { + Key hash = 2166136261U; //InitialFNV + for (size_t i = 0; i < size; i++) + { + hash = hash ^ (data[i]); // xor the low 8 bits + hash = hash * 16777619; // multiply by the magic number (FNVMultiple) + } + return hash; + } + // Function that calculates a hash value from a given data + // tested for random binary data (3 <= size <= 33) and performs VERY bad + static inline Key HashKeyR5(const uint8_t* data, Size size) + { + Key key = 0; + Key offset = 1357980759; + for (Size i=0; i> 15); + offset = key ^ (((~offset) >> 7) | (offset << 25)); + } + return key; + } + static inline Key HashKey(const uint8_t* data, Size size) { return HashKeyFNV(data, size); } + static inline Key HashKey(LPCTSTR sz) { return HashKey((const uint8_t*)sz, (Size)_tcslen(sz)); } + static inline Key HashKey(const String& str) { return HashKey((const uint8_t*)str.c_str(), (Size)str.size()); } + + // Convenience functions + inline Type* Find (LPCTSTR key) { return Find ( HashKey(key) ); } + inline Type* Find (const String& key) { return Find ( HashKey(key) ); } + inline const Type* Find (LPCTSTR key) const { return Find ( HashKey(key) ); } + inline const Type* Find (const String& key) const { return Find ( HashKey(key) ); } + inline void Delete (LPCTSTR key) { Delete( HashKey(key) ); } + inline void Delete (const String& key) { Delete( HashKey(key.c_str()) ); } + inline bool Exists (LPCTSTR key) const { return Exists( HashKey(key) ); } + inline bool Exists (const String& key) const { return Exists( HashKey(key.c_str()) ); } + inline Type& operator [] (LPCTSTR key) { return (*this)[ HashKey(key) ]; } + inline Type& operator [] (const String& key) { return (*this)[ HashKey(key.c_str()) ]; } + inline const Type& operator [] (LPCTSTR key) const { return (*this)[ HashKey(key) ]; } + inline const Type& operator [] (const String& key) const { return (*this)[ HashKey(key.c_str()) ]; } +}; + + +/************************************************************************************** + * cMapWrap template + * --------------- + * STL map wrapper + **************************************************************************************/ + +template +class cMapWrap : public std::unordered_map +{ +public: + typedef typename std::unordered_map Base; + typedef typename Base::const_iterator const_iterator; + typedef typename Base::iterator iterator; + +public: + cMapWrap() {} + ~cMapWrap() {} + + inline void Release() { this->clear(); } + inline bool IsEmpty() const { return this->empty(); } + inline size_t GetSize() const { return this->size(); } + inline const_iterator GetBegin() const { return this->begin(); } + inline const_iterator GetEnd() const { return this->end(); } + inline iterator GetBegin() { return this->begin(); } + inline iterator GetEnd() { return this->end(); } + inline const_iterator Find(const Key& key) const { return this->find(key); } + inline iterator Find(const Key& key) { return this->find(key); } + inline iterator Insert(const Key& key, bool& bExisted) { return Insert(key, Type(), bExisted); } + inline iterator Insert(const Key& key, const Type& val, bool& bExisted) { + const std::pair ret(this->emplace(key, val)); + bExisted = !ret.second; + return ret.first; + } +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_HASH_H__ diff --git a/libs/Common/Line.h b/libs/Common/Line.h new file mode 100644 index 0000000..408a1a8 --- /dev/null +++ b/libs/Common/Line.h @@ -0,0 +1,95 @@ +//////////////////////////////////////////////////////////////////// +// Line.h +// +// Copyright 2023 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_LINE_H__ +#define __SEACAVE_LINE_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +// Generic line class represented as two points +template +class TLine +{ + STATIC_ASSERT(DIMS > 1 && DIMS <= 3); + +public: + typedef Eigen::Matrix VECTOR; + typedef Eigen::Matrix POINT; + typedef SEACAVE::TAABB AABB; + typedef SEACAVE::TRay RAY; + enum { numScalar = (2*DIMS) }; + enum { numParams = numScalar-1 }; + + POINT pt1, pt2; // line description + + //--------------------------------------- + + inline TLine() {} + inline TLine(const POINT& pt1, const POINT& pt2); + template + inline TLine(const TLine&); + + inline void Set(const POINT& pt1, const POINT& pt2); + + int Optimize(const POINT*, size_t, int maxIters=100); + template + int Optimize(const POINT*, size_t, const RobustNormFunctor& robust, int maxIters=100); + + inline TYPE GetLength() const; + inline TYPE GetLengthSq() const; + inline POINT GetCenter() const; + inline VECTOR GetDir() const; + inline VECTOR GetNormDir() const; + inline RAY GetRay() const; + + inline bool IsSame(const TLine&, TYPE th) const; + + bool Intersects(const AABB& aabb) const; + bool Intersects(const AABB& aabb, TYPE& t) const; + + inline TYPE DistanceSq(const POINT&) const; + inline TYPE Distance(const POINT&) const; + + inline TYPE Classify(const POINT&) const; + inline POINT ProjectPoint(const POINT&) const; + + inline TYPE& operator [] (BYTE i) { ASSERT(i + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & pt1; + ar & pt2; + } + #endif +}; // class TLine +/*----------------------------------------------------------------*/ + +template +struct FitLineOnline : FitPlaneOnline { + template TPoint3 GetLine(TLine& line) const; +}; +/*----------------------------------------------------------------*/ + + +#include "Line.inl" +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_LINE_H__ diff --git a/libs/Common/Line.inl b/libs/Common/Line.inl new file mode 100644 index 0000000..bd7897c --- /dev/null +++ b/libs/Common/Line.inl @@ -0,0 +1,215 @@ +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + + +// C L A S S ////////////////////////////////////////////////////// + +template +inline TLine::TLine(const POINT& _pt1, const POINT& _pt2) + : + pt1(_pt1), pt2(_pt2) +{ + ASSERT(!ISZERO((_pt1-_pt2).norm())); +} // constructor +template +template +inline TLine::TLine(const TLine& rhs) + : + pt1(rhs.pt1.template cast()), pt2(rhs.pt2.template cast()) +{ +} // copy constructor +/*----------------------------------------------------------------*/ + + +// set attributes +template +inline void TLine::Set(const POINT& _pt1, const POINT& _pt2) +{ + ASSERT(!ISZERO((_pt1-_pt2).norm())); + pt1 = _pt1; + pt2 = _pt2; +} +/*----------------------------------------------------------------*/ + + +// least squares refinement of the line to the given 3D point set +// (return the number of iterations) +template +template +int TLine::Optimize(const POINT* points, size_t size, const RobustNormFunctor& robust, int maxIters) +{ + ASSERT(DIMS == 3); + ASSERT(size >= numParams); + struct OptimizationFunctor { + const POINT* points; + size_t size; + double scale; + const RobustNormFunctor& robust; + // construct with the data points + OptimizationFunctor(const POINT* _points, size_t _size, const RobustNormFunctor& _robust) + : points(_points), size(_size), robust(_robust) { ASSERT(size < std::numeric_limits::max()); } + static void Residuals(const double* x, int nPoints, const void* pData, double* fvec, double* fjac, int* /*info*/) { + const OptimizationFunctor& data = *reinterpret_cast(pData); + ASSERT((size_t)nPoints == data.size && fvec != NULL && fjac == NULL); + TLine line; + for (int j=0; j())); + } + } functor(points, size, robust); + double arrParams[numParams]; + for (int j=0; j +int TLine::Optimize(const POINT* points, size_t size, int maxIters) +{ + const auto identity = [](double x) { return x; }; + return Optimize(points, size, identity, maxIters); +} // Optimize +/*----------------------------------------------------------------*/ + + +// get attributes +template +inline TYPE TLine::GetLength() const +{ + return (pt2 - pt1).norm(); +} +template +inline TYPE TLine::GetLengthSq() const +{ + return (pt2 - pt1).squaredNorm(); +} +template +inline typename TLine::POINT TLine::GetCenter() const +{ + return (pt2 + pt1) / TYPE(2); +} +template +inline typename TLine::VECTOR TLine::GetDir() const +{ + return (pt2 - pt1); +} +template +inline typename TLine::VECTOR TLine::GetNormDir() const +{ + return (pt2 - pt1).normalized(); +} +template +inline typename TLine::RAY TLine::GetRay() const +{ + return RAY(pt1, GetNormDir()); +} +/*----------------------------------------------------------------*/ + + +template +inline bool TLine::IsSame(const TLine& line, TYPE th) const +{ + const TYPE thSq(SQUARE(th)); + const VECTOR l(pt2-pt1); + const TYPE invLenSq(INVERT(l.squaredNorm())); + const VECTOR r1(pt1-line.pt1); + const TYPE dSq1((l.cross(r1)).squaredNorm()*invLenSq); + if (dSq1 > thSq) + return false; + const VECTOR r2(pt1-line.pt2); + const TYPE dSq2((l.cross(r2)).squaredNorm()*invLenSq); + return dSq2 <= thSq; +} +/*----------------------------------------------------------------*/ + + +// test for intersection with aabb +template +bool TLine::Intersects(const AABB &aabb) const +{ + return GetRay().Intersects(aabb); +} // Intersects(AABB) +template +bool TLine::Intersects(const AABB &aabb, TYPE& t) const +{ + return GetRay().Intersects(aabb, t); +} // Intersects(AABB) +/*----------------------------------------------------------------*/ + +// Computes the distance between the line and a point. +template +inline TYPE TLine::DistanceSq(const POINT& pt) const +{ + const VECTOR l(pt2-pt1), r(pt1-pt); + if (DIMS == 2) + return TYPE(SQUARE(l[0]*r[1]-r[0]*l[1])/(l[0]*l[0]+l[1]*l[1])); + ASSERT(DIMS == 3); + return TYPE((l.cross(r)).squaredNorm()/l.squaredNorm()); +} // DistanceSq(POINT) +template +inline TYPE TLine::Distance(const POINT& pt) const +{ + return SQRT(DistanceSq(pt)); +} // Distance(POINT) +/*----------------------------------------------------------------*/ + + +// Computes the position on the line segment of the point projection. +// Returns 0 if it coincides with the first point, and 1 if it coincides with the second point. +template +inline TYPE TLine::Classify(const POINT& p) const +{ + const VECTOR vL(pt2 - pt1); + ASSERT(!ISZERO(vL.squaredNorm())); + const VECTOR vP(p - pt1); + return vL.dot(vP) / vL.squaredNorm(); +} // Classify(POINT) +// Calculate point's projection on this line (closest point to this line). +template +inline typename TLine::POINT TLine::ProjectPoint(const POINT& p) const +{ + const VECTOR vL(pt2 - pt1); + ASSERT(!ISZERO(vL.squaredNorm())); + const VECTOR vP(p - pt1); + return pt1 + vL * (vL.dot(vP) / vL.squaredNorm()); +} // ProjectPoint +/*----------------------------------------------------------------*/ + + +template +template +TPoint3 FitLineOnline::GetLine(TLine& line) const +{ + TPoint3 avg, dir; + const TPoint3 quality(this->GetModel(avg, dir)); + const TPoint3 pt2(avg+dir); + line.Set(TPoint3(avg), TPoint3(pt2)); + return TPoint3(quality); +} +/*----------------------------------------------------------------*/ diff --git a/libs/Common/LinkLib.h b/libs/Common/LinkLib.h new file mode 100644 index 0000000..dc793f6 --- /dev/null +++ b/libs/Common/LinkLib.h @@ -0,0 +1,111 @@ +//////////////////////////////////////////////////////////////////// +// LinkLib.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_LINKLIB_H__ +#define __SEACAVE_LINKLIB_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#ifdef _MSC_VER +#else +#include +#endif + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + + +/************************************************************************************** + * CLinkLib + * --------------- + * manage dynamic linked libraries + **************************************************************************************/ + +class GENERAL_API CLinkLib +{ +public: + #ifdef _MSC_VER + typedef HINSTANCE LibID; + #else + typedef void* LibID; + #endif + + CLinkLib() : m_hLib(NULL) + { + } + + CLinkLib(const String& dllName) : m_hLib(NULL) + { + Load(dllName); + } + + CLinkLib(const CLinkLib& lib) : m_hLib(lib.m_hLib) + { + ((CLinkLib&)lib).m_hLib = NULL; + } + + ~CLinkLib() + { + Free(); + } + + bool Load(const String& dllName) + { + Free(); + #ifdef _MSC_VER + m_hLib = LoadLibrary(dllName); + #else + m_hLib = dlopen(dllName, RTLD_NOW); + #endif + return (m_hLib != NULL); + } + + void Free() + { + if (!IsLoaded()) + return; + #ifdef _MSC_VER + FreeLibrary(m_hLib); + #else + dlclose(m_hLib); + #endif + m_hLib = NULL; + } + + void* GetFunction(const String& funcName) const + { + #ifdef _MSC_VER + return GetProcAddress(m_hLib, funcName); + #else + return dlsym(m_hLib, funcName); + #endif + } + + bool IsLoaded() const + { + return (m_hLib != NULL); + } + + LibID GetLibID() const + { + return m_hLib; + } + +protected: + LibID m_hLib; +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_LINKLIB_H__ diff --git a/libs/Common/List.h b/libs/Common/List.h new file mode 100644 index 0000000..fbe108d --- /dev/null +++ b/libs/Common/List.h @@ -0,0 +1,1704 @@ +//////////////////////////////////////////////////////////////////// +// List.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_LIST_H__ +#define __SEACAVE_LIST_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include + + +// D E F I N E S /////////////////////////////////////////////////// + +#ifndef _USE_VECTORINTERFACE +#define _USE_VECTORINTERFACE 1 +#endif + +// cList index type +#ifdef _SUPPORT_CPP11 +#define ARR2IDX(arr) typename std::remove_reference::type::size_type +#define SIZE2IDX(arr) typename std::remove_const::type>::type +#else +#define ARR2IDX(arr) IDX +#define SIZE2IDX(arr) IDX +#endif + +// cList iterator by index +#ifndef FOREACH +#define FOREACH(var, arr) for (ARR2IDX(arr) var=0, var##Size=(arr).size(); var0; ) +#endif +// cList iterator by pointer +#ifndef FOREACHPTR +#define FOREACHPTR(var, arr) for (auto var=(arr).begin(), var##End=(arr).end(); var!=var##End; ++var) +#endif +#ifndef RFOREACHPTR +#define RFOREACHPTR(var, arr) for (auto var=(arr).end(), var##Begin=(arr).begin(); var--!=var##Begin; ) +#endif + +// raw data array iterator by index +#ifndef FOREACHRAW +#define FOREACHRAW(var, sz) for (SIZE2IDX(sz) var=0, var##Size=(sz); var0; ) +#endif +// raw data array iterator by pointer +#ifndef FOREACHRAWPTR +#define FOREACHRAWPTR(var, arr, sz) for (auto var=(arr), var##End=var+sz; var!=var##End; ++var) +#endif +#ifndef RFOREACHRAWPTR +#define RFOREACHRAWPTR(var, arr, sz) for (auto var##Begin=(arr), var=var##Begin+sz; var--!=var##Begin; ) +#endif + +// constructs a cList reference to a given raw data array +#ifndef CLISTREFRAW +#define CLISTREFRAW(CLIST, var, arr, sz) uint8_t _ArrData##var[sizeof(CLIST)]; new(_ArrData##var) CLIST(sz, const_cast(arr)); const CLIST& var(*reinterpret_cast(_ArrData##var)) +#endif +// constructs a cList reference to a given std::_vector +#ifndef CLISTREFVECTOR +#define CLISTREFVECTOR(CLIST, var, vec) uint8_t _ArrData##var[sizeof(CLIST)]; new(_ArrData##var) CLIST(vec.size(), const_cast(&vec[0])); const CLIST& var(*reinterpret_cast(_ArrData##var)) +#endif + +#define CLISTDEF0(TYPE) SEACAVE::cList< TYPE, const TYPE&, 0 > +#define CLISTDEF2(TYPE) SEACAVE::cList< TYPE, const TYPE&, 2 > +#define CLISTDEF0IDX(TYPE,IDXTYPE) SEACAVE::cList< TYPE, const TYPE&, 0, 16, IDXTYPE > +#define CLISTDEFIDX(TYPE,IDXTYPE) SEACAVE::cList< TYPE, const TYPE&, 1, 16, IDXTYPE > +#define CLISTDEF2IDX(TYPE,IDXTYPE) SEACAVE::cList< TYPE, const TYPE&, 2, 16, IDXTYPE > + +#ifndef STCALL +#define STCALL +#endif + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +typedef size_t IDX; +#define NO_IDX DECLARE_NO_INDEX(IDX) + +/************************************************************************************** + * List template + * -------------- + * fast array + * - set "useConstruct" to 0 for not using constructors/destructors + * when adding/removing elements to the array + * - set "useConstruct" to 1 for using memcpy/memmove when moving arrays + * instead of copy-constructors + * - set "useConstruct" to 2 for always using constructors/destructors + * - set "grow" to the number of elements to increase the allocated array + * when more space is needed + * - if using sorting, the operator < needs to be implemented (the + * operator == is also needed if argument is not ARG_TYPE) + **************************************************************************************/ + +template < + typename TYPE, + typename ARG_TYPE=const TYPE&, + int useConstruct=1, + int grow=16, + typename IDX_TYPE=IDX> +class cList +{ +public: + typedef TYPE Type; + typedef ARG_TYPE ArgType; + typedef IDX_TYPE IDX; + typedef int (STCALL *TFncCompare)(const void* elem, const void* key); //returns 0 if equal, otherwise <0 or >0 respectively + + // construct an empty list + inline cList() : _size(0), _vectorSize(0), _vector(NULL) + { + } + + // construct a list containing size initialized elements + cList(IDX size) : _size(size), _vectorSize(size), _vector((TYPE*)operator new[] (size * sizeof(TYPE))) + { + ASSERT(size > 0 && size < NO_INDEX); + _ArrayConstruct(_vector, size); + } + + // construct a list containing size initialized elements and allocated space for _reserved elements + cList(IDX size, IDX _reserved) : _size(size), _vectorSize(_reserved), _vector((TYPE*)operator new[] (_reserved * sizeof(TYPE))) + { + ASSERT(_reserved >= size && _reserved < NO_INDEX); + _ArrayConstruct(_vector, size); + } + + // copy constructor: creates a deep-copy of the given list + cList(const cList& rList) : _size(rList._size), _vectorSize(rList._vectorSize), _vector(NULL) + { + if (_vectorSize == 0) { + ASSERT(_size == 0); + return; + } + _vector = (TYPE*)(operator new[] (_vectorSize * sizeof(TYPE))); + _ArrayCopyConstruct(_vector, rList._vector, _size); + } + #ifdef _SUPPORT_CPP11 + // copy constructor: creates a move-copy of the given list + cList(cList&& rList) : _size(rList._size), _vectorSize(rList._vectorSize), _vector(rList._vector) + { + rList._Init(); + } + #endif + + // constructor a list from a raw data array + explicit inline cList(TYPE* pDataBegin, TYPE* pDataEnd) : _size((IDX)(pDataEnd-pDataBegin)), _vectorSize(_size) + { + if (_vectorSize == 0) + return; + _vector = (TYPE*) operator new[] (_vectorSize * sizeof(TYPE)); + _ArrayCopyConstruct(_vector, pDataBegin, _size); + } + + // constructor a list from a raw data array, taking ownership of the array memory + explicit inline cList(IDX nSize, TYPE* pData) : _size(nSize), _vectorSize(nSize), _vector(pData) + { + } + + inline ~cList() + { + _Release(); + } + + // copy the content from the given list + inline cList& operator=(const cList& rList) + { + return CopyOf(rList); + } + + inline cList& CopyOf(const cList& rList, bool bForceResize=false) + { + if (this == &rList) + return *this; + if (bForceResize || _vectorSize < rList._vectorSize) { + _Release(); + _vectorSize = rList._vectorSize; + _vector = (TYPE*) operator new[] (_vectorSize * sizeof(TYPE)); + _ArrayCopyConstruct(_vector, rList._vector, rList._size); + } else { + if (_size >= rList._size) { + _ArrayDestruct(_vector+rList._size, _size-rList._size); + _ArrayCopyRestrict(_vector, rList._vector, rList._size); + } else { + _ArrayCopyRestrict(_vector, rList._vector, _size); + _ArrayCopyConstruct(_vector+_size, rList._vector+_size, rList._size-_size); + } + } + _size = rList._size; + return *this; + } + + inline cList& CopyOf(const TYPE* pData, IDX nSize, bool bForceResize=false) + { + if (_vector == pData) + return *this; + if (bForceResize || _vectorSize < nSize) { + _Release(); + _vectorSize = nSize; + _vector = (TYPE*) operator new[] (_vectorSize * sizeof(TYPE)); + _ArrayCopyConstruct(_vector, pData, nSize); + } else { + if (_size >= nSize) { + _ArrayDestruct(_vector+nSize, _size-nSize); + _ArrayCopyRestrict(_vector, pData, nSize); + } else { + _ArrayCopyRestrict(_vector, pData, _size); + _ArrayCopyConstruct(_vector+_size, pData+_size, nSize-_size); + } + } + _size = nSize; + return *this; + } + + // release current list and swap the content with the given list + inline cList& CopyOfRemove(cList& rList) + { + if (this == &rList) + return *this; + _Release(); + _size = rList._size; + _vectorSize = rList._vectorSize; + _vector = rList._vector; + rList._vector = NULL; + rList._size = rList._vectorSize = 0; + return *this; + } + + inline cList& Join(const cList& rList) + { + if (this == &rList || rList._size == 0) + return *this; + const IDX newSize = _size + rList._size; + Reserve(newSize); + _ArrayCopyConstruct(_vector+_size, rList._vector, rList._size); + _size = newSize; + return *this; + } + inline cList& Join(const TYPE* pData, IDX nSize) + { + const IDX newSize = _size + nSize; + Reserve(newSize); + _ArrayCopyConstruct(_vector+_size, pData, nSize); + _size = newSize; + return *this; + } + + template + inline cList& JoinFunctor(IDX nSize, const Functor& functor) { + Reserve(_size + nSize); + if (useConstruct) { + for (IDX n=0; n(_vector+_size, rList._vector, rList._size); + _size = newSize; + rList._size = 0; + return *this; + } + + // Swap the elements of the two lists. + inline cList& Swap(cList& rList) + { + if (this == &rList) + return *this; + const IDX tmpSize = _size; + _size = rList._size; + rList._size = tmpSize; + const IDX tmpVectorSize = _vectorSize; + _vectorSize = rList._vectorSize; + rList._vectorSize = tmpVectorSize; + TYPE* const tmpVector = _vector; + _vector = rList._vector; + rList._vector = tmpVector; + return *this; + } + + // Swap the two elements. + inline void Swap(IDX idx1, IDX idx2) + { + ASSERT(idx1 < _size && idx2 < _size); + TYPE tmp = _vector[idx1]; + _vector[idx1] = _vector[idx2]; + _vector[idx2] = tmp; + } + + inline bool operator==(const cList& rList) const { + if (_size != rList._size) + return false; + for (IDX i = 0; i < _size; ++i) + if (_vector[i] != rList._vector[i]) + return false; + return true; + } + + // Set the allocated memory (normally used for types without constructor). + inline void Memset(uint8_t val) + { + memset(_vector, val, _size * sizeof(TYPE)); + } + inline void MemsetValue(ARG_TYPE val) + { + std::fill_n(_vector, _size, val); + } + + // Delete the old array, and create a new one of the desired size; + // the old elements are deleted and the array is filled with new empty ones. + inline void Reset(IDX newSize) + { + _Release(); + _vectorSize = newSize; + _size = newSize; + if (newSize == 0) { + _vector = NULL; + return; + } + _vector = (TYPE*) operator new[] (newSize * sizeof(TYPE)); + _ArrayConstruct(_vector, newSize); + } + + // Pre-allocate memory for the array at the desired size; + // the old elements are kept. + inline void Reserve(IDX needVectorSize) + { + if (_vectorSize < needVectorSize) + _Grow(needVectorSize); + } + // Same as above, but only the extra needed space is passed. + inline void ReserveExtra(IDX needVectorExtraSize) + { + if (_vectorSize < _size+needVectorExtraSize) + _Grow(_vectorSize+MAXF(needVectorExtraSize,(IDX)grow)); + } + + // Set the size of the array at the new desired size; + // the old elements are kept, but only the ones that are below the new size. + // If increased, the array is filled with new empty elements. + inline void Resize(IDX newSize) + { + if (newSize == _size) + return; + if (newSize < _size) + return _Shrink(newSize); + Reserve(newSize); + _ArrayConstruct(_vector+_size, newSize-_size); + _size = newSize; + } + inline void ResizeExact(IDX newSize) + { + if (newSize == _size) + return; + if (newSize < _size) + return _ShrinkExact(newSize); + Reserve(newSize); + _ArrayConstruct(_vector+_size, newSize-_size); + _size = newSize; + } + + // Free unused memory. + inline void ReleaseFree() + { + if (_size < _vectorSize) + _ShrinkExact(_size); + } + + inline IDX GetIndex(const TYPE* pElem) const + { + ASSERT(pElem-_vector < _size); + return (IDX)(pElem-_vector); + } + + inline const TYPE& operator[](IDX index) const + { + ASSERT(index < _size); + return _vector[index]; + } + inline TYPE& operator[](IDX index) + { + ASSERT(index < _size); + return _vector[index]; + } + + inline TYPE* GetData() const + { + return _vector; + } + inline size_t GetDataSize() const + { + return sizeof(TYPE)*_size; + } + + inline TYPE* Begin() const + { + return _vector; + } + + inline TYPE* End() const + { + return _vector+_size; + } + + inline const TYPE& First() const + { + ASSERT(_size > 0); + return _vector[0]; + } + inline TYPE& First() + { + ASSERT(_size > 0); + return _vector[0]; + } + + inline const TYPE& Last() const + { + ASSERT(_size > 0); + return _vector[_size-1]; + } + inline TYPE& Last() + { + ASSERT(_size > 0); + return _vector[_size-1]; + } + + // Adds a new empty element at the end of the array. + inline TYPE& AddEmpty() + { + if (_vectorSize <= _size) + _Grow(_vectorSize + grow); + if (useConstruct) + return *(new(_vector + (_size++)) TYPE); + else + return _vector[_size++]; + } + inline TYPE* AddEmpty(IDX numElems) + { + const IDX newSize = _size + numElems; + if (_vectorSize < newSize) + _Grow(newSize + grow); + Type* const pData = _vector + _size; + _ArrayConstruct(pData, numElems); + _size = newSize; + return pData; + } + + // Adds a new empty element at the end of the array and pass the arguments to its constructor. + #ifdef _SUPPORT_CPP11 + template + inline TYPE& AddConstruct(Args&&... args) + { + if (_vectorSize <= _size) + _Grow(_vectorSize + grow); + return *(new(_vector + (_size++)) TYPE(std::forward(args)...)); + } + #else + template + inline TYPE& AddConstruct(Args... args) + { + if (_vectorSize <= _size) + _Grow(_vectorSize + grow); + return *(new(_vector + (_size++)) TYPE(args...)); + } + #endif + + inline IDX InsertEmpty() + { + if (_vectorSize <= _size) + _Grow(_vectorSize + grow); + if (useConstruct) + new(_vector + _size) TYPE; + return _size++; + } + + // Adds the new element at the end of the array. + #ifdef _SUPPORT_CPP11 + template + inline void Insert(T&& elem) + { + if (_vectorSize <= _size) + _Grow(_vectorSize + grow); + if (useConstruct) + new(_vector+(_size++)) TYPE(std::forward(elem)); + else + _vector[_size++] = std::forward(elem); + } + #else + inline void Insert(ARG_TYPE elem) + { + if (_vectorSize <= _size) + _Grow(_vectorSize + grow); + if (useConstruct) + new(_vector+(_size++)) TYPE(elem); + else + _vector[_size++] = elem; + } + #endif + + #ifdef _SUPPORT_CPP11 + template + inline void SetAt(IDX index, T&& elem) + { + if (_size <= index) + Resize(index + 1); + _vector[index] = std::forward(elem); + } + #else + inline void SetAt(IDX index, ARG_TYPE elem) + { + if (_size <= index) + Resize(index + 1); + _vector[index] = elem; + } + #endif + + #ifdef _SUPPORT_CPP11 + template + inline void AddAt(IDX index, T&& elem) + { + if (index < _size) + return InsertAt(index, std::forward(elem)); + const IDX newSize = index + 1; + if (_vectorSize <= newSize) + _Grow(newSize + grow); + _ArrayConstruct(_vector+_size, index-_size); + if (useConstruct) + new(_vector+index) TYPE(std::forward(elem)); + else + _vector[index] = std::forward(elem); + ++_size; + } + #else + inline void AddAt(IDX index, ARG_TYPE elem) + { + if (index < _size) + return InsertAt(index, elem); + const IDX newSize = index + 1; + if (_vectorSize <= newSize) + _Grow(newSize + grow); + _ArrayConstruct(_vector+_size, index-_size); + if (useConstruct) + new(_vector+index) TYPE(elem); + else + _vector[index] = elem; + ++_size; + } + #endif + + #ifdef _SUPPORT_CPP11 + template + inline void InsertAt(IDX index, T&& elem) + { + if (useConstruct) + new(AllocateAt(index)) TYPE(std::forward(elem)); + else + *AllocateAt(index) = std::forward(elem); + } + #else + inline void InsertAt(IDX index, ARG_TYPE elem) + { + if (useConstruct) + new(AllocateAt(index)) TYPE(elem); + else + *AllocateAt(index) = elem; + } + #endif + + // Same as Insert, but the constructor is not called. + inline TYPE* Allocate() + { + if (_vectorSize <= _size) + _Grow(_vectorSize + grow); + return _vector+_size++; + } + inline TYPE* AllocateAt(IDX index) + { + ASSERT(index <= _size); + const IDX move(_size-index); + Allocate(); + _ArrayMoveConstruct(_vector+index+1, _vector+index, move); + return _vector+index; + } + + inline IDX InsertSort(ARG_TYPE elem) + { + IDX l1(0), l2(_size); + while (l1 < l2) { + IDX i((l1 + l2) >> 1); + ARG_TYPE compElem(_vector[i]); + if (elem < compElem) + l2 = i; + else if (compElem < elem) + l1 = i+1; + else { + InsertAt(i, elem); + return i; + } + } + InsertAt(l1, elem); + return l1; + } + + inline IDX InsertSortPtr(ARG_TYPE elem) + { + IDX l1(0), l2(_size); + while (l1 < l2) { + IDX i((l1 + l2) >> 1); + ARG_TYPE compElem(_vector[i]); + if (*elem < *compElem) + l2 = i; + else if (*compElem < *elem) + l1 = i+1; + else { + InsertAt(i, elem); + return i; + } + } + InsertAt(l1, elem); + return l1; + } + + inline IDX InsertSort(ARG_TYPE elem, TFncCompare xCompare) + { + IDX l1(0), l2(_size); + while (l1 < l2) { + IDX i((l1 + l2) >> 1); + const int res(xCompare(_vector+i, &elem)); + if (res == 0) { + InsertAt(i, elem); + return i; + } + if (res < 0) + l1 = i+1; + else + l2 = i; + } + InsertAt(l1, elem); + return l1; + } + + inline TYPE& GetNth(IDX index) + { + ASSERT(index < _size); + TYPE* const nth(Begin()+index); + std::nth_element(Begin(), nth, End()); + return *nth; + } + template ::value,TYPE,REAL>::type> + inline RTYPE GetMedian() + { + ASSERT(_size > 0); + if (_size%2) + return static_cast(GetNth(_size >> 1)); + TYPE* const nth(Begin() + (_size>>1)); + std::nth_element(Begin(), nth, End()); + TYPE* const nth1(nth-1); + std::nth_element(Begin(), nth1, nth); + return (static_cast(*nth1) + static_cast(*nth)) / RTYPE(2); + } + + inline TYPE GetMean() + { + return std::accumulate(Begin(), End(), TYPE(0)) / _size; + } + + inline ArgType GetMax() const { + return *std::max_element(Begin(), End()); + } + template + inline ArgType GetMax(const Functor& functor) const { + return *std::max_element(Begin(), End(), functor); + } + inline IDX GetMaxIdx() const { + return static_cast(std::max_element(Begin(), End()) - Begin()); + } + template + inline IDX GetMaxIdx(const Functor& functor) const { + return static_cast(std::max_element(Begin(), End(), functor) - Begin()); + } + #ifdef _SUPPORT_CPP11 + inline std::pair GetMinMax() const { + const auto minmax(std::minmax_element(Begin(), End())); + return std::pair(*minmax.first, *minmax.second); + } + template + inline std::pair GetMinMax(const Functor& functor) const { + const auto minmax(std::minmax_element(Begin(), End(), functor)); + return std::pair(*minmax.first, *minmax.second); + } + inline std::pair GetMinMaxIdx() const { + const auto minmax(std::minmax_element(Begin(), End())); + return std::make_pair(static_cast(minmax.first-Begin()), static_cast(minmax.second-Begin())); + } + template + inline std::pair GetMinMaxIdx(const Functor& functor) const { + const auto minmax(std::minmax_element(Begin(), End(), functor)); + return std::make_pair(static_cast(minmax.first-Begin()), static_cast(minmax.second-Begin())); + } + #endif + + inline TYPE& PartialSort(IDX index) + { + TYPE* const nth(Begin()+index); + std::partial_sort(Begin(), nth, End()); + return *nth; + } + + inline void Sort() + { + std::sort(Begin(), End()); + } + template + inline void Sort(const Functor& functor) + { + std::sort(Begin(), End(), functor); + } + + inline bool IsSorted() const + { + #ifdef _SUPPORT_CPP11 + return std::is_sorted(Begin(), End()); + #else + if (_size < 2) + return true; + IDX i = _size-1; + do { + ARG_TYPE elem1 = _vector[i]; + ARG_TYPE elem0 = _vector[--i]; + if (elem1 < elem0) + return false; + } while (i > 0); + return true; + #endif + } + template + inline bool IsSorted(const Functor& functor) const + { + #ifdef _SUPPORT_CPP11 + return std::is_sorted(Begin(), End(), functor); + #else + if (_size < 2) + return true; + IDX i = _size-1; + do { + ARG_TYPE elem1 = _vector[i]; + ARG_TYPE elem0 = _vector[--i]; + if (functor(elem1, elem0)) + return false; + } while (i > 0); + return true; + #endif + } + + inline std::pair InsertSortUnique(ARG_TYPE elem) + { + IDX l1(0), l2(_size); + while (l1 < l2) { + IDX i((l1 + l2) >> 1); + ARG_TYPE compElem(_vector[i]); + if (elem < compElem) + l2 = i; + else if (compElem < elem) + l1 = i+1; + else + return std::make_pair(i,true); + } + InsertAt(l1, elem); + return std::make_pair(l1, false); + } + + inline std::pair InsertSortUniquePtr(ARG_TYPE elem) + { + IDX l1(0), l2(_size); + while (l1 < l2) { + IDX i((l1 + l2) >> 1); + ARG_TYPE compElem(_vector[i]); + if (*elem < *compElem) + l2 = i; + else if (*compElem < *elem) + l1 = i+1; + else + return std::make_pair(i, true); + } + InsertAt(l1, elem); + return std::make_pair(l1, false); + } + + inline std::pair InsertSortUnique(ARG_TYPE elem, TFncCompare xCompare) + { + IDX l1(0), l2(_size); + while (l1 < l2) { + IDX i((l1 + l2) >> 1); + const int res(xCompare(_vector+i, &elem)); + if (res == 0) + return std::make_pair(i, true); + if (res < 0) + l1 = i + 1; + else + l2 = i; + } + InsertAt(l1, elem); + return std::make_pair(l1, false); + } + + // insert given element if it is in the top N + template + inline void StoreTop(ARG_TYPE elem) { + const IDX idx(FindFirstEqlGreater(elem)); + if (idx < _size) { + if (_size >= N) + RemoveLast(); + InsertAt(idx, elem); + } else if (_size < N) { + Insert(elem); + } + } + inline void StoreTop(ARG_TYPE elem, IDX N) { + const IDX idx(FindFirstEqlGreater(elem)); + if (idx < _size) { + if (_size >= N) + RemoveLast(); + InsertAt(idx, elem); + } else if (_size < N) { + Insert(elem); + } + } + + inline IDX FindFirst(ARG_TYPE searchedKey) const + { + IDX l1(0), l2(_size); + while (l1 < l2) { + IDX i((l1 + l2) >> 1); + ARG_TYPE key(_vector[i]); + if (searchedKey < key) + l2 = i; + else if (key < searchedKey) + l1 = i + 1; + else + return i; + } + return NO_INDEX; + } + + inline IDX FindFirstPtr(ARG_TYPE searchedKey) const + { + IDX l1(0), l2(_size); + while (l1 < l2) { + IDX i((l1 + l2) >> 1); + ARG_TYPE key(_vector[i]); + if (*searchedKey < *key) + l2 = i; + else if (*key < *searchedKey) + l1 = i + 1; + else + return i; + } + return NO_INDEX; + } + + template + inline IDX FindFirst(const SEARCH_TYPE& searchedKey) const + { + IDX l1(0), l2(_size); + while (l1 < l2) { + IDX i((l1 + l2) >> 1); + ARG_TYPE key(_vector[i]); + if (key == searchedKey) + return i; + if (key < searchedKey) + l1 = i + 1; + else + l2 = i; + } + return NO_INDEX; + } + + template + inline IDX FindFirstPtr(const SEARCH_TYPE& searchedKey) const + { + IDX l1(0), l2(_size); + while (l1 < l2) { + IDX i((l1 + l2) >> 1); + ARG_TYPE key(_vector[i]); + if (*key == searchedKey) + return i; + if (*key < searchedKey) + l1 = i + 1; + else + l2 = i; + } + return NO_INDEX; + } + + inline IDX FindFirst(const void* searchedKey, TFncCompare xCompare) const + { + IDX l1(0), l2(_size); + while (l1 < l2) { + IDX i((l1 + l2) >> 1); + const int res(xCompare(_vector+i, searchedKey)); + if (res == 0) + return i; + if (res < 0) + l1 = i + 1; + else + l2 = i; + } + return NO_INDEX; + } + + inline IDX FindFirstBelow(ARG_TYPE searchedKey) const + { + if (_size == 0) return NO_INDEX; + IDX l1(0), l2(_size); + do { + IDX i((l1 + l2) >> 1); + ARG_TYPE key(_vector[i]); + if (searchedKey < key) + l2 = i; + else if (key < searchedKey) + l1 = i+1; + else { + while (i-- && !(_vector[i] < searchedKey)); + return i; + } + } while (l1 < l2); + return l1-1; + } + + template + inline IDX FindFirstBelow(const SEARCH_TYPE& searchedKey) const + { + if (_size == 0) return NO_INDEX; + IDX l1(0), l2(_size); + do { + IDX i((l1 + l2) >> 1); + ARG_TYPE key(_vector[i]); + if (key == searchedKey) { + while (i-- && _vector[i] == searchedKey); + return i; + } + if (key < searchedKey) + l1 = i+1; + else + l2 = i; + } while (l1 < l2); + return l1-1; + } + + inline IDX FindFirstEqlGreater(ARG_TYPE searchedKey) const + { + if (_size == 0) return 0; + IDX l1(0), l2(_size); + do { + IDX i((l1 + l2) >> 1); + ARG_TYPE key(_vector[i]); + if (searchedKey < key) + l2 = i; + else if (key < searchedKey) + l1 = i+1; + else { + while (i-- && !(_vector[i] < searchedKey)); + return i+1; + } + } while (l1 < l2); + return l1; + } + + template + inline IDX FindFirstEqlGreater(const SEARCH_TYPE& searchedKey) const + { + if (_size == 0) return 0; + IDX l1(0), l2(_size); + do { + IDX i((l1 + l2) >> 1); + ARG_TYPE key(_vector[i]); + if (key == searchedKey) { + while (i-- && _vector[i] == searchedKey); + return i+1; + } + if (key < searchedKey) + l1 = i+1; + else + l2 = i; + } while (l1 < l2); + return l1; + } + + // find the matching "elem" by brute-force (front to back) + // returns the first element found + inline IDX Find(ARG_TYPE elem) const + { + for (IDX i = 0; i < _size; ++i) + if (_vector[i] == elem) + return i; + return NO_INDEX; + } + template + inline IDX Find(const SEARCH_TYPE& searchedKey) const + { + for (IDX i = 0; i < _size; ++i) + if (_vector[i] == searchedKey) + return i; + return NO_INDEX; + } + template + inline IDX FindFunc(const Functor& functor) const + { + for (IDX i = 0; i < _size; ++i) + if (functor(_vector[i])) + return i; + return NO_INDEX; + } + + // find the matching "elem" by brute-force (back to front) + // returns the first element found + inline IDX RFind(ARG_TYPE elem) const + { + IDX i(_size); + while (i) + if (_vector[--i] == elem) + return i; + return NO_INDEX; + } + template + inline IDX RFind(const SEARCH_TYPE& searchedKey) const + { + IDX i(_size); + while (i) + if (_vector[--i] == searchedKey) + return i; + return NO_INDEX; + } + template + inline IDX RFindFunc(const Functor& functor) const + { + IDX i(_size); + while (i) + if (functor(_vector[--i])) + return i; + return NO_INDEX; + } + + // call a function or lambda on each element + template + inline void ForEach(const Functor& functor) const + { + FOREACH(i, *this) + functor(i); + } + template + inline void ForEachPtr(const Functor& functor) const + { + FOREACHPTR(ptr, *this) + functor(ptr); + } + template + inline void ForEachRef(const Functor& functor) const + { + FOREACHPTR(ptr, *this) + functor(*ptr); + } + // same, but in reverse order + template + inline void RForEach(const Functor& functor) const + { + RFOREACH(i, *this) + functor(i); + } + template + inline void RForEachPtr(const Functor& functor) const + { + RFOREACHPTR(ptr, *this) + functor(ptr); + } + template + inline void RForEachRef(const Functor& functor) const + { + RFOREACHPTR(ptr, *this) + functor(*ptr); + } + + // Erase each element matching "elem". + inline void Remove(ARG_TYPE elem) + { + IDX i = _size; + while (i) + if (_vector[--i] == elem) + RemoveAt(i); + } + + inline ARG_TYPE RemoveTail() + { + ASSERT(_size); + ASSERT(!useConstruct); + return _vector[--_size]; + } + + inline void RemoveLast() + { + ASSERT(_size); + _ArrayDestruct(_vector+(--_size), 1); + } + + inline void RemoveLast(IDX count) + { + ASSERT(count <= _size); + _ArrayDestruct(_vector+(_size-=count), count); + } + + inline void RemoveAt(IDX index) + { + ASSERT(index < _size); + if (index+1 == _size) + RemoveLast(); + else + _ArrayMoveCopy(_vector+index, _vector+(--_size), 1); + } + + inline void RemoveAt(IDX index, IDX count) + { + ASSERT(index+count <= _size); + if (index+count == _size) { + RemoveLast(count); + } else { + _size -= count; + const IDX move(_size-index); + TYPE* const vectorEnd(_vector+_size); + if (move < count) { + const IDX del(count-move); + _ArrayMoveCopy(_vector+index, vectorEnd+del, move); + _ArrayDestruct(vectorEnd, del); + } else { + _ArrayMoveCopy(_vector+index, vectorEnd, count); + } + } + } + + inline void RemoveAtMove(IDX index) + { + ASSERT(index < _size); + if (index+1 == _size) { + RemoveLast(); + } else { + _ArrayDestruct(_vector+index, 1); + _ArrayMoveConstructFwd(_vector+index, _vector+index+1, (--_size)-index); + } + } + + inline void RemoveAtMove(IDX index, IDX count) + { + ASSERT(index+count <= _size); + if (index+count == _size) { + RemoveLast(count); + } else { + _ArrayDestruct(_vector+index, count); + _ArrayMoveConstructFwd(_vector+index, _vector+index+count, (_size-=count)-index); + } + } + + inline void Empty() + { + _ArrayDestruct(_vector, _size); + _size = 0; + } + + // same as Empty(), plus free all allocated memory + inline void Release() + { + _Release(); + _Init(); + } + + // Discard all stored data and initialize it as an empty array. + // (note: call this only when you know what you are doing, + // you have to deallocate yourself all elements and _vector data) + inline void Reset() + { + _Init(); + } + + // Delete also the pointers (take care to use this function only if the elements are pointers). + inline void EmptyDelete() + { + while (_size) + delete _vector[--_size]; + } + + // same as EmptyDelete(), plus free all allocated memory + inline void ReleaseDelete() + { + EmptyDelete(); + operator delete[] (_vector); + _vector = NULL; + _vectorSize = 0; + } + + inline IDX GetCapacity() const + { + return _vectorSize; + } + + inline IDX GetSize() const + { + return _size; + } + + inline bool IsEmpty() const + { + return (_size == 0); + } + + static inline unsigned GetConstructType() + { + return useConstruct; + } + static inline IDX GetGrowSize() + { + return grow; + } + +protected: + // Free all memory. + inline void _Release() + { + _ArrayDestruct(_vector, _size); + operator delete[] (_vector); + } + + // Initialize array. + inline void _Init() + { + _vector = NULL; + _vectorSize = _size = 0; + } + + // Increase the size of the array at least to the specified amount. + inline void _Grow(IDX newVectorSize) + { + ASSERT(newVectorSize > _vectorSize); + // grow by 50% or at least to minNewVectorSize + const IDX expoVectorSize(_vectorSize + (_vectorSize>>1)); + if (newVectorSize < expoVectorSize) + newVectorSize = expoVectorSize; + // allocate a larger chunk of memory, copy the data and delete the old chunk + TYPE* const tmp(_vector); + _vector = (TYPE*) operator new[] (newVectorSize * sizeof(TYPE)); + _ArrayMoveConstruct(_vector, tmp, _size); + _vectorSize = newVectorSize; + operator delete[] (tmp); + } + + // Decrease the size of the array at the specified new size. + inline void _Shrink(IDX newSize) + { + ASSERT(newSize <= _size); + _ArrayDestruct(_vector+newSize, _size-newSize); + _size = newSize; + } + inline void _ShrinkExact(IDX newSize) + { + _Shrink(newSize); + _vectorSize = newSize; + if (newSize == 0) { + operator delete[] (_vector); + _vector = NULL; + } else { + TYPE* const tmp(_vector); + _vector = (TYPE*) operator new[] (_vectorSize * sizeof(TYPE)); + _ArrayMoveConstruct(_vector, tmp, _vectorSize); + operator delete[] (tmp); + } + } + + // Implement construct/destruct for the array elements. + static inline void _ArrayConstruct(TYPE* RESTRICT dst, IDX n) + { + if (useConstruct) { + while (n--) + new(dst+n) TYPE; + } + } + static inline void _ArrayCopyConstruct(TYPE* RESTRICT dst, const TYPE* RESTRICT src, IDX n) + { + if (useConstruct) { + while (n--) + new(dst+n) TYPE(src[n]); + } else { + memcpy((void*)dst, (const void*)src, n*sizeof(TYPE)); + } + } + static inline void _ArrayDestruct(TYPE* dst, IDX n) + { + if (useConstruct) { + while (n--) + (dst+n)->~TYPE(); + } + } + // Implement copy/move for the array elements. + static inline void _ArrayCopyRestrict(TYPE* RESTRICT dst, const TYPE* RESTRICT src, IDX n) + { + if (useConstruct) { + while (n--) + dst[n] = src[n]; + } else { + memcpy((void*)dst, (const void*)src, n*sizeof(TYPE)); + } + } + static inline void _ArrayMoveConstructFwd(TYPE* dst, TYPE* src, IDX n) + { + ASSERT(dst != src); + if (useConstruct > 1) { + for (IDX i=0; i~TYPE(); + } + } else { + memmove((void*)dst, (const void*)src, n*sizeof(TYPE)); + } + } + template + static inline void _ArrayMoveConstruct(TYPE* dst, TYPE* src, IDX n) + { + ASSERT(dst != src); + if (useConstruct > 1) { + while (n--) { + new(dst+n) TYPE(src[n]); + (src+n)->~TYPE(); + } + } else { + const size_t _size(sizeof(TYPE)*n); + if (bRestrict) + memcpy((void*)dst, (const void*)src, _size); + else + memmove((void*)dst, (const void*)src, _size); + } + } + template + static inline void _ArrayMoveCopy(TYPE* dst, TYPE* src, IDX n) + { + ASSERT(dst != src); + if (useConstruct > 1) { + while (n--) { + dst[n] = src[n]; + (src+n)->~TYPE(); + } + } else { + const size_t _size(sizeof(TYPE)*n); + if (useConstruct == 1) + while (n--) + (dst+n)->~TYPE(); + if (bRestrict) + memcpy((void*)dst, (const void*)src, _size); + else + memmove((void*)dst, (const void*)src, _size); + } + } + +protected: + IDX _size; + IDX _vectorSize; + TYPE* _vector; + +public: + enum : IDX { NO_INDEX = DECLARE_NO_INDEX(IDX) }; + +#if _USE_VECTORINTERFACE != 0 +public: + typedef IDX size_type; + typedef Type value_type; + typedef value_type* iterator; + typedef const value_type* const_iterator; + typedef value_type& reference; + typedef const value_type& const_reference; + typedef std::vector VectorType; + inline cList(const VectorType& rList) { CopyOf(&rList[0], rList.size()); } + #ifdef _SUPPORT_CPP11 + inline cList(std::initializer_list l) : _size(0), _vectorSize((size_type)l.size()), _vector(NULL) { ASSERT(l.size()_vector, elem); } + #ifdef _SUPPORT_CPP11 + template + inline reference emplace_back(Args&&... args) { return AddConstruct(std::forward(args)...); } + inline void push_back(value_type&& elem) { AddConstruct(elem); } + #endif + inline void push_back(const_reference elem) { Insert(elem); } + inline void pop_back() { RemoveLast(); } + inline void reserve(size_type newSize) { Reserve(newSize); } + inline void resize(size_type newSize) { Resize(newSize); } + inline void erase(const_iterator it) { RemoveAtMove(it-this->_vector); } + inline const_iterator cdata() const { return GetData(); } + inline const_iterator cbegin() const { return Begin(); } + inline const_iterator cend() const { return End(); } + inline iterator data() const { return GetData(); } + inline iterator begin() const { return Begin(); } + inline iterator end() const { return End(); } + inline ArgType front() const { return First(); } + inline ArgType back() const { return Last(); } + inline reference front() { return First(); } + inline reference back() { return Last(); } + inline void swap(cList& rList) { Swap(rList); } +#endif + +#ifdef _USE_BOOST +protected: + // implement BOOST serialization + friend class boost::serialization::access; + template + void save(Archive& ar, const unsigned int /*version*/) const { + ar & _size; + ar & boost::serialization::make_array(_vector, _size); + } + template + void load(Archive& ar, const unsigned int /*version*/) { + IDX newSize; + ar & newSize; + Resize(newSize); + ar & boost::serialization::make_array(_vector, _size); + } + BOOST_SERIALIZATION_SPLIT_MEMBER() +#endif +}; +/*----------------------------------------------------------------*/ + +template +inline bool ValidIDX(const IDX_TYPE& idx) { + return (idx != DECLARE_NO_INDEX(IDX_TYPE)); +} +/*----------------------------------------------------------------*/ + + +// some test functions +template +inline bool cListTest(unsigned iters) { + for (unsigned i=0; i arrR; + cList arr0; + cList arr1; + cList arr2; + cList arrC; + for (unsigned i=0; i= arrR.size()) + continue; + arrR.erase(arrR.begin()+nDel, arrR.begin()+nDel+nCount); + arr0.RemoveAtMove(nDel, nCount); + arr1.RemoveAtMove(nDel, nCount); + arr2.RemoveAtMove(nDel, nCount); + } + if (arrR.size() != arr0.GetSize() || + arrR.size() != arr1.GetSize() || + arrR.size() != arr2.GetSize()) { + ASSERT("there is a problem" == NULL); + return false; + } + for (size_t i=0; i arrS(1+RAND()%(2*elems)); + for (size_t i=0; i<6; ++i) { + arrS.Insert(RAND()); + } + arrS.RemoveLast(RAND()%arrS.GetSize()); + arrS.CopyOf(&arrR[0], arrR.size()); + for (size_t i=0; i<6; ++i) { + arrS.Insert(RAND()); + } + arrS.RemoveLast(6); + for (size_t i=0; i +class cListFixed { +public: + typedef TYPE Type; + typedef const TYPE& ArgType; + typedef unsigned IDX; + enum {MAX_SIZE = N}; + + inline cListFixed() : _size(0) {} + inline void CopyOf(const TYPE* pData, IDX nSize) { + memcpy(_vector, pData, nSize); + _size = nSize; + } + inline bool IsEmpty() const { + return (_size == 0); + } + inline IDX GetSize() const { + return _size; + } + inline const TYPE* Begin() const { + return _vector; + } + inline TYPE* Begin() { + return _vector; + } + inline const TYPE* End() const { + return _vector+_size; + } + inline TYPE* End() { + return _vector+_size; + } + inline const TYPE& First() const { + ASSERT(_size > 0); + return _vector[0]; + } + inline TYPE& First() { + ASSERT(_size > 0); + return _vector[0]; + } + inline const TYPE& Last() const { + ASSERT(_size > 0); + return _vector[_size-1]; + } + inline TYPE& Last() { + ASSERT(_size > 0); + return _vector[_size-1]; + } + inline const TYPE& operator[](IDX index) const { + ASSERT(index < _size); + return _vector[index]; + } + inline TYPE& operator[](IDX index) { + ASSERT(index < _size); + return _vector[index]; + } + inline bool operator==(const cListFixed& rList) const { + if (_size != rList._size) + return false; + for (IDX i = 0; i < _size; ++i) + if (_vector[i] != rList._vector[i]) + return false; + return true; + } + inline TYPE& AddEmpty() { + ASSERT(_size < N); + return _vector[_size++]; + } + inline void InsertAt(IDX index, ArgType elem) { + ASSERT(_size < N); + memmove(_vector+index+1, _vector+index, sizeof(TYPE)*(_size++ - index)); + _vector[index] = elem; + } + inline void Insert(ArgType elem) { + ASSERT(_size < N); + _vector[_size++] = elem; + } + inline void RemoveLast() { + ASSERT(_size); + --_size; + } + inline void RemoveAt(IDX index) { + ASSERT(index < _size); + if (index+1 == _size) + RemoveLast(); + else + memcpy(_vector+index, _vector+(--_size), sizeof(TYPE)); + } + inline void Empty() { + _ArrayDestruct(_vector, _size); + _size = 0; + } + inline void Sort() { + std::sort(Begin(), End()); + } + inline IDX Find(ArgType elem) const + { + IDX i = _size; + while (i) + if (_vector[--i] == elem) + return i; + return NO_INDEX; + } + inline IDX FindFirstEqlGreater(ArgType searchedKey) const { + if (_size == 0) return 0; + IDX l1 = 0, l2 = _size; + do { + IDX i = (l1 + l2) >> 1; + const TYPE& key = _vector[i]; + if (key == searchedKey) { + while (i-- && _vector[i] == searchedKey); + return i+1; + } + if (key < searchedKey) + l1 = i+1; + else + l2 = i; + } while (l1 < l2); + return l1; + } + inline void StoreTop(ArgType elem) { + const IDX idx(FindFirstEqlGreater(elem)); + if (idx < GetSize()) { + if (GetSize() >= N) + RemoveLast(); + InsertAt(idx, elem); + } else if (GetSize() < N) { + Insert(elem); + } + } + +protected: + TYPE _vector[N]; + IDX _size; + +public: + static const IDX NO_INDEX; + +#ifdef _USE_BOOST +protected: + // implement BOOST serialization + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & _size; + ar & boost::serialization::make_array(_vector, _size); + } +#endif +}; +template +const typename cListFixed::IDX cListFixed::NO_INDEX(DECLARE_NO_INDEX(IDX)); +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_LIST_H__ diff --git a/libs/Common/Log.cpp b/libs/Common/Log.cpp new file mode 100644 index 0000000..da19bd1 --- /dev/null +++ b/libs/Common/Log.cpp @@ -0,0 +1,453 @@ +//////////////////////////////////////////////////////////////////// +// Log.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" +#include "Log.h" + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +/*-----------------------------------------------------------* + * Log class implementation * + *-----------------------------------------------------------*/ + +#ifndef DEFAULT_LOGTYPE +Log::LogType Log::g_appType; +#endif + +/** + * Constructor + */ +Log::Log() +{ + // generate default log type + #ifndef DEFAULT_LOGTYPE + String appName = Util::getAppName(); + appName = (Util::getFileExt(appName) == _T(".exe") ? Util::getFileName(appName) : Util::getFileNameExt(appName)); + Idx n = MINF((Idx)appName.length(), (Idx)LOGTYPE_SIZE); + _tcsncpy(g_appType.szName, appName, n); + while (n < LOGTYPE_SIZE) + g_appType.szName[n++] = _T(' '); + g_appType.szName[LOGTYPE_SIZE] = _T('\0'); + #endif + ResetTypes(); +} + +void Log::RegisterListener(ClbkRecordMsg clbk) +{ + ASSERT(m_arrRecordClbk != NULL); + if (m_arrRecordClbk == NULL) + return; + m_arrRecordClbk->Insert(clbk); +} +void Log::UnregisterListener(ClbkRecordMsg clbk) +{ + ASSERT(m_arrRecordClbk != NULL); + if (m_arrRecordClbk == NULL) + return; + m_arrRecordClbk->Remove(clbk); +} + +//Register a new type of log messages (LOGTYPE_SIZE chars) +Log::Idx Log::RegisterType(LPCTSTR lt) +{ + ASSERT(strlen(lt) == LOGTYPE_SIZE); + const Idx idx = (Idx)m_arrLogTypes.GetSize(); + LogType& logType = m_arrLogTypes.AddEmpty(); + Idx n = MINF((Idx)strlen(lt), (Idx)LOGTYPE_SIZE); + _tcsncpy(logType.szName, lt, n); + while (n < LOGTYPE_SIZE) + logType.szName[n++] = _T(' '); + logType.szName[LOGTYPE_SIZE] = _T('\0'); + return idx; +} + +/** + * Empty the array with registered log types + */ +void Log::ResetTypes() +{ + m_arrLogTypes.Empty(); +} + +void Log::Write(LPCTSTR szFormat, ...) +{ + if (m_arrRecordClbk == NULL) + return; + va_list args; + va_start(args, szFormat); + _Record(NO_ID, szFormat, args); + va_end(args); +} +void Log::Write(Idx lt, LPCTSTR szFormat, ...) +{ + if (m_arrRecordClbk == NULL) + return; + va_list args; + va_start(args, szFormat); + _Record(lt, szFormat, args); + va_end(args); +} + +/** + * Write message to the log if this exists + * -> IN: Idx - log type + * LPCTSTR - format message + * ... - values + */ +void Log::_Record(Idx lt, LPCTSTR szFormat, va_list args) +{ + ASSERT(m_arrRecordClbk != NULL); + if (m_arrRecordClbk->IsEmpty()) + return; + + // Format a message by adding the date (auto adds new line) + TCHAR szTime[256]; + TCHAR szBuffer[2048]; + #if defined(LOG_DATE) || defined(LOG_TIME) + TCHAR* szPtrTime = szTime; + #ifdef _MSC_VER + SYSTEMTIME st; + GetLocalTime(&st); + #ifdef LOG_THREAD + WLock l(m_lock); + #endif + #ifdef LOG_DATE + szPtrTime += GetDateFormat(LOCALE_USER_DEFAULT,0,&st,_T("dd'.'MM'.'yy"),szPtrTime,80)-1; + #endif + #if defined(LOG_DATE) && defined(LOG_TIME) + szPtrTime[0] = _T('-'); ++szPtrTime; + #endif + #ifdef LOG_TIME + szPtrTime += GetTimeFormat(LOCALE_USER_DEFAULT,0,&st,_T("HH':'mm':'ss"),szPtrTime,80)-1; + #endif + #else // _MSC_VER + const time_t t = time(NULL); + const struct tm *tmp = localtime(&t); + #ifdef LOG_DATE + szPtrTime += strftime(szPtrTime, 80, "%y.%m.%d", tmp); + #endif + #if defined(LOG_DATE) && defined(LOG_TIME) + szPtrTime[0] = _T('-'); ++szPtrTime; + #endif + #ifdef LOG_TIME + szPtrTime += strftime(szPtrTime, 80, "%H:%M:%S", tmp); + #endif + #endif // _MSC_VER + #endif // LOG_DATE || LOG_TIME + #ifdef DEFAULT_LOGTYPE + LPCTSTR const logType(lt 2048) { + // not enough space for the full string, reprint dynamically + m_message.FormatSafe("%s [%s] %s" LINE_SEPARATOR_STR, szTime, logType, String::FormatStringSafe(szFormat, args).c_str()); + } else { + // enough space for all the string, print directly + m_message.Format("%s [%s] %s" LINE_SEPARATOR_STR, szTime, logType, szBuffer); + } + TRACE(m_message); + + // signal listeners + FOREACHPTR(pClbk, *m_arrRecordClbk) + (*pClbk)(m_message); +} +/*----------------------------------------------------------------*/ + + +/*-----------------------------------------------------------* + * LogFile class implementation * + *-----------------------------------------------------------*/ + +/** + * Constructor + */ +LogFile::LogFile() +{ +} + +bool LogFile::Open(LPCTSTR logName) +{ + Util::ensureFolder(logName); + m_ptrFile = new File(logName, File::WRITE, File::CREATE | File::TRUNCATE); + if (!m_ptrFile->isOpen()) { + m_ptrFile = NULL; + return false; + } + GET_LOG().RegisterListener(DELEGATEBINDCLASS(Log::ClbkRecordMsg, &LogFile::Record, this)); + return true; +} +void LogFile::Close() +{ + if (m_ptrFile == NULL) + return; + GET_LOG().UnregisterListener(DELEGATEBINDCLASS(Log::ClbkRecordMsg, &LogFile::Record, this)); + m_ptrFile = NULL; +} + +void LogFile::Pause() +{ + if (m_ptrFile == NULL) + return; + GET_LOG().UnregisterListener(DELEGATEBINDCLASS(Log::ClbkRecordMsg, &LogFile::Record, this)); +} +void LogFile::Play() +{ + if (m_ptrFile == NULL) + return; + GET_LOG().RegisterListener(DELEGATEBINDCLASS(Log::ClbkRecordMsg, &LogFile::Record, this)); +} + +void LogFile::Record(const String& msg) +{ + ASSERT(m_ptrFile != NULL); + m_ptrFile->print(msg); + m_ptrFile->flush(); +} +/*----------------------------------------------------------------*/ + + +/*-----------------------------------------------------------* + * LogConsole class implementation * + *-----------------------------------------------------------*/ + +#define MAX_CONSOLE_WIDTH 100 +#define MAX_CONSOLE_LINES 2000 + +#ifdef _MSC_VER +#include +#define STDIN_FILENO 0 /* file descriptor for stdin */ +#define STDOUT_FILENO 1 /* file descriptor for stdout */ +#define STDERR_FILENO 2 /* file descriptor for stderr */ +#else +#include +#endif +#include + +class LogConsoleOutbuf : public std::streambuf { +public: + LogConsoleOutbuf() { + setp(0, 0); + } + + virtual int_type overflow(int_type c = traits_type::eof()) { + return fputc(c, stdout) == EOF ? traits_type::eof() : c; + } +}; + +/** + * Constructor + */ +LogConsole::LogConsole() + : + #ifdef _USE_COSOLEFILEHANDLES + m_fileIn(NULL), m_fileOut(NULL), m_fileErr(NULL), + #else + m_fileIn(-1), m_fileOut(-1), m_fileErr(-1), + #endif + m_cout(NULL), m_coutOld(NULL), + m_cerr(NULL), m_cerrOld(NULL), + bManageConsole(false) +{ +} + +bool LogConsole::IsOpen() const +{ + #ifdef _USE_COSOLEFILEHANDLES + return (m_fileOut != NULL || m_fileIn != NULL || m_fileErr != NULL); + #else + return (m_fileOut != -1 || m_fileIn != -1 || m_fileErr != -1); + #endif +} + +#ifdef _MSC_VER + +void LogConsole::Open() +{ + if (IsOpen()) + return; + + // allocate a console for this app + bManageConsole = (AllocConsole()!=FALSE?true:false); + + // capture std::cout and std::cerr + if (bManageConsole && !m_cout) { + // set std::cout to use our custom streambuf + m_cout = new LogConsoleOutbuf; + m_coutOld = std::cout.rdbuf(m_cout); + // use same buffer for std::cerr as well + m_cerr = NULL; + m_cerrOld = std::cerr.rdbuf(m_cout); + } + + // set the screen buffer to be big enough to let us scroll text + CONSOLE_SCREEN_BUFFER_INFO coninfo; + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &coninfo); + coninfo.dwSize.X = MAX_CONSOLE_WIDTH; // does not resize the console window + coninfo.dwSize.Y = MAX_CONSOLE_LINES; + SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), coninfo.dwSize); + + #if 1 && defined(_USE_COSOLEFILEHANDLES) + // redirect standard stream to the console + m_fileIn = freopen("CONIN$", "r", stdin); + m_fileOut = freopen("CONOUT$", "w", stdout); + // there isn't any CONERR$, so that we merge stderr into CONOUT$ + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683231%28v=vs.85%29.aspx + m_fileErr = freopen("CONOUT$", "w", stderr); + #elif 1 && defined(_USE_COSOLEFILEHANDLES) + // Fix up the allocated console so that output works in all cases. + // Change std handles to refer to new console handles. Before doing so, ensure + // that stdout/stderr haven't been redirected to a valid file + // See http://support.microsoft.com/kb/105305/en-us + // stdout + if (_fileno(stdout) == -1 || _get_osfhandle(fileno(stdout)) == -1) { + int hCrt = ::_open_osfhandle((intptr_t)GetStdHandle(STD_OUTPUT_HANDLE), _O_TEXT); + if (hCrt != -1) { + m_fileOut = ::_fdopen(hCrt, "w"); + if (m_fileOut) + *stdout = *m_fileOut; + } + } + // stderr + if (_fileno(stderr) == -1 || _get_osfhandle(fileno(stderr)) == -1) { + int hCrt = ::_open_osfhandle((intptr_t)::GetStdHandle(STD_ERROR_HANDLE), _O_TEXT); + if (hCrt != -1) { + m_fileErr = ::_fdopen(hCrt, "w"); + if (m_fileErr) + *stderr = *m_fileErr; + } + } + // stdin + if (_fileno(stdin) == -1 || _get_osfhandle(fileno(stdin)) == -1) { + int hCrt = ::_open_osfhandle((intptr_t)::GetStdHandle(STD_INPUT_HANDLE), _O_TEXT); + if (hCrt != -1) { + m_fileIn = ::_fdopen(hCrt, "r"); + if (m_fileIn) + *stdin = *m_fileIn; + } + } + #else + int hConHandle; + // redirect unbuffered STDIN to the console + hConHandle = _open_osfhandle((intptr_t)GetStdHandle(STD_INPUT_HANDLE), _O_TEXT); + m_fileIn = _dup(STDIN_FILENO); + _dup2(hConHandle, STDIN_FILENO); + _close(hConHandle); + setvbuf(stdin, NULL, _IONBF, 0); + // redirect unbuffered STDOUT to the console + hConHandle = _open_osfhandle((intptr_t)GetStdHandle(STD_OUTPUT_HANDLE), _O_TEXT); + m_fileOut = _dup(STDOUT_FILENO); + _dup2(hConHandle, STDOUT_FILENO); + _close(hConHandle); + setvbuf(stdout, NULL, _IONBF, 0); + // redirect unbuffered STDERR to the console + hConHandle = _open_osfhandle((intptr_t)GetStdHandle(STD_ERROR_HANDLE), _O_TEXT); + m_fileErr = _dup(STDERR_FILENO); + _dup2(hConHandle, STDERR_FILENO); + _close(hConHandle); + setvbuf(stderr, NULL, _IONBF, 0); + // make cout, wcout, cin, wcin, wcerr, cerr, wclog and clog + // point to console as well + std::ios::sync_with_stdio(); + #endif + + // register with our log system + GET_LOG().RegisterListener(DELEGATEBINDCLASS(Log::ClbkRecordMsg, &LogConsole::Record, this)); +} + +void LogConsole::Close() +{ + if (!IsOpen()) + return; + GET_LOG().UnregisterListener(DELEGATEBINDCLASS(Log::ClbkRecordMsg, &LogConsole::Record, this)); + #ifdef _USE_COSOLEFILEHANDLES + // close console stream handles + fclose(m_fileIn); m_fileIn = NULL; + fclose(m_fileOut); m_fileOut = NULL; + fclose(m_fileErr); m_fileErr = NULL; + #else + // restore STDIN + _dup2(m_fileIn, STDIN_FILENO); + _close(m_fileIn); + m_fileIn = -1; + // restore STDOUT + _dup2(m_fileOut, STDOUT_FILENO); + _close(m_fileOut); + m_fileOut = -1; + // restore STDERR + _dup2(m_fileErr, STDERR_FILENO); + _close(m_fileErr); + m_fileErr = -1; + #endif + // close console + if (bManageConsole) { + if (m_cout) { + // set std::cout to the original streambuf + std::cout.rdbuf(m_coutOld); + std::cerr.rdbuf(m_cerrOld); + delete m_cout; m_cout = NULL; + } + FreeConsole(); + } + #ifndef _USE_COSOLEFILEHANDLES + std::ios::sync_with_stdio(); + #endif +} + +void LogConsole::Record(const String& msg) +{ + ASSERT(IsOpen()); + printf(msg); + fflush(stdout); +} + +#else + +void LogConsole::Open() +{ + if (IsOpen()) + return; + ++m_fileIn; + // register with our log system + GET_LOG().RegisterListener(DELEGATEBINDCLASS(Log::ClbkRecordMsg, &LogConsole::Record, this)); +} + +void LogConsole::Close() +{ + if (!IsOpen()) + return; + // unregister with our log system + GET_LOG().UnregisterListener(DELEGATEBINDCLASS(Log::ClbkRecordMsg, &LogConsole::Record, this)); + --m_fileIn; +} + +void LogConsole::Record(const String& msg) +{ + ASSERT(IsOpen()); + printf(_T("%s"), msg.c_str()); + fflush(stdout); +} + +#endif // _MSC_VER + +void LogConsole::Pause() +{ + if (IsOpen()) + GET_LOG().UnregisterListener(DELEGATEBINDCLASS(Log::ClbkRecordMsg, &LogConsole::Record, this)); +} +void LogConsole::Play() +{ + if (IsOpen()) + GET_LOG().RegisterListener(DELEGATEBINDCLASS(Log::ClbkRecordMsg, &LogConsole::Record, this)); +} +/*----------------------------------------------------------------*/ diff --git a/libs/Common/Log.h b/libs/Common/Log.h new file mode 100644 index 0000000..44eb14d --- /dev/null +++ b/libs/Common/Log.h @@ -0,0 +1,211 @@ +//////////////////////////////////////////////////////////////////// +// Log.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_LOG_H__ +#define __SEACAVE_LOG_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + +// use file handles to access console +#define _USE_COSOLEFILEHANDLES + +//#define LOG_DATE // add date info for every log +#define LOG_TIME // add time info for every log +#define LOG_THREAD // make log multi-thread safe +#define LOG_STREAM // add stream support (operator <<) +#define LOGTYPE_SIZE 8 +#define DEFAULT_LOGTYPE _T("App ") + +#define DECLARE_LOG() \ + protected: static const Log::Idx ms_nLogType; +#define DEFINE_LOG(classname, log) \ + const Log::Idx classname::ms_nLogType(REGISTER_LOG(log)); + +#ifdef LOG_THREAD +#include "CriticalSection.h" +#endif + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class GENERAL_API Log +{ + DECLARE_SINGLETON(Log); + +public: + typedef uint32_t Idx; + typedef DELEGATE ClbkRecordMsg; + typedef cList ClbkRecordMsgArray; + typedef CSharedPtr ClbkRecordMsgArrayPtr; + +public: + // log methods + void Open() { m_arrRecordClbk = new ClbkRecordMsgArray; } + void Close() { m_arrRecordClbk = NULL; } + void Join(Log& log) { m_arrRecordClbk = log.m_arrRecordClbk; } + void RegisterListener(ClbkRecordMsg); + void UnregisterListener(ClbkRecordMsg); + Idx RegisterType(LPCTSTR); + void ResetTypes(); + void Write(LPCTSTR, ...); + void Write(Idx, LPCTSTR, ...); + + #ifdef LOG_STREAM + template inline Log& operator<<(const T& val) { + #ifdef LOG_THREAD + Lock l(m_cs); + std::ostringstream& ostr = m_streams[__THREAD__]; + #else + std::ostringstream& ostr = m_stream; + #endif + ostr << val; + const std::string& line = ostr.str(); + if (!line.empty() && *(line.end()-1) == _T('\n')) { + Write(line.substr(0, line.size()-1).c_str()); + ostr.str(_T("")); + } + return *this; + } + // the type of std::cout + typedef std::basic_ostream > CoutType; + // the function signature of std::endl + typedef CoutType& (*StandardEndLine)(CoutType&); + // define an operator<< to take in std::endl + inline Log& operator<<(StandardEndLine) { + #ifdef LOG_THREAD + Lock l(m_cs); + std::ostringstream& ostr = m_streams[__THREAD__]; + #else + std::ostringstream& ostr = m_stream; + #endif + Write(ostr.str().c_str()); + ostr.str(_T("")); + return *this; + } + #endif + +protected: + // write a message of a certain type to the log + void _Record(Idx, LPCTSTR, va_list); + +protected: + struct LogType { + TCHAR szName[LOGTYPE_SIZE+1]; + inline operator LPCTSTR () const { return szName; } + inline operator LPTSTR () { return szName; } + }; + typedef cList LogTypeArr; + + // log members + String m_message; // last recorded message + ClbkRecordMsgArrayPtr m_arrRecordClbk;// the array with all registered listeners + LogTypeArr m_arrLogTypes; // the array with all the registered log types + + #ifdef LOG_THREAD + // threading + RWLock m_lock; // mutex used to ensure multi-thread safety + #endif + + #ifdef LOG_STREAM + // streaming + #ifdef LOG_THREAD + typedef std::unordered_map StreamMap; + StreamMap m_streams; // stream object used to handle one log with operator << (one for each thread) + CriticalSection m_cs; // mutex used to ensure multi-thread safety for accessing m_streams + #else + std::ostringstream m_stream; // stream object used to handle one log with operator << + #endif + #endif + + // static + #ifndef DEFAULT_LOGTYPE + static LogType g_appType; + #endif +}; +#define GET_LOG() SEACAVE::Log::GetInstance() +#define OPEN_LOG() GET_LOG().Open() +#define CLOSE_LOG() GET_LOG().Close() +#define JOIN_LOG(log) GET_LOG().Join(log) +#define REGISTER_LOG(lt) GET_LOG().RegisterType(lt) +#define LOG GET_LOG().Write +#define SLOG(msg) GET_LOG() << msg +#ifndef _RELEASE +#define LOGV LOG // include extra details in the log +#else +#define LOGV(...) +#endif +/*----------------------------------------------------------------*/ + + +class GENERAL_API LogFile +{ + DECLARE_SINGLETON(LogFile); + +public: + ~LogFile() { Close(); } + + // log methods + bool Open(LPCTSTR); + void Close(); + void Pause(); + void Play(); + void Record(const String&); + +protected: + FilePtr m_ptrFile; // the log file +}; +#define GET_LOGFILE() LogFile::GetInstance() +#define OPEN_LOGFILE(log) GET_LOGFILE().Open(log) +#define CLOSE_LOGFILE() GET_LOGFILE().Close() +/*----------------------------------------------------------------*/ + + +class GENERAL_API LogConsole +{ + DECLARE_SINGLETON(LogConsole); + +public: + ~LogConsole() { Close(); } + + bool IsOpen() const; + + // log methods + void Open(); + void Close(); + void Pause(); + void Play(); + void Record(const String&); + +protected: + #ifdef _USE_COSOLEFILEHANDLES + typedef FILE* StreamHandle; + #else + typedef int StreamHandle; + #endif + StreamHandle m_fileIn; // the log file in + StreamHandle m_fileOut; // the log file out + StreamHandle m_fileErr; // the log file error + std::streambuf* m_cout; // the redirected cout stream + std::streambuf* m_coutOld; // the original cout stream + std::streambuf* m_cerr; // the redirected cerr stream + std::streambuf* m_cerrOld; // the original cout stream + bool bManageConsole; // remember if the console is created here or is an existing console +}; +#define GET_LOGCONSOLE() LogConsole::GetInstance() +#define OPEN_LOGCONSOLE() GET_LOGCONSOLE().Open() +#define CLOSE_LOGCONSOLE() GET_LOGCONSOLE().Close() +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_LOG_H__ diff --git a/libs/Common/MemFile.h b/libs/Common/MemFile.h new file mode 100644 index 0000000..86a0ce1 --- /dev/null +++ b/libs/Common/MemFile.h @@ -0,0 +1,182 @@ +//////////////////////////////////////////////////////////////////// +// MemFile.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_MEMFILE_H__ +#define __SEACAVE_MEMFILE_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Streams.h" + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class GENERAL_API MemFile : public IOStream { +public: + MemFile() : m_buffer(NULL), m_sizeBuffer(0), m_size(0), m_pos(0) { + } + MemFile(size_t initialSize) : m_buffer(new BYTE[initialSize]), m_sizeBuffer(initialSize), m_size(0), m_pos(0) { + } + MemFile(BYTE* data, size_t size) : m_buffer(data), m_sizeBuffer(0), m_size(size), m_pos(0) { + } + virtual ~MemFile() { + close(); + } + + static LPCSTR getClassType() { return "MemFile"; } + virtual LPCSTR getClassName() const { return MemFile::getClassType(); } + + bool isOpen() const { return m_buffer != NULL; } + + virtual void close() { + if (!isOpen()) + return; + if (m_sizeBuffer) + delete[] m_buffer; + m_sizeBuffer = 0; + m_buffer = NULL; + m_size = 0; + m_pos = 0; + } + + virtual size_f_t getSizeBuffer() const { + return m_sizeBuffer; + } + + virtual size_f_t getSizeLeft() const { + return m_size - m_pos; + } + + virtual size_f_t getSize() const { + return m_size; + } + + virtual bool setSize(size_f_t newSize) { + ASSERT(newSize >= 0); + if (newSize > m_sizeBuffer) + setMaxSize(newSize); + m_size = newSize; + if (m_pos > m_size) + m_pos = m_size; + return true; + } + + virtual bool growSize(size_f_t deltaSize) { + return setSize(m_size+deltaSize); + } + + virtual bool moveSize(size_f_t delataSize) { + return setSize(m_size + delataSize); + } + + virtual bool setMaxSize(size_f_t newSize) { + ASSERT(newSize > m_sizeBuffer); + // grow by 50% or at least to minNewVectorSize + const size_f_t expoSize(m_sizeBuffer + (m_sizeBuffer>>1)); + if (newSize < expoSize) + newSize = expoSize; + // allocate a larger chunk of memory, copy the data and delete the old chunk + BYTE* const tmp(m_buffer); + m_buffer = new BYTE[(size_t)newSize]; + if (!m_buffer) { + m_buffer = tmp; + return false; + } + if (m_size > newSize) { + m_size = newSize; + if (m_pos > m_size) + m_pos = m_size; + } + memcpy(m_buffer, tmp, (size_t)m_size); + if (m_sizeBuffer) + delete[] tmp; + m_sizeBuffer = newSize; + return true; + } + + virtual bool growMaxSize(size_f_t deltaSize) { + return setMaxSize(m_sizeBuffer+deltaSize); + } + + virtual bool ensureSize(size_f_t extraSize) { + const size_f_t newSize = m_size + extraSize; + if (newSize > m_sizeBuffer) + return setMaxSize(newSize); + return true; + } + + virtual size_f_t getPos() const { + return m_pos; + } + + virtual bool setPos(size_f_t pos) { + if (pos > m_size && !setSize(pos)) + return false; + m_pos = pos; + return true; + } + + virtual bool movePos(size_f_t delataPos) { + return setPos(m_pos + delataPos); + } + + virtual bool isEOF() { + return (m_pos == m_size); + } + + virtual size_t read(void* buf, size_t len) { + if (m_pos >= m_size) + return 0; + if (m_pos+(size_f_t)len > m_size) + len = (size_t)(m_size-m_pos); + memcpy(buf, m_buffer+m_pos, len); + m_pos += len; + return len; + } + + virtual size_t write(const void* buf, size_t len) { + const size_f_t endSize = m_pos+len; + if (endSize > m_size && !setSize(endSize)) + return 0; + memcpy(m_buffer+m_pos, buf, len); + m_pos += len; + return len; + } + + virtual BYTE* getData() { + return m_buffer + m_pos; + } + + virtual BYTE* getBuffer() { + return m_buffer; + } + + virtual size_t flush() { + return 0; + } + +protected: + BYTE* m_buffer; //buffer where we will store the data + size_f_t m_sizeBuffer; //size of the whole buffer, 0 if no buffer or not allocated here + size_f_t m_size; //size of the stored data + size_f_t m_pos; //current position in the stored data + +private: + MemFile(const MemFile&); + MemFile& operator=(const MemFile&); +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_MEMFILE_H__ diff --git a/libs/Common/OBB.h b/libs/Common/OBB.h new file mode 100644 index 0000000..adf321e --- /dev/null +++ b/libs/Common/OBB.h @@ -0,0 +1,127 @@ +//////////////////////////////////////////////////////////////////// +// OBB.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_OBB_H__ +#define __SEACAVE_OBB_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +template +class TAABB; + +template +class TRay; + +// Basic oriented bounding-box class +template +class TOBB +{ + STATIC_ASSERT(DIMS > 0 && DIMS <= 3); + +public: + typedef TYPE Type; + typedef Eigen::Matrix POINT; + typedef Eigen::Matrix MATRIX; + typedef SEACAVE::TAABB AABB; + typedef SEACAVE::TRay RAY; + typedef unsigned ITYPE; + typedef Eigen::Matrix TRIANGLE; + enum { numCorners = (DIMS==1 ? 2 : (DIMS==2 ? 4 : 8)) }; // 2^DIMS + enum { numScalar = (5*DIMS) }; + + MATRIX m_rot; // rotation matrix from world to local (orthonormal axes) + POINT m_pos; // translation from local to world (center-point) + POINT m_ext; // bounding box extents in local (half axis length) + + //--------------------------------------- + + inline TOBB() {} + inline TOBB(bool); + inline TOBB(const AABB&); + inline TOBB(const MATRIX& rot, const POINT& ptMin, const POINT& ptMax); + inline TOBB(const POINT* pts, size_t n); + inline TOBB(const POINT* pts, size_t n, const TRIANGLE* tris, size_t s); + template + inline TOBB(const TOBB&); + + inline void Set(const AABB&); // build from AABB + inline void Set(const MATRIX& rot, const POINT& ptMin, const POINT& ptMax); // build from rotation matrix from world to local, and local min/max corners + inline void Set(const POINT* pts, size_t n); // build from points + inline void Set(const POINT* pts, size_t n, const TRIANGLE* tris, size_t s); // build from triangles + inline void Set(const MATRIX& C, const POINT* pts, size_t n); // build from covariance matrix + inline void SetRotation(const MATRIX& C); // build rotation only from covariance matrix + inline void SetBounds(const POINT* pts, size_t n); // build size and center only from given points + + inline void BuildBegin(); // start online build for computing the rotation + inline void BuildAdd(const POINT&); // add a new point to the online build + inline void BuildEnd(); // end online build for computing the rotation + + inline bool IsValid() const; + + inline TOBB& Enlarge(TYPE); + inline TOBB& EnlargePercent(TYPE); + + inline void Translate(const POINT&); + inline void Transform(const MATRIX&); + + inline POINT GetCenter() const; + inline void GetCenter(POINT&) const; + + inline POINT GetSize() const; + inline void GetSize(POINT&) const; + + inline void GetCorners(POINT pts[numCorners]) const; + inline AABB GetAABB() const; + + inline TYPE GetVolume() const; + + bool Intersects(const POINT&) const; + + inline TYPE& operator [] (BYTE i) { ASSERT(i> (std::istream& st, TOBB& obb) { + st >> obb.m_rot; + st >> obb.m_pos; + st >> obb.m_ext; + return st; + } + + #ifdef _USE_BOOST + // implement BOOST serialization + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & m_rot; + ar & m_pos; + ar & m_ext; + } + #endif +}; // class TOBB +/*----------------------------------------------------------------*/ + + +#include "OBB.inl" +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_OBB_H__ diff --git a/libs/Common/OBB.inl b/libs/Common/OBB.inl new file mode 100644 index 0000000..6a8dd76 --- /dev/null +++ b/libs/Common/OBB.inl @@ -0,0 +1,401 @@ +//////////////////////////////////////////////////////////////////// +// OBB.inl +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +template +inline TOBB::TOBB(bool) + : + m_rot(MATRIX::Identity()), + m_pos(POINT::Zero()), + m_ext(POINT::Zero()) +{ +} +template +inline TOBB::TOBB(const AABB& aabb) +{ + Set(aabb); +} +template +inline TOBB::TOBB(const MATRIX& rot, const POINT& ptMin, const POINT& ptMax) +{ + Set(rot, ptMin, ptMax); +} +template +inline TOBB::TOBB(const POINT* pts, size_t n) +{ + Set(pts, n); +} +template +inline TOBB::TOBB(const POINT* pts, size_t n, const TRIANGLE* tris, size_t s) +{ + Set(pts, n, tris, s); +} // constructor +template +template +inline TOBB::TOBB(const TOBB& rhs) + : + m_rot(rhs.m_rot.template cast()), + m_pos(rhs.m_pos.template cast()), + m_ext(rhs.m_ext.template cast()) +{ +} // copy constructor +/*----------------------------------------------------------------*/ + + +template +inline void TOBB::Set(const AABB& aabb) +{ + m_rot.setIdentity(); + m_pos = aabb.GetCenter(); + m_ext = aabb.GetSize()/TYPE(2); +} + +// build from rotation matrix from world to local, and local min/max corners +template +inline void TOBB::Set(const MATRIX& rot, const POINT& ptMin, const POINT& ptMax) +{ + m_rot = rot; + m_pos = (ptMax + ptMin) * TYPE(0.5); + m_ext = (ptMax - ptMin) * TYPE(0.5); +} + +// Inspired from "Fitting Oriented Bounding Boxes" by James Gregson +// http://jamesgregson.blogspot.ro/2011/03/latex-test.html + +// build an OBB from a vector of input points. This +// method just forms the covariance matrix and hands +// it to the build_from_covariance_matrix method +// which handles fitting the box to the points +template +inline void TOBB::Set(const POINT* pts, size_t n) +{ + ASSERT(n >= DIMS); + + // loop over the points to find the mean point + // location and to build the covariance matrix; + // note that we only have + // to build terms for the upper triangular + // portion since the matrix is symmetric + POINT mu(POINT::Zero()); + TYPE cxx=0, cxy=0, cxz=0, cyy=0, cyz=0, czz=0; + for (size_t i=0; i +inline void TOBB::Set(const POINT* pts, size_t n, const TRIANGLE* tris, size_t s) +{ + ASSERT(n >= DIMS); + + // loop over the triangles this time to find the + // mean location + POINT mu(POINT::Zero()); + TYPE Am=0; + TYPE cxx=0, cxy=0, cxz=0, cyy=0, cyz=0, czz=0; + for (size_t i=0; i +inline void TOBB::Set(const MATRIX& C, const POINT* pts, size_t n) +{ + // extract rotation from the covariance matrix + SetRotation(C); + // extract size and center from the given points + SetBounds(pts, n); +} +// method to set the OBB rotation which produce a box oriented according to +// the covariance matrix C (only the rotations is set) +template +inline void TOBB::SetRotation(const MATRIX& C) +{ + // extract the eigenvalues and eigenvectors from C + const Eigen::SelfAdjointEigenSolver es(C); + ASSERT(es.info() == Eigen::Success); + // find the right, up and forward vectors from the eigenvectors + // and set the rotation matrix using the eigenvectors + ASSERT(es.eigenvalues()(0) < es.eigenvalues()(1) && es.eigenvalues()(1) < es.eigenvalues()(2)); + m_rot = es.eigenvectors().transpose(); + if (m_rot.determinant() < 0) + m_rot = -m_rot; +} +// method to set the OBB center and size that contains the given points +// the rotations should be already set +template +inline void TOBB::SetBounds(const POINT* pts, size_t n) +{ + ASSERT(n >= DIMS); + ASSERT(ISEQUAL((m_rot*m_rot.transpose()).trace(), TYPE(3)) && ISEQUAL(m_rot.determinant(), TYPE(1))); + + // build the bounding box extents in the rotated frame + const TYPE tmax = std::numeric_limits::max(); + POINT minim(tmax, tmax, tmax), maxim(-tmax, -tmax, -tmax); + for (size_t i=0; i p_prime(0)) minim(0) = p_prime(0); + if (minim(1) > p_prime(1)) minim(1) = p_prime(1); + if (minim(2) > p_prime(2)) minim(2) = p_prime(2); + if (maxim(0) < p_prime(0)) maxim(0) = p_prime(0); + if (maxim(1) < p_prime(1)) maxim(1) = p_prime(1); + if (maxim(2) < p_prime(2)) maxim(2) = p_prime(2); + } + + // set the center of the OBB to be the average of the + // minimum and maximum, and the extents be half of the + // difference between the minimum and maximum + const POINT center((maxim+minim)*TYPE(0.5)); + m_pos = m_rot.transpose() * center; + m_ext = (maxim-minim)*TYPE(0.5); +} // Set +/*----------------------------------------------------------------*/ + + +template +inline void TOBB::BuildBegin() +{ + m_rot = MATRIX::Zero(); + m_pos = POINT::Zero(); + m_ext = POINT::Zero(); +} +template +inline void TOBB::BuildAdd(const POINT& p) +{ + // store mean in m_pos + m_pos += p; + // store covariance params in m_rot + m_rot(0,0) += p(0)*p(0); + m_rot(0,1) += p(0)*p(1); + m_rot(0,2) += p(0)*p(2); + m_rot(1,0) += p(1)*p(1); + m_rot(1,1) += p(1)*p(2); + m_rot(1,2) += p(2)*p(2); + // store count in m_ext + ++(*((size_t*)m_ext.data())); +} +template +inline void TOBB::BuildEnd() +{ + const TYPE invN(TYPE(1)/TYPE(*((size_t*)m_ext.data()))); + const TYPE cxx = (m_rot(0,0) - m_pos(0)*m_pos(0)*invN)*invN; + const TYPE cxy = (m_rot(0,1) - m_pos(0)*m_pos(1)*invN)*invN; + const TYPE cxz = (m_rot(0,2) - m_pos(0)*m_pos(2)*invN)*invN; + const TYPE cyy = (m_rot(1,0) - m_pos(1)*m_pos(1)*invN)*invN; + const TYPE cyz = (m_rot(1,1) - m_pos(1)*m_pos(2)*invN)*invN; + const TYPE czz = (m_rot(1,2) - m_pos(2)*m_pos(2)*invN)*invN; + + // now build the covariance matrix + MATRIX C; + C(0,0) = cxx; C(0,1) = cxy; C(0,2) = cxz; + C(1,0) = cxy; C(1,1) = cyy; C(1,2) = cyz; + C(2,0) = cxz; C(2,1) = cyz; C(2,2) = czz; + SetRotation(C); +} // Build +/*----------------------------------------------------------------*/ + + +// check if the oriented bounding box has positive size +template +inline bool TOBB::IsValid() const +{ + return m_ext.minCoeff() > TYPE(0); +} // IsValid +/*----------------------------------------------------------------*/ + + +template +inline TOBB& TOBB::Enlarge(TYPE x) +{ + m_ext.array() += x; + return *this; +} +template +inline TOBB& TOBB::EnlargePercent(TYPE x) +{ + m_ext *= x; + return *this; +} // Enlarge +/*----------------------------------------------------------------*/ + + +// Update the box by the given pos delta. +template +inline void TOBB::Translate(const POINT& d) +{ + m_pos += d; +} +// Update the box by the given transform. +template +inline void TOBB::Transform(const MATRIX& m) +{ + m_rot = m * m_rot; + m_pos = m * m_pos; +} +/*----------------------------------------------------------------*/ + + +template +inline typename TOBB::POINT TOBB::GetCenter() const +{ + return m_pos; +} +template +inline void TOBB::GetCenter(POINT& ptCenter) const +{ + ptCenter = m_pos; +} // GetCenter +/*----------------------------------------------------------------*/ + + +template +inline typename TOBB::POINT TOBB::GetSize() const +{ + return m_ext*2; +} +template +inline void TOBB::GetSize(POINT& ptSize) const +{ + ptSize = m_ext*2; +} // GetSize +/*----------------------------------------------------------------*/ + + +template +inline void TOBB::GetCorners(POINT pts[numCorners]) const +{ + if (DIMS == 2) { + const POINT pEAxis[2] = { + m_rot.row(0)*m_ext[0], + m_rot.row(1)*m_ext[1] + }; + const POINT pos(m_rot.transpose()*m_pos); + pts[0] = pos - pEAxis[0] - pEAxis[1]; + pts[1] = pos + pEAxis[0] - pEAxis[1]; + pts[2] = pos + pEAxis[0] + pEAxis[1]; + pts[3] = pos - pEAxis[0] + pEAxis[1]; + } + if (DIMS == 3) { + const POINT pEAxis[3] = { + m_rot.row(0)*m_ext[0], + m_rot.row(1)*m_ext[1], + m_rot.row(2)*m_ext[2] + }; + const POINT pos(m_rot.transpose()*m_pos); + pts[0] = pos - pEAxis[0] - pEAxis[1] - pEAxis[2]; + pts[1] = pos - pEAxis[0] - pEAxis[1] + pEAxis[2]; + pts[2] = pos + pEAxis[0] - pEAxis[1] - pEAxis[2]; + pts[3] = pos + pEAxis[0] - pEAxis[1] + pEAxis[2]; + pts[4] = pos + pEAxis[0] + pEAxis[1] - pEAxis[2]; + pts[5] = pos + pEAxis[0] + pEAxis[1] + pEAxis[2]; + pts[6] = pos - pEAxis[0] + pEAxis[1] - pEAxis[2]; + pts[7] = pos - pEAxis[0] + pEAxis[1] + pEAxis[2]; + } +} // GetCorners +// constructs the corner of the aligned bounding box in world space +template +inline typename TOBB::AABB TOBB::GetAABB() const +{ + POINT pts[numCorners]; + GetCorners(pts); + return AABB(pts, numCorners); +} // GetAABB +/*----------------------------------------------------------------*/ + +// computes the volume of the OBB, which is a measure of +// how tight the fit is (better OBBs will have smaller volumes) +template +inline TYPE TOBB::GetVolume() const +{ + return m_ext.prod()*numCorners; +} +/*----------------------------------------------------------------*/ + + +template +bool TOBB::Intersects(const POINT& pt) const +{ + const POINT dist(m_rot * (pt - m_pos)); + if (DIMS == 2) { + return ABS(dist[0]) <= m_ext[0] + && ABS(dist[1]) <= m_ext[1]; + } + if (DIMS == 3) { + return ABS(dist[0]) <= m_ext[0] + && ABS(dist[1]) <= m_ext[1] + && ABS(dist[2]) <= m_ext[2]; + } +} // Intersects(POINT) +/*----------------------------------------------------------------*/ diff --git a/libs/Common/Octree.h b/libs/Common/Octree.h new file mode 100644 index 0000000..de1bbad --- /dev/null +++ b/libs/Common/Octree.h @@ -0,0 +1,237 @@ +//////////////////////////////////////////////////////////////////// +// Octree.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_OCTREE_H__ +#define __SEACAVE_OCTREE_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "AABB.h" +#include "Ray.h" + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +// basic octree class +// each item should define the operator const POINT_TYPE& returning its center; +// at build time, a functor must be supplied that returns true as long as +// the current cell should be split farther, given its number of items and radius, ex: +// [](Octree::IDX_TYPE size, Octree::Type radius) { +// return size > SIZE && radius > RADIUS; +// } +// where: +// SIZE is the minimum number of items contained by the cell so that this to be divided further +// RADIUS is the minimum size of the cell allowed to be divided further +// both conditions represent exclusive limits and both should be true for the division to take place +template +class TOctree +{ + STATIC_ASSERT(DIMS > 0 && DIMS <= 3); + +public: + typedef TYPE Type; + typedef typename ITEMARR_TYPE::Type ITEM_TYPE; + typedef typename ITEMARR_TYPE::IDX IDX_TYPE; + typedef SEACAVE::cList IDXARR_TYPE; + typedef Eigen::Matrix POINT_TYPE; + typedef SEACAVE::TAABB AABB_TYPE; + typedef uint32_t SIZE_TYPE; + + class CELL_TYPE { + public: + typedef struct { + POINT_TYPE center; // center of the current cell + } NODE_TYPE; + typedef struct { + IDX_TYPE idxBegin; // index in the global array of the first item contained by this cell + SIZE_TYPE size; // number of items contained by this cell in the global array + DATA_TYPE data; // user data associated with this leaf + } LEAF_TYPE; + enum { numChildren = (2<<(DIMS-1)) }; + + public: + inline CELL_TYPE(); + inline ~CELL_TYPE(); + + inline void Release(); + inline void Swap(CELL_TYPE&); + + inline unsigned ComputeChild(const POINT_TYPE& item) const; + static void ComputeCenter(POINT_TYPE []); + static inline POINT_TYPE ComputeChildCenter(const POINT_TYPE&, TYPE, unsigned); + + inline bool IsLeaf() const { return (m_child==NULL); } + inline const CELL_TYPE& GetChild(int i) const { ASSERT(!IsLeaf() && i CELLPTRARR_TYPE; + + struct IndexInserter { + IDXARR_TYPE& indices; + IndexInserter(IDXARR_TYPE& _indices) : indices(_indices) {} + void operator()(IDX_TYPE idx) { indices.Insert(idx); } + void operator()(const IDX_TYPE* idices, SIZE_TYPE size) { indices.Join(idices, size); } + }; + + struct CellInserter { + CELLPTRARR_TYPE& cells; + CellInserter(CELLPTRARR_TYPE& _cells) : cells(_cells) {} + void operator()(CELL_TYPE& cell) { cells.Insert(&cell); } + }; + +public: + inline TOctree() {} + template + inline TOctree(const ITEMARR_TYPE&, Functor split); + template + inline TOctree(const ITEMARR_TYPE&, const AABB_TYPE&, Functor split); + + inline void Release(); + inline void Swap(TOctree&); + + template + void Insert(const ITEMARR_TYPE&, Functor split); + template + void Insert(const ITEMARR_TYPE&, const AABB_TYPE&, Functor split); + + template + inline void CollectCells(INSERTER&) const; + template + void CollectCells(const CELL_TYPE&, INSERTER&) const; + template + inline void ParseCells(PARSER&); + + template + inline void Collect(INSERTER& inserter, const AABB_TYPE& aabb) const; + inline void Collect(IDXARR_TYPE& indices, const AABB_TYPE& aabb) const; + inline void Collect(IDX_TYPE maxNeighbors, IDXARR_TYPE& indices, const AABB_TYPE& aabb) const; + + template + inline void Collect(INSERTER& inserter, const POINT_TYPE& center, TYPE radius) const; + inline void Collect(IDXARR_TYPE& indices, const POINT_TYPE& center, TYPE radius) const; + inline void Collect(IDX_TYPE maxNeighbors, IDXARR_TYPE& indices, const POINT_TYPE& center, TYPE radius) const; + + template + inline void Collect(INSERTER& inserter, const COLLECTOR& collector) const; + template + inline void Collect(IDXARR_TYPE& indices, const COLLECTOR& collector) const; + + template + inline void Traverse(const TFrustum&, INSERTER&) const; + template + inline void Traverse(const TFrustum&, IDXARR_TYPE&) const; + template + inline void TraverseCells(const TFrustum&, PARSER&); + template + inline void TraverseCells(const TFrustum&, CELLPTRARR_TYPE&); + + template + void SplitVolume(float maxArea, AREAESTIMATOR& areaEstimator, CHUNKINSERTER& chunkInserter); + + inline const CELL_TYPE& GetRoot() const { return m_root; } + inline TYPE GetRadius() const { return m_radius; } + inline AABB_TYPE GetAabb() const { return m_root.GetAabb(m_radius); } + inline bool IsEmpty() const { return m_indices.empty(); } + inline size_t GetNumItems() const { return m_indices.size(); } + inline const IDXARR_TYPE& GetIndexArr() const { return m_indices; } + inline const ITEM_TYPE* GetItems() const { return m_items; } + inline void ResetItems() { m_items = NULL; } + +protected: + template + struct _InsertData { + enum : IDX_TYPE { NO_INDEX = DECLARE_NO_INDEX(IDX_TYPE) }; + IDXARR_TYPE successors; // single connected list of next item indices + Functor split; // used to decide if a cell needs to be split farther + }; + template + void _Insert(CELL_TYPE&, const POINT_TYPE& center, TYPE radius, IDX_TYPE start, IDX_TYPE size, _InsertData&); + + template + void _ParseCells(CELL_TYPE&, TYPE, PARSER&); + + template + void _Collect(const CELL_TYPE&, const AABB_TYPE&, INSERTER&) const; + template + void _Collect(const CELL_TYPE&, TYPE, const COLLECTOR&, INSERTER&) const; + + template + void _Traverse(const CELL_TYPE&, TYPE, const TFrustum&, INSERTER&) const; + template + void _TraverseCells(CELL_TYPE&, TYPE, const TFrustum&, PARSER&); + + template + void _SplitVolume(const CELL_TYPE& parentCell, TYPE parentRadius, unsigned idxChild, float maxArea, AREAESTIMATOR& areaEstimator, CHUNKINSERTER& chunkInserter, const UnsignedArr& indices=UnsignedArr{0,1,2,3,4,5,6,7}); + +protected: + const ITEM_TYPE* m_items; // original input items (the only condition is that every item to resolve to a position) + IDXARR_TYPE m_indices; // indices to input items re-arranged spatially (as dictated by the octree) + CELL_TYPE m_root; // first cell of the tree (always of Node type) + TYPE m_radius; // size of the sphere containing all cells + +public: + typedef struct DEBUGINFO_TYPE { + size_t memSize; // total memory used + size_t memStruct; // memory used for the tree structure + size_t memItems; // memory used for the contained items + size_t numItems; // number of contained items + size_t numNodes; // total nodes... + size_t numLeaves; // ... from which this number of leaves + size_t minDepth; // minimum tree depth + size_t maxDepth; // maximum tree depth + float avgDepth; // average tree depth + void Init() { memset(this, 0, sizeof(DEBUGINFO_TYPE)); } + void operator += (const DEBUGINFO_TYPE& r) { + avgDepth = avgDepth*numNodes + r.avgDepth*r.numNodes; + memSize += r.memSize; memStruct += r.memStruct; memItems += r.memItems; + numItems += r.numItems; numNodes += r.numNodes; numLeaves += r.numLeaves; + if (minDepth > r.minDepth) minDepth = r.minDepth; + if (maxDepth < r.maxDepth) maxDepth = r.maxDepth; + avgDepth /= numNodes; + } + } DEBUGINFO; + + void GetDebugInfo(DEBUGINFO* =NULL, bool bPrintStats=false) const; + static void LogDebugInfo(const DEBUGINFO&); + +protected: + void _GetDebugInfo(const CELL_TYPE&, unsigned, DEBUGINFO&) const; +}; // class TOctree +/*----------------------------------------------------------------*/ + + +#include "Octree.inl" +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_OCTREE_H__ diff --git a/libs/Common/Octree.inl b/libs/Common/Octree.inl new file mode 100644 index 0000000..0a55929 --- /dev/null +++ b/libs/Common/Octree.inl @@ -0,0 +1,851 @@ +//////////////////////////////////////////////////////////////////// +// Octree.inl +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + + +// D E F I N E S /////////////////////////////////////////////////// + +#ifdef _USE_OPENMP +// minimum number of polygons for which we do multi-threading +#define OCTREE_MIN_ITEMS_MINTHREAD 1024*2 +#endif + + +// S T R U C T S /////////////////////////////////////////////////// + +template +inline TOctree::CELL_TYPE::CELL_TYPE() + : + m_child(NULL) +{ +} // constructor +template +inline TOctree::CELL_TYPE::~CELL_TYPE() +{ + delete[] m_child; +} // destructor +/*----------------------------------------------------------------*/ + + +template +inline void TOctree::CELL_TYPE::Release() +{ + delete[] m_child; + m_child = NULL; +} // Release +// swap the two octrees +template +inline void TOctree::CELL_TYPE::Swap(CELL_TYPE& rhs) +{ + std::swap(m_child, rhs.m_child); + if (IsLeaf()) + std::swap(m_leaf, rhs.m_leaf); + else + std::swap(m_node, rhs.m_node); +} // Swap +/*----------------------------------------------------------------*/ + + +// compute item's index corresponding to the containing cell +template +inline unsigned TOctree::CELL_TYPE::ComputeChild(const POINT_TYPE& item) const +{ + ASSERT(!IsLeaf()); + unsigned idx = 0; + if (item[0] >= Node().center[0]) + idx |= (1<<0); + if (DIMS > 1) + if (item[1] >= Node().center[1]) + idx |= (1<<1); + if (DIMS > 2) + if (item[2] >= Node().center[2]) + idx |= (1<<2); + return idx; +} // ComputeChild +/*----------------------------------------------------------------*/ + +template +void TOctree::CELL_TYPE::ComputeCenter(POINT_TYPE centers[]) +{ + if (DIMS == 1) { + centers[0] << -1; + centers[1] << 1; + } else + if (DIMS == 2) { + centers[0] << -1,-1; + centers[1] << 1,-1; + centers[2] << -1, 1; + centers[3] << 1, 1; + } else + if (DIMS == 3) { + centers[0] << -1,-1,-1; + centers[1] << 1,-1,-1; + centers[2] << -1, 1,-1; + centers[3] << 1, 1,-1; + centers[4] << -1,-1, 1; + centers[5] << 1,-1, 1; + centers[6] << -1, 1, 1; + centers[7] << 1, 1, 1; + } +} // ComputeCenter +/*----------------------------------------------------------------*/ + +template +inline typename TOctree::POINT_TYPE TOctree::CELL_TYPE::ComputeChildCenter(const POINT_TYPE& center, TYPE radius, unsigned idxChild) +{ + struct CENTERARR_TYPE { + POINT_TYPE child[CELL_TYPE::numChildren]; + inline CENTERARR_TYPE() { CELL_TYPE::ComputeCenter(child); } + }; + static const CENTERARR_TYPE centers; + return center + centers.child[idxChild] * radius; +} // ComputeChildCenter +/*----------------------------------------------------------------*/ + + +// count the number of items contained by the given octree-cell +template +size_t TOctree::CELL_TYPE::GetNumItemsHeld() const +{ + if (IsLeaf()) + return GetNumItems(); + size_t numItems = 0; + for (int i=0; i +template +inline TOctree::TOctree(const ITEMARR_TYPE& items, Functor split) +{ + Insert(items, split); +} +template +template +inline TOctree::TOctree(const ITEMARR_TYPE& items, const AABB_TYPE& aabb, Functor split) +{ + Insert(items, aabb, split); +} // constructor +/*----------------------------------------------------------------*/ + + +// destroy tree +template +inline void TOctree::Release() +{ + m_indices.Release(); + m_root.Release(); +} // Release +// swap the two octrees +template +inline void TOctree::Swap(TOctree& rhs) +{ + std::swap(m_items, rhs.m_items); + m_indices.Swap(rhs.m_indices); + m_root.Swap(rhs.m_root); + std::swap(m_radius, rhs.m_radius); +} // Swap +/*----------------------------------------------------------------*/ + + +// add the given item to the tree +template +template +void TOctree::_Insert(CELL_TYPE& cell, const POINT_TYPE& center, TYPE radius, IDX_TYPE start, IDX_TYPE size, _InsertData& insertData) +{ + ASSERT(size > 0); + // if this child cell needs to be divided further + if (bForceSplit || insertData.split(size, radius)) { + // init node and proceed recursively + ASSERT(cell.m_child == NULL); + cell.m_child = new CELL_TYPE[CELL_TYPE::numChildren]; + cell.Node().center = center; + struct ChildData { + enum { ESTART=0, EEND=CELL_TYPE::numChildren, ESIZE=CELL_TYPE::numChildren*2, EALL=CELL_TYPE::numChildren*3}; + IDX_TYPE data[EALL]; + ChildData() { memset(data, 0, sizeof(IDX_TYPE)*EALL); } + inline IDX_TYPE Start(unsigned i) const { return data[ESTART+i]; } + inline IDX_TYPE& Start(unsigned i) { return data[ESTART+i]; } + inline IDX_TYPE End(unsigned i) const { return data[EEND+i]; } + inline IDX_TYPE& End(unsigned i) { return data[EEND+i]; } + inline IDX_TYPE Size(unsigned i) const { return data[ESIZE+i]; } + inline IDX_TYPE& Size(unsigned i) { return data[ESIZE+i]; } + } childD; + IDX_TYPE idx(start); + for (IDX_TYPE i=0; i::NO_INDEX); + const TYPE childRadius(radius / TYPE(2)); + for (unsigned i=0; i::NO_INDEX; // mark the end of child successors + const POINT_TYPE childCenter(CELL_TYPE::ComputeChildCenter(center, childRadius, i)); + _Insert(child, childCenter, childRadius, childD.Start(i), childD.Size(i), insertData); + } + } else { + // init leaf + cell.Leaf().idxBegin = m_indices.size(); + cell.Leaf().size = (SIZE_TYPE)size; + for (IDX_TYPE idx=start; idx!=_InsertData::NO_INDEX; idx=insertData.successors[idx]) + m_indices.push_back(idx); + } +} // _Insert +/*----------------------------------------------------------------*/ + +template +template +inline void TOctree::Insert(const ITEMARR_TYPE& items, const AABB_TYPE& aabb, Functor split) +{ + Release(); + m_items = items.data(); + // create root as node, even if we do not need to divide + m_indices.Reserve(items.size()); + // divide cell + const POINT_TYPE center = aabb.GetCenter(); + m_radius = aabb.GetSize().maxCoeff()/Type(2); + // single connected list of next item indices + _InsertData insertData = {items.size(), split}; + std::iota(insertData.successors.begin(), insertData.successors.end(), IDX_TYPE(1)); + insertData.successors.back() = _InsertData::NO_INDEX; + // setup each cell + _Insert(m_root, center, m_radius, 0, items.size(), insertData); +} +template +template +inline void TOctree::Insert(const ITEMARR_TYPE& items, Functor split) +{ + ASSERT(!items.IsEmpty()); + ASSERT(sizeof(POINT_TYPE) == sizeof(typename ITEMARR_TYPE::Type)); + AABB_TYPE aabb((const POINT_TYPE*)items.data(), items.size()); + aabb.Enlarge(ZEROTOLERANCE()*TYPE(10)); + Insert(items, aabb, split); +} // Insert +/*----------------------------------------------------------------*/ + + +template +template +void TOctree::CollectCells(const CELL_TYPE& cell, INSERTER& inserter) const +{ + if (cell.IsLeaf()) { + inserter(m_indices.data()+cell.GetFirstItemIdx(), cell.GetNumItems()); + return; + } + for (int i=0; i +template +void TOctree::_ParseCells(CELL_TYPE& cell, TYPE radius, PARSER& parser) +{ + if (cell.IsLeaf()) { + parser(cell, radius); + return; + } + const TYPE childRadius = radius / TYPE(2); + for (int i=0; i +template +void TOctree::CollectCells(INSERTER& inserter) const +{ + CollectCells(m_root, inserter); +} +// calls parser for each leaf of the octree (the CELL_TYPE operator has to be defined) +template +template +void TOctree::ParseCells(PARSER& parser) +{ + _ParseCells(m_root, m_radius, parser); +} +/*----------------------------------------------------------------*/ + + +// find all items contained by the given bounding box +template +template +void TOctree::_Collect(const CELL_TYPE& cell, const AABB_TYPE& aabb, INSERTER& inserter) const +{ + if (cell.IsLeaf()) { + // add all items contained by the bounding-box + for (IDX_TYPE i=0; i +template +void TOctree::_Collect(const CELL_TYPE& cell, TYPE radius, const COLLECTOR& collector, INSERTER& inserter) const +{ + ASSERT(!cell.IsLeaf()); + const TYPE childRadius = radius / TYPE(2); + for (int i=0; i +template +inline void TOctree::Collect(INSERTER& inserter, const AABB_TYPE& aabb) const +{ + _Collect(m_root, aabb, inserter); +} +template +inline void TOctree::Collect(IDXARR_TYPE& indices, const AABB_TYPE& aabb) const +{ + IndexInserter inserter(indices); + _Collect(m_root, aabb, inserter); +} + +template +template +inline void TOctree::Collect(INSERTER& inserter, const POINT_TYPE& center, TYPE radius) const +{ + _Collect(m_root, AABB_TYPE(center, radius), inserter); +} +template +inline void TOctree::Collect(IDXARR_TYPE& indices, const POINT_TYPE& center, TYPE radius) const +{ + IndexInserter inserter(indices); + _Collect(m_root, AABB_TYPE(center, radius), inserter); +} + +template +template +inline void TOctree::Collect(INSERTER& inserter, const COLLECTOR& collector) const +{ + _Collect(m_root, m_radius, collector, inserter); +} +template +template +inline void TOctree::Collect(IDXARR_TYPE& indices, const COLLECTOR& collector) const +{ + IndexInserter inserter(indices); + _Collect(m_root, m_radius, collector, inserter); +} + +template +inline void TOctree::Collect(IDX_TYPE maxNeighbors, IDXARR_TYPE& indices, const AABB_TYPE& aabb) const +{ + _Collect(m_root, aabb, IndexInserter(indices)); + if (indices.size() > maxNeighbors) { + // keep only the closest neighbors + typedef TIndexScore ItemIndexScore; + typedef cList ItemIndexScoreArr; + ItemIndexScoreArr indexscores(indices.size()); + const POINT_TYPE center(aabb.GetCenter()); + FOREACH(i, indices) { + const IDX_TYPE& idx = indices[i]; + const TYPE score(-(center-m_items[idx]).squaredNorm()); + indexscores[i] = ItemIndexScore(idx,score); + } + indices.Empty(); + indexscores.Sort(); + for (IDX_TYPE i=0; i +inline void TOctree::Collect(IDX_TYPE maxNeighbors, IDXARR_TYPE& indices, const POINT_TYPE& center, TYPE radius) const +{ + Collect(maxNeighbors, indices, AABB_TYPE(center, radius)); +} // Collect +/*----------------------------------------------------------------*/ + + +// walk through the tree and collect visible indices +template +template +void TOctree::_Traverse(const CELL_TYPE& cell, TYPE radius, const TFrustum& frustum, INSERTER& inserter) const +{ + ASSERT(!cell.IsLeaf()); + switch (frustum.Classify(cell.GetAabb(radius))) { + case CLIPPED: { + const TYPE childRadius = radius / TYPE(2); + for (int i=0; i +template +void TOctree::_TraverseCells(CELL_TYPE& cell, TYPE radius, const TFrustum& frustum, PARSER& parser) +{ + ASSERT(!cell.IsLeaf()); + switch (frustum.Classify(cell.GetAabb(radius))) { + case CLIPPED: { + const TYPE childRadius = radius / TYPE(2); + for (int i=0; i +template +inline void TOctree::Traverse(const TFrustum& frustum, INSERTER& inserter) const +{ + _Traverse(m_root, m_radius, frustum, inserter); +} +template +template +inline void TOctree::Traverse(const TFrustum& frustum, IDXARR_TYPE& indices) const +{ + _Traverse(m_root, m_radius, frustum, IndexInserter(indices)); +} +template +template +inline void TOctree::TraverseCells(const TFrustum& frustum, PARSER& parser) +{ + _TraverseCells(m_root, m_radius, frustum, parser); +} +template +template +inline void TOctree::TraverseCells(const TFrustum& frustum, CELLPTRARR_TYPE& leaves) +{ + _TraverseCells(m_root, m_radius, frustum, CellInserter(leaves)); +} // Traverse +/*----------------------------------------------------------------*/ + +template +template +void TOctree::_SplitVolume(const CELL_TYPE& parentCell, TYPE parentRadius, unsigned idxChild, float maxArea, AREAESTIMATOR& areaEstimator, CHUNKINSERTER& chunkInserter, const UnsignedArr& indices) +{ + ASSERT(!indices.empty()); + typedef std::pair PairIndices; + struct GenerateSamples { + const UnsignedArr& indices; + const unsigned numSamples; + const unsigned halfSamples; + const unsigned numCommonAxis; + POINT_TYPE centers[8]; + cList arrHalfIndices; + GenerateSamples(const UnsignedArr& _indices) + : indices(_indices), numSamples((unsigned)indices.size()), halfSamples(numSamples/2), numCommonAxis(halfSamples==4?1:2), arrHalfIndices(0, numSamples) + { + ASSERT(indices.size()%2 == 0 && indices.IsSorted()); + ASSERT(halfSamples == 4 || halfSamples == 2); + CELL_TYPE::ComputeCenter(centers); + UnsignedArr samples(halfSamples); + for (unsigned hs=0; hs= maxArea) + ++numOverAreas; + if (numOverAreas == indices.size()) { + for (unsigned c: indices) + if (childArea[c] > 0) + _SplitVolume(cell, radius, c, maxArea, areaEstimator, chunkInserter); + return; + } + // split mesh children and retain the components with surface smaller than the given area + const cList halfIndices(std::move(GenerateSamples(indices).arrHalfIndices)); + IDX bestSplit(NO_ID); + float bestArea(0); + Point2f bestAs; + FOREACH(idx, halfIndices) { + const PairIndices& pairHalfIndices = halfIndices[idx]; + ASSERT(pairHalfIndices.first.size() == pairHalfIndices.second.size()); + Point2f as(Point2f::ZERO); + for (unsigned i=0; i qIndicesFirst(std::move(GenerateSamples(pairHalfIndices.first).arrHalfIndices)); + const cList qIndicesSecond(std::move(GenerateSamples(pairHalfIndices.second).arrHalfIndices)); + ASSERT(qIndicesFirst.size() == qIndicesSecond.size()); + FOREACH(q, qIndicesFirst) { + const PairIndices& qFirst = qIndicesFirst[q]; + const PairIndices& qSecond = qIndicesSecond[q]; + Eigen::Vector4f as(Eigen::Vector4f::Zero()); + for (unsigned i=0; i 0) { + // store found clusters + for (unsigned i=0; i<4; ++i) { + if (bestAs[i] < maxArea) { + chunkInserter(cell, radius, bestQIndices[i]); + } else { + _SplitVolume(cell, radius, bestQIndices[i][0], maxArea, areaEstimator, chunkInserter); + _SplitVolume(cell, radius, bestQIndices[i][1], maxArea, areaEstimator, chunkInserter); + } + } + return; + } + } + // split each child + for (unsigned c: indices) { + if (childArea[c] == 0) + continue; + if (childArea[c] < maxArea) + chunkInserter(cell, radius, UnsignedArr{c}); + else + _SplitVolume(cell, radius, c, maxArea, areaEstimator, chunkInserter); + } +} +template +template +void TOctree::SplitVolume(float maxArea, AREAESTIMATOR& areaEstimator, CHUNKINSERTER& chunkInserter) +{ + CELL_TYPE parent; + parent.m_child = new CELL_TYPE[1]; + parent.m_child[0].m_child = m_root.m_child; + parent.m_child[0].Node() = m_root.Node(); + parent.Node().center = m_root.Node().center + POINT_TYPE::Constant(m_radius); + _SplitVolume(parent, m_radius*TYPE(2), 0, maxArea, areaEstimator, chunkInserter); + parent.m_child[0].m_child = NULL; +} // SplitVolume +/*----------------------------------------------------------------*/ + + +template +void TOctree::_GetDebugInfo(const CELL_TYPE& cell, unsigned nDepth, DEBUGINFO& info) const +{ + if (cell.IsLeaf()) { + if (info.minDepth > nDepth) + info.minDepth = nDepth; + if (info.maxDepth < nDepth) + info.maxDepth = nDepth; + info.avgDepth += nDepth; + info.numLeaves++; + return; + } + nDepth++; + info.numNodes++; + for (int i=0; i +void TOctree::GetDebugInfo(DEBUGINFO* pInfo, bool bPrintStats) const +{ + DEBUGINFO localInfo; + DEBUGINFO& info = (pInfo ? *pInfo : localInfo); + info.Init(); + _GetDebugInfo(m_root, 0, info); + info.avgDepth /= info.numLeaves; + info.numItems = GetNumItems(); + info.numNodes += info.numLeaves; + info.memStruct = info.numNodes*sizeof(CELL_TYPE) + sizeof(TOctree); + info.memItems = sizeof(IDX_TYPE)*info.numItems; + info.memSize = info.memStruct + info.memItems; + if (pInfo == NULL || bPrintStats) + LogDebugInfo(info); +} // GetDebugInfo +/*----------------------------------------------------------------*/ + +template +void TOctree::LogDebugInfo(const DEBUGINFO& info) +{ + //VERBOSE("NoItems: %d; Mem %s; MemItems %s; MemStruct %s; AvgMemStruct %.2f%%%%; NoNodes %d; NoLeaf %d; AvgLeaf %.2f%%%%; AvgDepth %.2f; MinDepth %d; MaxDepth %d", + VERBOSE("NumItems %d; Mem %s (%s items, %s struct - %.2f%%%%); NumNodes %d (leaves %d - %.2f%%%%); Depth %.2f (%d min, %d max)", + info.numItems, + Util::formatBytes(info.memSize).c_str(), Util::formatBytes(info.memItems).c_str(), Util::formatBytes(info.memStruct).c_str(), double(info.memStruct)*100.0/info.memSize, + info.numNodes, info.numLeaves, float(info.numLeaves*100)/info.numNodes, + info.avgDepth, info.minDepth, info.maxDepth); +} // LogDebugInfo +/*----------------------------------------------------------------*/ + +// if everything works fine, this function should return true +template +inline bool OctreeTest(unsigned iters, unsigned maxItems=1000, bool bRandom=true) { + STATIC_ASSERT(DIMS > 0 && DIMS <= 3); + srand(bRandom ? (unsigned)time(NULL) : 0); + typedef Eigen::Matrix POINT_TYPE; + typedef CLISTDEF0(POINT_TYPE) TestArr; + typedef TOctree TestTree; + const TYPE ptMinData[] = {0,0,0}, ptMaxData[] = {640,480,240}; + typename TestTree::AABB_TYPE aabb; + aabb.Set(Eigen::Map(ptMinData), Eigen::Map(ptMaxData)); + aabb.Enlarge(ZEROTOLERANCE()*TYPE(10)); + unsigned nTotalMatches = 0; + unsigned nTotalMissed = 0; + unsigned nTotalExtra = 0; + #ifndef _RELEASE + typename TestTree::DEBUGINFO_TYPE totalInfo; + totalInfo.Init(); + #endif + for (unsigned iter=0; iter(RAND()%ROUND2INT(ptMaxData[j])); + // random query point + POINT_TYPE pt; + for (int j=0; j(RAND()%ROUND2INT(ptMaxData[j])); + const TYPE radius(TYPE(3+RAND()%30)); + // build octree and find interest items + TestTree tree(items, aabb, [](typename TestTree::IDX_TYPE size, typename TestTree::Type radius) { + return size > 16 && radius > 10; + }); + typename TestTree::IDXARR_TYPE indices; + tree.Collect(indices, pt, radius); + // find interest items by brute force + typename TestTree::IDXARR_TYPE trueIndices; + #if 1 + // use square bound + typename TestTree::AABB_TYPE aabbQuery(pt, radius); + FOREACH(i, items) + if (aabbQuery.Intersects(items[i])) + trueIndices.Insert(i); + #else + // use circle bound + FOREACH(i, items) + if ((items[i]-pt).norm() < radius) + trueIndices.Insert(i); + #endif + // compare results + unsigned nMatches = 0; + FOREACH(i, trueIndices) { + const typename TestTree::IDX_TYPE idx = trueIndices[i]; + FOREACH(j, indices) { + if (indices[j] == idx) { + ++nMatches; + break; + } + } + } + nTotalMatches += nMatches; + nTotalMissed += (unsigned)trueIndices.size()-nMatches; + nTotalExtra += (unsigned)indices.size()-nMatches; + #ifndef _RELEASE + // print stats + typename TestTree::DEBUGINFO_TYPE info; + tree.GetDebugInfo(&info); + totalInfo += info; + #endif + } + #ifndef _RELEASE + TestTree::LogDebugInfo(totalInfo); + VERBOSE("Test %s (TotalMissed %d, TotalExtra %d)", (nTotalMissed == 0 && nTotalExtra == 0 ? "successful" : "FAILED"), nTotalMissed, nTotalExtra); + #endif + return (nTotalMissed == 0 && nTotalExtra == 0); +} diff --git a/libs/Common/Plane.h b/libs/Common/Plane.h new file mode 100644 index 0000000..7b396e9 --- /dev/null +++ b/libs/Common/Plane.h @@ -0,0 +1,161 @@ +//////////////////////////////////////////////////////////////////// +// Plane.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_PLANE_H__ +#define __SEACAVE_PLANE_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +// Basic hyper-plane class +// (plane represented in Hessian Normal Form: n.x+d=0 <=> ax+by+cz+d=0) +template +class TPlane +{ + STATIC_ASSERT(DIMS > 0 && DIMS <= 3); + +public: + typedef Eigen::Matrix VECTOR; + typedef Eigen::Matrix POINT; + typedef SEACAVE::TAABB AABB; + typedef SEACAVE::TRay RAY; + enum { numScalar = DIMS+1 }; + enum { numParams = numScalar-1 }; + + VECTOR m_vN; // plane normal vector + TYPE m_fD; // distance to origin + + //--------------------------------------- + + inline TPlane() {} + inline TPlane(const VECTOR&, TYPE); + inline TPlane(const VECTOR&, const POINT&); + inline TPlane(const POINT&, const POINT&, const POINT&); + inline TPlane(const TYPE p[DIMS+1]); + inline TPlane(const Eigen::Matrix&); + + inline void Set(const VECTOR&, TYPE); + inline void Set(const VECTOR&, const POINT&); + inline void Set(const POINT&, const POINT&, const POINT&); + inline void Set(const TYPE p[DIMS+1]); + inline void Set(const Eigen::Matrix&); + + int Optimize(const POINT*, size_t, int maxIters=100); + template + int Optimize(const POINT*, size_t, const RobustNormFunctor& robust, int maxIters=100); + + inline void Invalidate(); + inline bool IsValid() const; + + inline void Negate(); + inline TPlane Negated() const; + + inline TYPE Distance(const TPlane&) const; + inline TYPE Distance(const POINT&) const; + inline TYPE DistanceAbs(const POINT&) const; + + inline POINT ProjectPoint(const POINT&) const; + + inline GCLASS Classify(const POINT&) const; + inline GCLASS Classify(const AABB&) const; + + bool Clip(const RAY&, TYPE, RAY*, RAY*) const; + + bool Intersects(const POINT& p0, const POINT& p1, const POINT& p2) const; + bool Intersects(const TPlane& plane, RAY& ray) const; + bool Intersects(const AABB& aabb) const; + + inline TYPE& operator [] (BYTE i) { ASSERT(i + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & m_vN; + ar & m_fD; + } + #endif +}; // class TPlane +/*----------------------------------------------------------------*/ + +template +struct FitPlaneOnline { + TYPEW sumX, sumSqX, sumXY, sumXZ; + TYPEW sumY, sumSqY, sumYZ; + TYPEW sumZ, sumSqZ; + size_t size; + FitPlaneOnline(); + void Update(const TPoint3& P); + TPoint3 GetModel(TPoint3& avg, TPoint3& dir) const; + template TPoint3 GetPlane(TPlane& plane) const; +}; +/*----------------------------------------------------------------*/ + + +// Basic 3D frustum class +// (represented as 6 planes oriented toward outside the frustum volume) +template +class TFrustum +{ + STATIC_ASSERT(DIMS > 0 && DIMS <= 6); + +public: + typedef Eigen::Matrix MATRIX4x4; + typedef Eigen::Matrix MATRIX3x4; + typedef Eigen::Matrix VECTOR; + typedef Eigen::Matrix POINT; + typedef SEACAVE::TPlane PLANE; + typedef SEACAVE::TSphere SPHERE; + typedef SEACAVE::TAABB AABB; + enum { numCorners = (1<<3) }; + + PLANE m_planes[DIMS]; // left, right, top, bottom, near and far planes + + //--------------------------------------- + + inline TFrustum() {} + inline TFrustum(const MATRIX4x4&, TYPE width, TYPE height, TYPE nearZ=TYPE(0.0001), TYPE farZ=TYPE(1000)); + inline TFrustum(const MATRIX3x4&, TYPE width, TYPE height, TYPE nearZ=TYPE(0.0001), TYPE farZ=TYPE(1000)); + + void Set(const MATRIX4x4&, TYPE width, TYPE height, TYPE nearZ=TYPE(0.0001), TYPE farZ=TYPE(1000)); + void Set(const MATRIX3x4&, TYPE width, TYPE height, TYPE nearZ=TYPE(0.0001), TYPE farZ=TYPE(1000)); + void Set(const VECTOR corners[numCorners]); + void SetProjectionGL(const MATRIX4x4&); + + GCLASS Classify(const POINT&) const; + GCLASS Classify(const SPHERE&) const; + GCLASS Classify(const AABB&) const; + + inline TYPE& operator [] (BYTE i) { ASSERT(i + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & m_planes; + } + #endif +}; // class TPlane +/*----------------------------------------------------------------*/ + + +#include "Plane.inl" +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_PLANE_H__ diff --git a/libs/Common/Plane.inl b/libs/Common/Plane.inl new file mode 100644 index 0000000..e1956cb --- /dev/null +++ b/libs/Common/Plane.inl @@ -0,0 +1,623 @@ +//////////////////////////////////////////////////////////////////// +// Plane.inl +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +// Construct plane given its normal and the distance from the origin. +template +inline TPlane::TPlane(const VECTOR& vN, TYPE fD) + : + m_vN(vN), m_fD(fD) +{ +} +// Construct plane given its normal and a point on the plane. +template +inline TPlane::TPlane(const VECTOR& vN, const POINT& p) + : + m_vN(vN), m_fD(-vN.dot(p)) +{ +} +// Construct plane given three points on the plane. +template +inline TPlane::TPlane(const POINT& p0, const POINT& p1, const POINT& p2) +{ + Set(p0, p1, p2); +} +// Construct plane given its standard equation: Ax + By + Cz + D = 0 +template +inline TPlane::TPlane(const TYPE p[DIMS+1]) +{ + Set(p); +} +// Construct plane given its standard equation: Ax + By + Cz + D = 0 +template +inline TPlane::TPlane(const Eigen::Matrix& v) +{ + Set(v); +} // constructors +/*----------------------------------------------------------------*/ + +template +inline void TPlane::Set(const VECTOR& vN, TYPE fD) +{ + m_vN = vN; + m_fD = fD; +} +template +inline void TPlane::Set(const VECTOR& vN, const POINT& p) +{ + m_vN = vN; + m_fD = -vN.dot(p); +} +template +inline void TPlane::Set(const POINT& p0, const POINT& p1, const POINT& p2) +{ + const VECTOR vcEdge1 = p1 - p0; + const VECTOR vcEdge2 = p2 - p0; + m_vN = vcEdge1.cross(vcEdge2).normalized(); + m_fD = -m_vN.dot(p0); +} +template +inline void TPlane::Set(const TYPE p[DIMS+1]) +{ + const Eigen::Map vN(p); + const TYPE invD(INVERT(vN.norm())); + Set(vN*invD, p[DIMS]*invD); +} +template +inline void TPlane::Set(const Eigen::Matrix& v) +{ + const VECTOR vN = v.template topLeftCorner<3,1>(); + const TYPE invD(INVERT(vN.norm())); + Set(vN*invD, v(DIMS)*invD); +} // Set +/*----------------------------------------------------------------*/ + + +// least squares refinement of the given plane to the 3D point set +// (return the number of iterations) +template +template +int TPlane::Optimize(const POINT* points, size_t size, const RobustNormFunctor& robust, int maxIters) +{ + ASSERT(DIMS == 3); + ASSERT(size >= numParams); + struct OptimizationFunctor { + const POINT* points; + const size_t size; + const RobustNormFunctor& robust; + // construct with the data points + OptimizationFunctor(const POINT* _points, size_t _size, const RobustNormFunctor& _robust) + : points(_points), size(_size), robust(_robust) { ASSERT(size < (size_t)std::numeric_limits::max()); } + static void Residuals(const double* x, int nPoints, const void* pData, double* fvec, double* fjac, int* /*info*/) { + const OptimizationFunctor& data = *reinterpret_cast(pData); + ASSERT((size_t)nPoints == data.size && fvec != NULL && fjac == NULL); + TPlane plane; { + Point3d N; + plane.m_fD = x[0]; + Dir2Normal(reinterpret_cast(x[1]), N); + plane.m_vN = N; + } + for (size_t i=0; i())); + } + } functor(points, size, robust); + double arrParams[numParams]; { + arrParams[0] = (double)m_fD; + const Point3d N(m_vN.x(), m_vN.y(), m_vN.z()); + Normal2Dir(N, reinterpret_cast(arrParams[1])); + } + lm_control_struct control = {1.e-6, 1.e-7, 1.e-8, 1.e-7, 100.0, maxIters}; // lm_control_float; + lm_status_struct status; + lmmin(numParams, arrParams, (int)size, &functor, OptimizationFunctor::Residuals, &control, &status); + switch (status.info) { + //case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + case 10: + case 11: + case 12: + DEBUG_ULTIMATE("error: refine plane: %s", lm_infmsg[status.info]); + return 0; + } + // set plane + { + Point3d N; + Dir2Normal(reinterpret_cast(arrParams[1]), N); + Set(Cast(N), (TYPE)arrParams[0]); + } + return status.nfev; +} +template +int TPlane::Optimize(const POINT* points, size_t size, int maxIters) +{ + const auto identity = [](double x) { return x; }; + return Optimize(points, size, identity, maxIters); +} // Optimize +/*----------------------------------------------------------------*/ + + +template +inline void TPlane::Invalidate() +{ + m_fD = std::numeric_limits::max(); +} // Invalidate +template +inline bool TPlane::IsValid() const +{ + return m_fD != std::numeric_limits::max(); +} // IsValid +/*----------------------------------------------------------------*/ + + +template +inline void TPlane::Negate() +{ + m_vN = -m_vN; + m_fD = -m_fD; +} // Negate +template +inline TPlane TPlane::Negated() const +{ + return TPlane(-m_vN, -m_fD); +} // Negated +/*----------------------------------------------------------------*/ + + +template +inline TYPE TPlane::Distance(const TPlane& p) const +{ + return ABS(m_fD - p.m_fD); +} +/*----------------------------------------------------------------*/ + +// Calculate distance to point. Plane normal must be normalized. +template +inline TYPE TPlane::Distance(const POINT& p) const +{ + return m_vN.dot(p) + m_fD; +} +template +inline TYPE TPlane::DistanceAbs(const POINT& p) const +{ + return ABS(Distance(p)); +} +/*----------------------------------------------------------------*/ + + +// Calculate point's projection on this plane (closest point to this plane). +template +inline typename TPlane::POINT TPlane::ProjectPoint(const POINT& p) const +{ + return p - m_vN*Distance(p); +} +/*----------------------------------------------------------------*/ + + +// Classify point to plane. +template +inline GCLASS TPlane::Classify(const POINT& p) const +{ + const TYPE f(Distance(p)); + if (f > ZEROTOLERANCE()) return FRONT; + if (f < -ZEROTOLERANCE()) return BACK; + return PLANAR; +} +/*----------------------------------------------------------------*/ + + +// Classify bounding box to plane. +template +inline GCLASS TPlane::Classify(const AABB& aabb) const +{ + const GCLASS classMin = Classify(aabb.ptMin); + const GCLASS classMax = Classify(aabb.ptMax); + if (classMin == classMax) return classMin; + return CLIPPED; +} +/*----------------------------------------------------------------*/ + + +// clips a ray into two segments if it intersects the plane +template +bool TPlane::Clip(const RAY& ray, TYPE fL, RAY* pF, RAY* pB) const +{ + POINT ptHit = POINT::ZERO; + + // ray intersects plane at all? + if (!ray.Intersects(*this, false, fL, NULL, &ptHit)) + return false; + + GCLASS n = Classify(ray.m_pOrig); + + // ray comes from planes backside + if ( n == BACK ) { + if (pB) pB->Set(ray.m_pOrig, ray.m_vDir); + if (pF) pF->Set(ptHit, ray.m_vDir); + } + // ray comes from planes front side + else if ( n == FRONT ) { + if (pF) pF->Set(ray.m_pOrig, ray.m_vDir); + if (pB) pB->Set(ptHit, ray.m_vDir); + } + + return true; +} // Clip(Ray) +/*----------------------------------------------------------------*/ + + +// Intersection of two planes. +// Returns the line of intersection. +// http://paulbourke.net/geometry/pointlineplane/ +template +bool TPlane::Intersects(const TPlane& plane, RAY& ray) const +{ + // if crossproduct of normals 0 than planes parallel + const VECTOR vCross(m_vN.cross(plane.m_vN)); + const TYPE fSqrLength(vCross.squaredNorm()); + if (fSqrLength < ZEROTOLERANCE()) + return false; + + // find line of intersection + #if 0 + // the general case + const TYPE fN00 = m_vN.squaredNorm(); + const TYPE fN01 = m_vN.dot(plane.m_vN); + const TYPE fN11 = plane.m_vN.squaredNorm(); + const TYPE fDet = fN01*fN01 - fN00*fN11; + const TYPE fInvDet = INVERT(fDet); + const TYPE fC0 = (fN11*m_fD - fN01*plane.m_fD) * fInvDet; + const TYPE fC1 = (fN00*plane.m_fD - fN01*m_fD) * fInvDet; + #else + // plane normals assumed to be normalized vectors + ASSERT(ISEQUAL(m_vN.norm(), TYPE(1))); + ASSERT(ISEQUAL(plane.m_vN.norm(), TYPE(1))); + const TYPE fN01 = m_vN.dot(plane.m_vN); + const TYPE fInvDet = INVERT(fN01*fN01-TYPE(1)); + const TYPE fC0 = (m_fD - fN01*plane.m_fD) * fInvDet; + const TYPE fC1 = (plane.m_fD - fN01*m_fD) * fInvDet; + #endif + + ray.m_vDir = vCross * RSQRT(fSqrLength); + ray.m_pOrig = m_vN*fC0 + plane.m_vN*fC1; + return true; +} // Intersects(Plane) +/*----------------------------------------------------------------*/ + + +// Intersection of a plane with a triangle. If all vertices of the +// triangle are on the same side of the plane, no intersection occurred. +template +bool TPlane::Intersects(const POINT& p0, const POINT& p1, const POINT& p2) const +{ + const GCLASS n(Classify(p0)); + if ((n == Classify(p1)) && + (n == Classify(p2))) + return false; + return true; +} // Intersects(Tri) +/*----------------------------------------------------------------*/ + + +// Intersection with AABB. Search for AABB diagonal that is most +// aligned to plane normal. Test its two vertices against plane. +// (M�ller/Haines, "Real-Time Rendering") +template +bool TPlane::Intersects(const AABB& aabb) const +{ + POINT Vmin, Vmax; + + // x component + if (m_vN(0) >= TYPE(0)) { + Vmin(0) = aabb.ptMin(0); + Vmax(0) = aabb.ptMax(0); + } + else { + Vmin(0) = aabb.ptMax(0); + Vmax(0) = aabb.ptMin(0); + } + + // y component + if (m_vN(1) >= TYPE(0)) { + Vmin(1) = aabb.ptMin(1); + Vmax(1) = aabb.ptMax(1); + } + else { + Vmin(1) = aabb.ptMax(1); + Vmax(1) = aabb.ptMin(1); + } + + // z component + if (m_vN(2) >= TYPE(0)) { + Vmin(2) = aabb.ptMin(2); + Vmax(2) = aabb.ptMax(2); + } + else { + Vmin(2) = aabb.ptMax(2); + Vmax(2) = aabb.ptMin(2); + } + + if (((m_vN * Vmin) + m_fD) > TYPE(0)) + return false; + + if (((m_vN * Vmax) + m_fD) >= TYPE(0)) + return true; + + return false; +} // Intersects(AABB) +/*----------------------------------------------------------------*/ + + +// same as above, but online version +template +FitPlaneOnline::FitPlaneOnline() + : sumX(0), sumSqX(0), sumXY(0), sumXZ(0), sumY(0), sumSqY(0), sumYZ(0), sumZ(0), sumSqZ(0), size(0) +{ +} +template +void FitPlaneOnline::Update(const TPoint3& P) +{ + const TYPEW X((TYPEW)P.x), Y((TYPEW)P.y), Z((TYPEW)P.z); + sumX += X; sumSqX += X*X; sumXY += X*Y; sumXZ += X*Z; + sumY += Y; sumSqY += Y*Y; sumYZ += Y*Z; + sumZ += Z; sumSqZ += Z*Z; + ++size; +} +template +TPoint3 FitPlaneOnline::GetModel(TPoint3& avg, TPoint3& dir) const +{ + const TYPEW avgX(sumX/(TYPEW)size), avgY(sumY/(TYPEW)size), avgZ(sumZ/(TYPEW)size); + // assemble covariance (lower-triangular) matrix + typedef Eigen::Matrix Mat3x3; + Mat3x3 A; + A(0,0) = sumSqX - TYPEW(2)*sumX*avgX + avgX*avgX*(TYPEW)size; + A(1,0) = sumXY - sumX*avgY - avgX*sumY + avgX*avgY*(TYPEW)size; + A(1,1) = sumSqY - TYPEW(2)*sumY*avgY + avgY*avgY*(TYPEW)size; + A(2,0) = sumXZ - sumX*avgZ - avgX*sumZ + avgX*avgZ*(TYPEW)size; + A(2,1) = sumYZ - sumY*avgZ - avgY*sumZ + avgY*avgZ*(TYPEW)size; + A(2,2) = sumSqZ - TYPEW(2)*sumZ*avgZ + avgZ*avgZ*(TYPEW)size; + // the plane normal is simply the eigenvector corresponding to least eigenvalue + const int nAxis(bFitLineMode ? 2 : 0); + const Eigen::SelfAdjointEigenSolver es(A); + ASSERT(ISEQUAL(es.eigenvectors().col(nAxis).norm(), TYPEW(1))); + avg = TPoint3(avgX,avgY,avgZ); + dir = es.eigenvectors().col(nAxis); + const TYPEW* const vals(es.eigenvalues().data()); + ASSERT(vals[0] <= vals[1] && vals[1] <= vals[2]); + return *reinterpret_cast*>(vals); +} +template +template +TPoint3 FitPlaneOnline::GetPlane(TPlane& plane) const +{ + TPoint3 avg, dir; + const TPoint3 quality(GetModel(avg, dir)); + plane.Set(TPoint3(dir), TPoint3(avg)); + return TPoint3(quality); +} +/*----------------------------------------------------------------*/ + + +// Least squares fits a plane to a 3D point set. +// See http://www.geometrictools.com/Documentation/LeastSquaresFitting.pdf +// Returns a fitting quality (1 - lambda_min/lambda_max): +// 1 is best (zero variance orthogonally to the fitting line) +// 0 is worst (isotropic case, returns a plane with default direction) +template +TYPE FitPlane(const TPoint3* points, size_t size, TPlane& plane) { + // compute a point on the plane, which is shown to be the centroid of the points + const Eigen::Map< const Eigen::Matrix > vPoints((const TYPE*)points, size, 3); + const TPoint3 c(vPoints.colwise().mean()); + + // assemble covariance matrix; matrix numbering: + // 0 + // 1 2 + // 3 4 5 + Eigen::Matrix A(Eigen::Matrix::Zero()); + FOREACHRAWPTR(pPt, points, size) { + const TPoint3 X(*pPt - c); + A(0,0) += X.x*X.x; + A(1,0) += X.x*X.y; + A(1,1) += X.y*X.y; + A(2,0) += X.x*X.z; + A(2,1) += X.y*X.z; + A(2,2) += X.z*X.z; + } + + // the plane normal is simply the eigenvector corresponding to least eigenvalue + const Eigen::SelfAdjointEigenSolver< Eigen::Matrix > es(A); + ASSERT(ISEQUAL(es.eigenvectors().col(0).norm(), TYPE(1))); + plane.Set(es.eigenvectors().col(0), c); + const TYPE* const vals(es.eigenvalues().data()); + ASSERT(vals[0] <= vals[1] && vals[1] <= vals[2]); + return TYPE(1) - vals[0]/vals[1]; +} +/*----------------------------------------------------------------*/ + + +// C L A S S ////////////////////////////////////////////////////// + +// Construct frustum given a projection matrix. +template +inline TFrustum::TFrustum(const MATRIX4x4& m, TYPE w, TYPE h, TYPE n, TYPE f) +{ + Set(m, w, h, n, f); +} +template +inline TFrustum::TFrustum(const MATRIX3x4& m, TYPE w, TYPE h, TYPE n, TYPE f) +{ + Set(m, w, h, n, f); +} // Constructor +/*----------------------------------------------------------------*/ + + +// Set frustum planes, normals pointing outwards, from SfM projection matrix and image plane details +template +void TFrustum::Set(const MATRIX4x4& m, TYPE w, TYPE h, TYPE n, TYPE f) +{ + const VECTOR ltn(0,0,n), rtn(w*n,0,n), lbn(0,h*n,n), rbn(w*n,h*n,n); + const VECTOR ltf(0,0,f), rtf(w*f,0,f), lbf(0,h*f,f), rbf(w*f,h*f,f); + const MATRIX4x4 inv(m.inverse()); + const VECTOR corners[] = { + (inv*ltn.homogeneous()).template topRows<3>(), + (inv*rtn.homogeneous()).template topRows<3>(), + (inv*lbn.homogeneous()).template topRows<3>(), + (inv*rbn.homogeneous()).template topRows<3>(), + (inv*ltf.homogeneous()).template topRows<3>(), + (inv*rtf.homogeneous()).template topRows<3>(), + (inv*lbf.homogeneous()).template topRows<3>(), + (inv*rbf.homogeneous()).template topRows<3>() + }; + Set(corners); +} +template +void TFrustum::Set(const MATRIX3x4& m, TYPE w, TYPE h, TYPE n, TYPE f) +{ + MATRIX4x4 M(MATRIX4x4::Identity()); + M.template topLeftCorner<3,4>() = m; + Set(M, w, h, n, f); +} +// Set frustum planes, normals pointing outwards, from the given corners +template +void TFrustum::Set(const VECTOR corners[numCorners]) +{ + // left clipping plane + m_planes[0].Set(corners[0], corners[4], corners[6]); + if (DIMS > 1) // right clipping plane + m_planes[1].Set(corners[1], corners[7], corners[5]); + if (DIMS > 2) // top clipping plane + m_planes[2].Set(corners[0], corners[5], corners[4]); + if (DIMS > 3) // bottom clipping plane + m_planes[3].Set(corners[2], corners[6], corners[7]); + if (DIMS > 4) // near clipping plane + m_planes[4].Set(corners[0], corners[2], corners[3]); + if (DIMS > 5) // far clipping plane + m_planes[5].Set(corners[4], corners[5], corners[7]); +} // Set +// Set frustum planes, normals pointing outwards, from the OpenGL projection-view matrix +template +void TFrustum::SetProjectionGL(const MATRIX4x4& mProjectionView) +{ + // left clipping plane + m_planes[0].Set(-mProjectionView.row(3) - mProjectionView.row(0)); + if (DIMS > 1) // right clipping plane + m_planes[1].Set(-mProjectionView.row(3) + mProjectionView.row(0)); + if (DIMS > 2) // top clipping plane + m_planes[2].Set(-mProjectionView.row(3) + mProjectionView.row(1)); + if (DIMS > 3) // bottom clipping plane + m_planes[3].Set(-mProjectionView.row(3) - mProjectionView.row(1)); + if (DIMS > 4) // near clipping plane + m_planes[4].Set(-mProjectionView.row(3) - mProjectionView.row(2)); + if (DIMS > 5) // far clipping plane + m_planes[5].Set(-mProjectionView.row(3) + mProjectionView.row(2)); +} +/*----------------------------------------------------------------*/ + + +/** + * Culls POINT to n sided frustum. Normals pointing outwards. + * -> IN: POINT - point to be tested + * OUT: VISIBLE - point inside frustum + * CULLED - point outside frustum + */ +template +GCLASS TFrustum::Classify(const POINT& p) const +{ + // check if on the front side of any of the planes + for (int i=0; i IN: POINT - center of the sphere to be tested + * TYPE - radius of the sphere to be tested + * OUT: VISIBLE - sphere inside frustum + * CLIPPED - sphere clipped by frustum + * CULLED - sphere outside frustum + */ +template +GCLASS TFrustum::Classify(const SPHERE& s) const +{ + // compute distances to each of the planes + for (int i=0; i s.radius) + return CULLED; + // if the distance is between +- radius, the sphere intersects the frustum + if (ABS(dist) < s.radius) + return CLIPPED; + } // for + // otherwise sphere is fully in view + return VISIBLE; +} + +/** + * Culls AABB to n sided frustum. Normals pointing outwards. + * -> IN: AABB - bounding box to be tested + * OUT: VISIBLE - aabb totally inside frustum + * CLIPPED - aabb clipped by frustum + * CULLED - aabb totally outside frustum + */ +template +GCLASS TFrustum::Classify(const AABB& aabb) const +{ + bool bIntersects = false; + + // find and test extreme points + for (int i=0; i= TYPE(0)) { + ptPlaneMin(0) = aabb.ptMin(0); + ptPlaneMax(0) = aabb.ptMax(0); + } else { + ptPlaneMin(0) = aabb.ptMax(0); + ptPlaneMax(0) = aabb.ptMin(0); + } + // y coordinate + if (plane.m_vN(1) >= TYPE(0)) { + ptPlaneMin(1) = aabb.ptMin(1); + ptPlaneMax(1) = aabb.ptMax(1); + } else { + ptPlaneMin(1) = aabb.ptMax(1); + ptPlaneMax(1) = aabb.ptMin(1); + } + // z coordinate + if (plane.m_vN(2) >= TYPE(0)) { + ptPlaneMin(2) = aabb.ptMin(2); + ptPlaneMax(2) = aabb.ptMax(2); + } else { + ptPlaneMin(2) = aabb.ptMax(2); + ptPlaneMax(2) = aabb.ptMin(2); + } + + if (plane.m_vN.dot(ptPlaneMin) > -plane.m_fD) + return CULLED; + + if (plane.m_vN.dot(ptPlaneMax) >= -plane.m_fD) + bIntersects = true; + } // for + + if (bIntersects) return CLIPPED; + return VISIBLE; +} +/*----------------------------------------------------------------*/ diff --git a/libs/Common/Queue.h b/libs/Common/Queue.h new file mode 100644 index 0000000..a11bcf7 --- /dev/null +++ b/libs/Common/Queue.h @@ -0,0 +1,296 @@ +//////////////////////////////////////////////////////////////////// +// Queue.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_QUEUE_H__ +#define __SEACAVE_QUEUE_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +/************************************************************************************** +// Queue template +**************************************************************************************/ +template +class cQueue +{ +public: + cQueue(int_t xSize = 16) : + first(-1), last(-1), vectorSize(0), vector(NULL) + { + if (xSize > 0) + Grow(xSize); + } + + ~cQueue() + { + Release(); + } + + void Grow(int_t gr) + { + ASSERT(gr > 0); + if (IsEmpty()) { + ASSERT(first == -1 && last == -1); + operator delete[] (vector); + vectorSize += gr; + vector = (TYPE*) operator new[] (vectorSize * sizeof(TYPE)); + return; + } + TYPE* tmp = (TYPE*) operator new[] ((vectorSize + gr) * sizeof(TYPE)); + if (first > last) { + memcpy(tmp, vector + first, (vectorSize - first) * sizeof(TYPE)); + memcpy(tmp + (vectorSize - first), vector, (last + 1) * sizeof(TYPE)); + last += vectorSize - first; + } else { + memcpy(tmp, vector + first, (last - first + 1) * sizeof(TYPE)); + last = last - first; + } + first = 0; + vectorSize += gr; + operator delete[] (vector); + vector = tmp; + } + + inline TYPE* GetData() + { + return vector; + } + + inline void PushHead() + { + _PushHead(); + if (useConstruct) + new(vector+(--first)) TYPE(); + else + --first; + } + inline void PushTail() + { + _PushTail(); + if (useConstruct) + new(vector+(++last)) TYPE(); + else + ++last; + } + + inline TYPE& AddEmptyHead() + { + _PushHead(); + if (useConstruct) + return *(new(vector+(--first)) TYPE()); + else + return vector[--first]; + } + inline TYPE& AddEmptyTail() + { + _PushTail(); + if (useConstruct) + return *(new(vector+(++last)) TYPE()); + else + return vector[++last]; + } + + inline void AddHead(ARG_TYPE elem) + { + _PushHead(); + if (useConstruct) + new(vector+(--first)) TYPE(elem); + else + vector[--first] = elem; + } + inline void AddTail(ARG_TYPE elem) + { + _PushTail(); + if (useConstruct) + new(vector+(++last)) TYPE(elem); + else + vector[++last] = elem; + } + + inline TYPE& GetHead() const + { + ASSERT(first >= 0 && last >= 0); + return vector[first]; + } + inline TYPE& GetTail() const + { + ASSERT(first >= 0 && last >= 0); + return vector[last]; + } + + inline TYPE& GetHead(uint_t delta) const + { + ASSERT(first >= 0 && last >= 0 && delta < GetSize()); + delta += first; + if ((int_t)delta < vectorSize) + return vector[delta]; + return vector[delta-vectorSize]; + } + inline TYPE& GetTail(uint_t delta) const + { + ASSERT(first >= 0 && last >= 0 && delta < GetSize()); + if ((int_t)delta <= last) + return vector[last-delta]; + return vector[vectorSize-(delta-last)]; + } + + inline void PopHead() + { + ASSERT(first >= 0 && last >= 0); + if (useConstruct) + (vector+first)->~TYPE(); + if (first > last) { + if (++first >= vectorSize) + first = 0; + } else { + if (++first > last) + first = last = -1; + } + } + inline void PopTail() + { + ASSERT(first >= 0 && last >= 0); + if (useConstruct) + (vector+last)->~TYPE(); + if (last >= first) { + if (--last < first) + first = last = -1; + } else { + if (--last < 0) + last = vectorSize-1; + } + } + + inline TYPE RemoveHead() + { + ASSERT(first >= 0 && last >= 0); + if (useConstruct) { + TYPE elem(vector[first]); + PopHead(); + return elem; + } else { + TYPE& elem(vector[first]); + PopHead(); + return elem; + } + } + inline TYPE RemoveTail() + { + ASSERT(first >= 0 && last >= 0); + if (useConstruct) { + TYPE elem(vector[last]); + PopTail(); + return elem; + } else { + TYPE& elem(vector[last]); + PopTail(); + return elem; + } + } + + inline uint_t GetSize() const + { + if (first < 0) + return 0; + if (first > last) // circular + return (vectorSize - (first - last) + 1); + return uint_t(last - first + 1); + } + + inline void Empty() + { + first = last = -1; + } + + // same as Empty(), plus free all allocated memory + inline void Release() + { + ASSERT((vector != NULL && vectorSize > 0) || (vector == NULL && vectorSize == 0)); + if (vector != NULL) { + if (useConstruct) { + if (first > last) { + for (; first < vectorSize; first++) + (vector+first)->~TYPE(); + for (; last >= 0; last--) + (vector+last)->~TYPE(); + } else if (first >= 0) { + for (; first <= last; first++) + (vector+first)->~TYPE(); + } + } + operator delete[] (vector); + vector = NULL; + vectorSize = 0; + } + Empty(); + } + + inline bool IsEmpty() const + { + return (first < 0); + } + + inline TYPE& operator[](uint_t index) const + { + return GetHead(index); + } + +protected: + inline void _PushHead() + { + if (first > last) { + if (last + 1 == first) { + Grow(xGrow); + first = vectorSize; + } + } else { + if (first <= 0) { + if (last + 1 >= vectorSize) + Grow(xGrow); + first = vectorSize; + } + if (last < 0) + last = first - 1; + } + } + inline void _PushTail() + { + if (last >= first) { + if (last + 1 >= vectorSize) { + if (first <= 0) + Grow(xGrow); + else + last = -1; + } + if (first < 0) + first = 0; + } else { + if (last + 1 == first) + Grow(xGrow); + } + } + +protected: + int_t first; + int_t last; + int_t vectorSize; + TYPE* vector; +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_QUEUE_H__ diff --git a/libs/Common/Random.h b/libs/Common/Random.h new file mode 100644 index 0000000..6be44ad --- /dev/null +++ b/libs/Common/Random.h @@ -0,0 +1,164 @@ +//////////////////////////////////////////////////////////////////// +// Random.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_RANDOM_H__ +#define __SEACAVE_RANDOM_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +// Stateless random number generation +// uniform random number generation +FORCEINLINE float random() { + return RANDOM(); +} +FORCEINLINE double randomd() { + return RANDOM(); +} +FORCEINLINE long double randomld() { + return RANDOM(); +} +STATIC_ASSERT(RAND_MAX < 2147483648); // integer randomRange assumes this is capped +template +FORCEINLINE T randomRange(T nMin, T nMax) { + ASSERT(nMin <= nMax && nMax-nMin+1 < 8589934596); // not to overflow a uint64_t + return nMin + T((uint64_t(nMax-nMin)*RAND()+RAND_MAX/2)/RAND_MAX); +} +template<> +FORCEINLINE float randomRange(float fMin, float fMax) { + return fMin + (fMax - fMin) * random(); +} +template<> +FORCEINLINE double randomRange(double fMin, double fMax) { + return fMin + (fMax - fMin) * randomd(); +} +template<> +FORCEINLINE long double randomRange(long double fMin, long double fMax) { + return fMin + (fMax - fMin) * randomld(); +} +template +FORCEINLINE T randomMeanRange(T mean, T delta/*=(max-min)/2*/) { + ASSERT(delta >= 0 && delta*2+1 < 8589934596); // not to overflow a uint64_t + return (mean + T((uint64_t(delta)*2*RAND()+RAND_MAX/2)/RAND_MAX)) - delta; +} +template<> +FORCEINLINE float randomMeanRange(float mean, float delta/*=(max-min)/2*/) { + return mean + delta * (2.f * random() - 1.f); +} +template<> +FORCEINLINE double randomMeanRange(double mean, double delta/*=(max-min)/2*/) { + return mean + delta * (2.0 * randomd() - 1.0); +} +template<> +FORCEINLINE long double randomMeanRange(long double mean, long double delta/*=(max-min)/2*/) { + return mean + delta * (2.0L * randomld() - 1.0L); +} +// gaussian random number generation +template +FORCEINLINE T gaussian(T val, T sigma) { + return EXP(-SQUARE(val/sigma)/2)/(SQRT(T(M_PI*2))*sigma); +} +template +FORCEINLINE T randomGaussian(T mean, T sigma) { + T x, y, r2; + do { + x = T(-1) + T(2) * RANDOM(); + y = T(-1) + T(2) * RANDOM(); + r2 = x * x + y * y; + } while (r2 > T(1) || r2 == T(0)); + return mean + sigma * y * SQRT(T(-2) * LOGN(r2) / r2); +} +template +FORCEINLINE T randomGaussian(T sigma) { + return randomGaussian(T(0), sigma); +} +FORCEINLINE float randomGaussian() { + return randomGaussian(0.f, 1.f); +} +FORCEINLINE double randomGaussiand() { + return randomGaussian(0.0, 1.0); +} +FORCEINLINE long double randomGaussianld() { + return randomGaussian(0.0L, 1.0L); +} +/*----------------------------------------------------------------*/ + + +// Encapsulates state for random number generation +// based on C++11 random number generator functionality +struct Random : std::mt19937 { + typedef std::mt19937 generator_type; + + Random() : generator_type(std::random_device()()) {} + Random(result_type seed) : generator_type(seed) {} + + // integer randomRange assumes this is capped + STATIC_ASSERT(max() < 4294967296); + + // returns a uniform random number in the range [0, 1] + template + FORCEINLINE typename std::enable_if::value, T>::type random() { + return (T)random()/(T)max(); + } + // returns a uniform random number in the range [0, max()] + template + FORCEINLINE typename std::enable_if::value, T>::type random() { + return (T)operator()(); + } + + // returns a uniform random number in the range [nMin, nMax] + template + FORCEINLINE typename std::enable_if::value, T>::type randomRange(T nMin, T nMax) { + return nMin + (nMax-nMin) * random(); + } + template + FORCEINLINE typename std::enable_if::value, T>::type randomRange(T nMin, T nMax) { + ASSERT(nMin <= nMax && nMax-nMin+1 < 4294967297); // not to overflow a uint64_t + return nMin + (T)(((uint64_t)(nMax-nMin) * random() + max()/2)/max()); + } + + // returns a uniform random number in the range [mean-delta, mean+delta] + template + FORCEINLINE typename std::enable_if::value, T>::type randomMeanRange(T mean, T delta/*=(max-min)/2*/) { + return mean + delta * (T(2) * random() - T(1)); + } + template + FORCEINLINE typename std::enable_if::value, T>::type randomMeanRange(T mean, T delta/*=(max-min)/2*/) { + ASSERT(delta >= 0 && delta*T(2)+1 < 4294967297); // not to overflow a uint64_t + return mean + (T)(((uint64_t)delta*2 * random() + max()/2)/max()) - delta; + } + + // returns a uniform random number in the range [nMin, nMax] using std implementation + template + FORCEINLINE typename std::enable_if::value, T>::type randomUniform(T nMin, T nMax) { + return std::uniform_real_distribution(nMin, nMax)(*this); + } + template + FORCEINLINE typename std::enable_if::value, T>::type randomUniform(T nMin, T nMax) { + return std::uniform_int_distribution(nMin, nMax)(*this); + } + + // returns a gaussian random number using std implementation + template + FORCEINLINE T randomGaussian(T mean, T stddev) { + return std::normal_distribution(mean, stddev)(*this); + } +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_RANDOM_H__ diff --git a/libs/Common/Ray.h b/libs/Common/Ray.h new file mode 100644 index 0000000..8549a2e --- /dev/null +++ b/libs/Common/Ray.h @@ -0,0 +1,226 @@ +//////////////////////////////////////////////////////////////////// +// Ray.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_RAY_H__ +#define __SEACAVE_RAY_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +// Basic triangle class +template +class TTriangle +{ + STATIC_ASSERT(DIMS > 1 && DIMS <= 3); + +public: + typedef Eigen::Matrix VECTOR; + typedef Eigen::Matrix POINT; + typedef SEACAVE::TAABB AABB; + typedef SEACAVE::TPlane PLANE; + enum { numScalar = (3*DIMS) }; + + POINT a, b, c; // triangle vertices + + //--------------------------------------- + + inline TTriangle() {} + inline TTriangle(const POINT&, const POINT&, const POINT&); + + inline void Set(const POINT&, const POINT&, const POINT&); + + inline POINT GetCenter() const; + inline AABB GetAABB() const; + inline PLANE GetPlane() const; + + inline const POINT& operator [] (BYTE i) const { ASSERT(i<3); return (&a)[i]; } + inline POINT& operator [] (BYTE i) { ASSERT(i<3); return (&a)[i]; } +}; // class TTriangle +/*----------------------------------------------------------------*/ + + +// Basic ray class +template +class TRay +{ + STATIC_ASSERT(DIMS > 1 && DIMS <= 3); + +public: + typedef Eigen::Matrix MATRIX; + typedef Eigen::Matrix VECTOR; + typedef Eigen::Matrix POINT; + typedef SEACAVE::TTriangle TRIANGLE; + typedef SEACAVE::TSphere SPHERE; + typedef SEACAVE::TAABB AABB; + typedef SEACAVE::TOBB OBB; + typedef SEACAVE::TPlane PLANE; + enum { numScalar = (2*DIMS) }; + + VECTOR m_vDir; // ray direction (normalized) + POINT m_pOrig; // ray origin + + //--------------------------------------- + + inline TRay() {} + inline TRay(const POINT& pOrig, const VECTOR& vDir); + inline TRay(const POINT& pt0, const POINT& pt1, bool bPoints); + + inline void Set(const POINT& pOrig, const VECTOR& vDir); + inline void SetFromPoints(const POINT& pt0, const POINT& pt1); + inline TYPE SetFromPointsLen(const POINT& pt0, const POINT& pt1); + inline void DeTransform(const MATRIX&); // move to matrix space + + template + bool Intersects(const TRIANGLE&, TYPE *t) const; + template + bool Intersects(const TRIANGLE&, TYPE fL, TYPE *t) const; + bool Intersects(const PLANE& plane, bool bCull, + TYPE *t, POINT* pPtHit) const; + inline TYPE IntersectsDist(const PLANE& plane) const; + inline POINT Intersects(const PLANE& plane) const; + bool Intersects(const PLANE& plane, bool bCull, + TYPE fL, TYPE *t, POINT* pPtHit) const; + bool Intersects(const SPHERE& sphere) const; + bool Intersects(const SPHERE& sphere, TYPE& t) const; + bool Intersects(const AABB& aabb) const; + bool Intersects(const AABB& aabb, TYPE& t) const; + bool Intersects(const AABB& aabb, TYPE fL, TYPE *t) const; + bool Intersects(const OBB& obb) const; + bool Intersects(const OBB& obb, TYPE &t) const; + bool Intersects(const OBB& obb, TYPE fL, TYPE *t) const; + bool Intersects(const TRay& ray, TYPE& s) const; + bool Intersects(const TRay& ray, POINT& p) const; + bool Intersects(const TRay& ray, TYPE& s1, TYPE& s2) const; + bool IntersectsAprox(const TRay& ray, POINT& p) const; + bool IntersectsAprox2(const TRay& ray, POINT& p) const; + + inline TYPE CosAngle(const TRay&) const; + inline bool Coplanar(const TRay&) const; + inline bool Parallel(const TRay&) const; + + inline TYPE Classify(const POINT&) const; + inline POINT ProjectPoint(const POINT&) const; + bool DistanceSq(const POINT&, TYPE&) const; + bool Distance(const POINT&, TYPE&) const; + TYPE DistanceSq(const POINT&) const; + TYPE Distance(const POINT&) const; + POINT GetPoint(TYPE) const; + + TRay operator * (const MATRIX&) const; // matrix multiplication + inline TRay& operator *= (const MATRIX&); // matrix multiplication + + inline TYPE& operator [] (BYTE i) { ASSERT(i<6); return m_vDir.data()[i]; } + inline TYPE operator [] (BYTE i) const { ASSERT(i<6); return m_vDir.data()[i]; } +}; // class TRay +/*----------------------------------------------------------------*/ + + +// Basic cylinder class +template +class TCylinder +{ + STATIC_ASSERT(DIMS > 1 && DIMS <= 3); + +public: + typedef Eigen::Matrix VECTOR; + typedef Eigen::Matrix POINT; + typedef SEACAVE::TRay RAY; + typedef SEACAVE::TSphere SPHERE; + enum { numScalar = (2*DIMS+2) }; + + RAY ray; // central ray defining starting the point and direction + TYPE radius; // cylinder radius + TYPE minHeight; // cylinder heights satisfying: + TYPE maxHeight; // std::numeric_limits::lowest() <= minHeight <= 0 < maxHeight <= std::numeric_limits::max() + + //--------------------------------------- + + inline TCylinder() {} + + inline TCylinder(const RAY& _ray, TYPE _radius, TYPE _minHeight=TYPE(0), TYPE _maxHeight=std::numeric_limits::max()) + : ray(_ray), radius(_radius), minHeight(_minHeight), maxHeight(_maxHeight) { ASSERT(TYPE(0) >= minHeight && minHeight < maxHeight); } + + inline bool IsFinite() const { return maxHeight < std::numeric_limits::max() && minHeight > std::numeric_limits::lowest(); } + inline TYPE GetLength() const { return maxHeight - minHeight; } + + bool Intersects(const SPHERE&) const; + + inline GCLASS Classify(const POINT&, TYPE& t) const; +}; // class TCylinder +/*----------------------------------------------------------------*/ + + +// Basic cone class +template +class TCone +{ + STATIC_ASSERT(DIMS > 1 && DIMS <= 3); + +public: + typedef Eigen::Matrix VECTOR; + typedef Eigen::Matrix POINT; + typedef SEACAVE::TRay RAY; + typedef SEACAVE::TSphere SPHERE; + enum { numScalar = (2*DIMS+3) }; + + RAY ray; // ray origin is the cone vertex and ray direction is the cone axis direction + TYPE angle; // cone angle, in radians, must be inside [0, pi/2] + TYPE minHeight; // heights satisfying: + TYPE maxHeight; // 0 <= minHeight < maxHeight <= std::numeric_limits::max() + + //--------------------------------------- + + inline TCone() {} + inline TCone(const RAY& _ray, TYPE _angle, TYPE _minHeight=TYPE(0), TYPE _maxHeight=std::numeric_limits::max()) + : ray(_ray), angle(_angle), minHeight(_minHeight), maxHeight(_maxHeight) { ASSERT(TYPE(0) <= minHeight && minHeight < maxHeight); } +}; // class TCone + +// Structure used to compute the intersection between a cone and the given sphere/point +template +class TConeIntersect +{ + STATIC_ASSERT(DIMS > 1 && DIMS <= 3); + +public: + typedef Eigen::Matrix VECTOR; + typedef Eigen::Matrix POINT; + typedef SEACAVE::TCone CONE; + typedef SEACAVE::TSphere SPHERE; + + const CONE& cone; + + // cache angle derivatives, to avoid calling trigonometric functions in geometric queries + const TYPE cosAngle, sinAngle, sinAngleSq, cosAngleSq, invSinAngle; + + //--------------------------------------- + + inline TConeIntersect(const CONE& _cone) + : cone(_cone), cosAngle(COS(cone.angle)), sinAngle(SIN(cone.angle)), + sinAngleSq(SQUARE(sinAngle)), cosAngleSq(SQUARE(cosAngle)), + invSinAngle(TYPE(1)/sinAngle) {} + + bool operator()(const SPHERE&) const; + + GCLASS Classify(const POINT&, TYPE& t) const; +}; // class TConeIntersect +/*----------------------------------------------------------------*/ + + +#include "Ray.inl" +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_RAY_H__ diff --git a/libs/Common/Ray.inl b/libs/Common/Ray.inl new file mode 100644 index 0000000..34d4f45 --- /dev/null +++ b/libs/Common/Ray.inl @@ -0,0 +1,951 @@ +//////////////////////////////////////////////////////////////////// +// Ray.inl +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +template +inline TTriangle::TTriangle(const POINT& p0, const POINT& p1, const POINT& p2) + : + a(p0), b(p1), c(p2) +{ +} // constructors +/*----------------------------------------------------------------*/ + +// set attributes +template +inline void TTriangle::Set(const POINT& p0, const POINT& p1, const POINT& p2) +{ + a = p0; + b = p1; + c = p2; +} +/*----------------------------------------------------------------*/ + + +// get center +template +inline typename TTriangle::POINT TTriangle::GetCenter() const +{ + return (a + b + c) / TYPE(3); +} +// get AABB +template +inline typename TTriangle::AABB TTriangle::GetAABB() const +{ + return AABB(& operator [] (0), 3); +} +// get plane +template +inline typename TTriangle::PLANE TTriangle::GetPlane() const +{ + return PLANE(a, b, c); +} +/*----------------------------------------------------------------*/ + + +// S T R U C T S /////////////////////////////////////////////////// + +template +inline TRay::TRay(const POINT& pOrig, const VECTOR& vDir) + : + m_vDir(vDir), m_pOrig(pOrig) +{ + ASSERT(ISEQUAL(m_vDir.squaredNorm(), TYPE(1))); +} // constructors +template +inline TRay::TRay(const POINT& pt0, const POINT& pt1, bool /*bPoints*/) + : + m_vDir((pt1-pt0).normalized()), m_pOrig(pt0) +{ +} // constructors +/*----------------------------------------------------------------*/ + +// set attributes +template +inline void TRay::Set(const POINT& pOrig, const VECTOR& vDir) +{ + m_vDir = vDir; + m_pOrig = pOrig; + ASSERT(ISEQUAL(m_vDir.squaredNorm(), TYPE(1))); +} +template +inline void TRay::SetFromPoints(const POINT& pt0, const POINT& pt1) +{ + m_vDir = (pt1-pt0).normalized(); + m_pOrig = pt0; +} +template +inline TYPE TRay::SetFromPointsLen(const POINT& pt0, const POINT& pt1) +{ + m_vDir = pt1-pt0; + const TYPE len = m_vDir.norm(); + if (len > TYPE(0)) + m_vDir *= TYPE(1)/len; + m_pOrig = pt0; + return len; +} +/*----------------------------------------------------------------*/ + + +// transform ray into matrix space +template +inline void TRay::DeTransform(const MATRIX& _m) +{ + STATIC_ASSERT(DIMS == 3); + MATRIX m(_m); + + // invert translation + typedef Eigen::Matrix VECTOR4; + const VECTOR4 vcOrig(m_pOrig[0]-m(3,0), m_pOrig[1]-m(3,1), m_pOrig[2]-m(3,2)); + + // delete it from matrix + m(3,0) = m(3,1) = m(3,2) = TYPE(0); + + // invert matrix and apply to ray + const MATRIX mInv = m.inverse(); + m_pOrig = vcOrig * mInv; + m_vDir = VECTOR4(m_vDir) * mInv; +} +/*----------------------------------------------------------------*/ + + +// Transform the ray by a matrix. +template +TRay TRay::operator*(const MATRIX& m) const +{ + TRay ray; + ray.SetFromPoints(m_pOrig*m, (m_pOrig+m_vDir)*m); + return ray; +} // operator * +template +inline TRay& TRay::operator*=(const MATRIX& m) +{ + *this = operator * (m); + return *this; +} // operator *= +/*----------------------------------------------------------------*/ + + +// test for intersection with triangle +template +template +bool TRay::Intersects(const TRIANGLE& tri, TYPE *pt) const +{ + const VECTOR edge1(tri.b - tri.a); + const VECTOR edge2(tri.c - tri.a); + const VECTOR pvec(m_vDir.cross(edge2)); + const TYPE det(edge1.dot(pvec)); + // check if ray and triangle plane are parallel + if ((bCull ? det : ABS(det)) < ZEROTOLERANCE() * TYPE(0.01)) + return false; + const TYPE invDet(TYPE(1) / det); + const VECTOR tvec(m_pOrig - tri.a); + const TYPE u(tvec.dot(pvec) * invDet); + // check if intersection is outside of the line segment defined by points a and c + if (u < -ZEROTOLERANCE() * TYPE(10)) + return false; + const VECTOR qvec(tvec.cross(edge1)); + const TYPE v(m_vDir.dot(qvec) * invDet); + // check if intersection is outside of the line segment defined by points a and b + if (v < -ZEROTOLERANCE() * TYPE(10)) + return false; + // check if intersection is outside of the line segment defined by points b and c + if (u + v > TYPE(1) + ZEROTOLERANCE() * TYPE(10)) + return false; + const TYPE t(edge2.dot(qvec) * invDet); + // check if intersection is behind the ray origin + if (bCull && t < -ZEROTOLERANCE()) + return false; + if (pt) + *pt = t; + return true; +} // Intersects(Tri) +/*----------------------------------------------------------------*/ + +// test for intersection with triangle at certain length (line segment); +// same as above, but test distance to intersection vs segment length +template +template +bool TRay::Intersects(const TRIANGLE& tri, TYPE fL, TYPE *t) const +{ + TYPE _t; + TYPE* const pt(t ? t : &_t); + Intersects(tri, pt); + // collision but not on segment? + return *pt <= fL; +} // Intersects(Tri at length) +/*----------------------------------------------------------------*/ + + +// test if the ray intersects the given sphere +template +bool TRay::Intersects(const SPHERE& sphere) const +{ + TYPE dSq; + if (!DistanceSq(sphere.center, dSq)) + return false; + return dSq <= SQUARE(sphere.radius); +} +// same as above, but returns also the distance on the ray corresponding to the closest point +template +bool TRay::Intersects(const SPHERE& sphere, TYPE& t) const +{ + const VECTOR a(sphere.center - m_pOrig); + t = a.dot(m_vDir); + // point behind the ray origin + if (t < TYPE(0)) + return false; + const TYPE dSq((a - m_vDir*t).squaredNorm()); + return dSq <= SQUARE(sphere.radius); +} // Intersects(Sphere) +/*----------------------------------------------------------------*/ + + +template +bool TRay::Intersects(const AABB &aabb) const +{ + bool to_infinity = true; + TYPE _min, _max; + // first on x value + if (m_vDir[0] == TYPE(0)) { + if (m_pOrig[0] < aabb.ptMin[0] || m_pOrig[0] > aabb.ptMax[0]) + return false; // NO_INTERSECTION + } else { + if (m_vDir[0] > TYPE(0)) { + _min = (aabb.ptMin[0]-m_pOrig[0])/m_vDir[0]; + _max = (aabb.ptMax[0]-m_pOrig[0])/m_vDir[0]; + } else { + _min = (aabb.ptMax[0]-m_pOrig[0])/m_vDir[0]; + _max = (aabb.ptMin[0]-m_pOrig[0])/m_vDir[0]; + } + to_infinity = false; + } + // now on y value + if (m_vDir[1] == TYPE(0)) { + if (m_pOrig[1] < aabb.ptMin[1] || m_pOrig[1] > aabb.ptMax[1]) + return false; // NO_INTERSECTION + } else { + TYPE newmin, newmax; + if (m_vDir[1] > TYPE(0)) { + newmin = (aabb.ptMin[1]-m_pOrig[1])/m_vDir[1]; + newmax = (aabb.ptMax[1]-m_pOrig[1])/m_vDir[1]; + } else { + newmin = (aabb.ptMax[1]-m_pOrig[1])/m_vDir[1]; + newmax = (aabb.ptMin[1]-m_pOrig[1])/m_vDir[1]; + } + #if 0 + if (to_infinity) { + _min = newmin; + _max = newmax; + } else { + #else + if (to_infinity) + return true; + { + #endif + if (newmin > _min) + _min = newmin; + if (newmax < _max) + _max = newmax; + if (_max < _min) + return false; // NO_INTERSECTION + } + to_infinity = false; + } + // now on z value + if (DIMS == 3) { + if (m_vDir[2] == TYPE(0)) { + if (m_pOrig[2] < aabb.ptMin[2] || m_pOrig[2] > aabb.ptMax[2]) + return false; // NO_INTERSECTION + } else { + TYPE newmin, newmax; + if (m_vDir[2] > TYPE(0)) { + newmin = (aabb.ptMin[2]-m_pOrig[2])/m_vDir[2]; + newmax = (aabb.ptMax[2]-m_pOrig[2])/m_vDir[2]; + } else { + newmin = (aabb.ptMax[2]-m_pOrig[2])/m_vDir[2]; + newmax = (aabb.ptMin[2]-m_pOrig[2])/m_vDir[2]; + } + #if 0 + if (to_infinity) { + _min = newmin; + _max = newmax; + } else { + #else + if (to_infinity) + return true; + { + #endif + if (newmin > _min) + _min = newmin; + if (newmax < _max) + _max = newmax; + if (_max < _min) + return false; // NO_INTERSECTION + } + to_infinity = false; + } + } + ASSERT(!to_infinity); + #if 0 + if (_max < _min) + return true; // POINT_INTERSECTION + #endif + return true; // SEGMENT_INTERSECTION +} // Intersects(AABB) + +// test for intersection with aabb, original code by Andrew Woo, +// from "Geometric Tools...", Morgan Kaufmann Publ., 2002 +template +bool TRay::Intersects(const AABB &aabb, TYPE& t) const +{ + TYPE t0, t1, tmp; + TYPE tNear(-999999); + TYPE tFar ( 999999); + + // first pair of planes + if (ISZERO(m_vDir[0])) { + if ((m_pOrig[0] < aabb.ptMin[0]) || + (m_pOrig[0] > aabb.ptMax[0])) + return false; + } + t0 = (aabb.ptMin[0] - m_pOrig[0]) / m_vDir[0]; + t1 = (aabb.ptMax[0] - m_pOrig[0]) / m_vDir[0]; + if (t0 > t1) { tmp=t0; t0=t1; t1=tmp; } + if (t0 > tNear) tNear = t0; + if (t1 < tFar) tFar = t1; + if (tNear > tFar) return false; + if (tFar < TYPE(0)) return false; + + // second pair of planes + if (ISZERO(m_vDir[1])) { + if ((m_pOrig[1] < aabb.ptMin[1]) || + (m_pOrig[1] > aabb.ptMax[1]) ) + return false; + } + t0 = (aabb.ptMin[1] - m_pOrig[1]) / m_vDir[1]; + t1 = (aabb.ptMax[1] - m_pOrig[1]) / m_vDir[1]; + if (t0 > t1) { tmp=t0; t0=t1; t1=tmp; } + if (t0 > tNear) tNear = t0; + if (t1 < tFar) tFar = t1; + if (tNear > tFar) return false; + if (tFar < TYPE(0)) return false; + + if (DIMS == 3) { + // third pair of planes + if (ISZERO(m_vDir[2])) { + if ((m_pOrig[2] < aabb.ptMin[2]) || + (m_pOrig[2] > aabb.ptMax[2]) ) + return false; + } + t0 = (aabb.ptMin[2] - m_pOrig[2]) / m_vDir[2]; + t1 = (aabb.ptMax[2] - m_pOrig[2]) / m_vDir[2]; + if (t0 > t1) { tmp=t0; t0=t1; t1=tmp; } + if (t0 > tNear) tNear = t0; + if (t1 < tFar) tFar = t1; + if (tNear > tFar) return false; + if (tFar < TYPE(0)) return false; + } + + t = (tNear > TYPE(0) ? tNear : tFar); + return true; +} // Intersects(AABB) + +// test for intersection with aabb, original code by Andrew Woo, +// from "Geometric Tools...", Morgan Kaufmann Publ., 2002 +template +bool TRay::Intersects(const AABB &aabb, TYPE fL, TYPE *t) const +{ + TYPE t0, t1, tmp, tFinal; + TYPE tNear(-999999); + TYPE tFar ( 999999); + + // first pair of planes + if (ISZERO(m_vDir[0])) { + if ((m_pOrig[0] < aabb.ptMin[0]) || + (m_pOrig[0] > aabb.ptMax[0]) ) + return false; + } + t0 = (aabb.ptMin[0] - m_pOrig[0]) / m_vDir[0]; + t1 = (aabb.ptMax[0] - m_pOrig[0]) / m_vDir[0]; + if (t0 > t1) { tmp=t0; t0=t1; t1=tmp; } + if (t0 > tNear) tNear = t0; + if (t1 < tFar) tFar = t1; + if (tNear > tFar) return false; + if (tFar < TYPE(0)) return false; + + // second pair of planes + if (ISZERO(m_vDir[1])) { + if ((m_pOrig[1] < aabb.ptMin[1]) || + (m_pOrig[1] > aabb.ptMax[1]) ) + return false; + } + t0 = (aabb.ptMin[1] - m_pOrig[1]) / m_vDir[1]; + t1 = (aabb.ptMax[1] - m_pOrig[1]) / m_vDir[1]; + if (t0 > t1) { tmp=t0; t0=t1; t1=tmp; } + if (t0 > tNear) tNear = t0; + if (t1 < tFar) tFar = t1; + if (tNear > tFar) return false; + if (tFar < TYPE(0)) return false; + + if (DIMS == 3) { + // third pair of planes + if (ISZERO(m_vDir[2])) { + if ((m_pOrig[2] < aabb.ptMin[2]) || + (m_pOrig[2] > aabb.ptMax[2]) ) + return false; + } + t0 = (aabb.ptMin[2] - m_pOrig[2]) / m_vDir[2]; + t1 = (aabb.ptMax[2] - m_pOrig[2]) / m_vDir[2]; + if (t0 > t1) { tmp=t0; t0=t1; t1=tmp; } + if (t0 > tNear) tNear = t0; + if (t1 < tFar) tFar = t1; + if (tNear > tFar) return false; + if (tFar < 0) return false; + } + + tFinal = (tNear > TYPE(0) ? tNear : tFar); + + if (tFinal > fL) return false; + if (t) *t = tFinal; + return true; +} // Intersects(AABB) at length +/*----------------------------------------------------------------*/ + + +template +bool TRay::Intersects(const OBB& obb) const +{ + TYPE t; + return Intersects(obb, t); +} // Intersects(OBB) + +// test for intersection with obb, slaps method +template +bool TRay::Intersects(const OBB& obb, TYPE& t) const +{ + TYPE e, f, t1, t2, temp; + TYPE tmin(-999999); + TYPE tmax( 999999); + + const POINT vP = obb.m_pos - m_pOrig; + + // 1st slap + e = obb.m_rot.row(0) * vP; + f = obb.m_rot.row(0) * m_vDir; + if (!ISZERO(f)) { + t1 = (e + obb.m_ext[0]) / f; + t2 = (e - obb.m_ext[0]) / f; + + if (t1 > t2) { temp=t1; t1=t2; t2=temp; } + if (t1 > tmin) tmin = t1; + if (t2 < tmax) tmax = t2; + if (tmin > tmax) return false; + if (tmax < 0.0f) return false; + } else + if (((-e - obb.m_ext[0]) > 0.0f) || ((-e + obb.m_ext[0]) < 0.0f)) + return false; + + // 2nd slap + e = obb.m_rot.row(1) * vP; + f = obb.m_rot.row(1) * m_vDir; + if (!ISZERO(f)) { + t1 = (e + obb.m_ext[1]) / f; + t2 = (e - obb.m_ext[1]) / f; + + if (t1 > t2) { temp=t1; t1=t2; t2=temp; } + if (t1 > tmin) tmin = t1; + if (t2 < tmax) tmax = t2; + if (tmin > tmax) return false; + if (tmax < 0.0f) return false; + } else + if (((-e - obb.m_ext[1]) > 0.0f) || ((-e + obb.m_ext[1]) < 0.0f)) + return false; + + // 3rd slap + e = obb.m_rot.row(2) * vP; + f = obb.m_rot.row(2) * m_vDir; + if (!ISZERO(f)) { + t1 = (e + obb.m_ext[2]) / f; + t2 = (e - obb.m_ext[2]) / f; + + if (t1 > t2) { temp=t1; t1=t2; t2=temp; } + if (t1 > tmin) tmin = t1; + if (t2 < tmax) tmax = t2; + if (tmin > tmax) return false; + if (tmax < 0.0f) return false; + } else + if (((-e - obb.m_ext[2]) > 0) || ((-e + obb.m_ext[2]) < 0)) + return false; + + if (tmin > 0) { + if (t) *t = tmin; + return true; + } + + t = tmax; + return true; +} // Intersects(AABB) + +// test for intersection with obb at certain length (line segment), +// slaps method but compare result if true to length prior return. +template +bool TRay::Intersects(const OBB& obb, TYPE fL, TYPE *t) const +{ + TYPE e, f, t1, t2, temp; + TYPE tmin(-999999); + TYPE tmax( 999999); + + const POINT vP = obb.m_pos - m_pOrig; + + // 1st slap + e = obb.m_rot.row(0) * vP; + f = obb.m_rot.row(0) * m_vDir; + if (!ISZERO(f)) { + t1 = (e + obb.m_ext[0]) / f; + t2 = (e - obb.m_ext[0]) / f; + + if (t1 > t2) { temp=t1; t1=t2; t2=temp; } + if (t1 > tmin) tmin = t1; + if (t2 < tmax) tmax = t2; + if (tmin > tmax) return false; + if (tmax < 0.0f) return false; + } else + if (((-e - obb.m_ext[0]) > 0) || ((-e + obb.m_ext[0]) < 0)) + return false; + + // 2nd slap + e = obb.m_rot.row(1) * vP; + f = obb.m_rot.row(1) * m_vDir; + if (!ISZERO(f)) { + t1 = (e + obb.m_ext[1]) / f; + t2 = (e - obb.m_ext[1]) / f; + + if (t1 > t2) { temp=t1; t1=t2; t2=temp; } + if (t1 > tmin) tmin = t1; + if (t2 < tmax) tmax = t2; + if (tmin > tmax) return false; + if (tmax < 0.0f) return false; + } else + if (((-e - obb.m_ext[1]) > 0) || ((-e + obb.m_ext[1]) < 0)) + return false; + + // 3rd slap + e = obb.m_rot.row(2) * vP; + f = obb.m_rot.row(2) * m_vDir; + if (!ISZERO(f)) { + t1 = (e + obb.m_ext[2]) / f; + t2 = (e - obb.m_ext[2]) / f; + + if (t1 > t2) { temp=t1; t1=t2; t2=temp; } + if (t1 > tmin) tmin = t1; + if (t2 < tmax) tmax = t2; + if (tmin > tmax) return false; + if (tmax < 0.0f) return false; + } else + if (((-e - obb.m_ext[2]) > 0) || ((-e + obb.m_ext[2]) < 0)) + return false; + + if ((tmin > 0) && (tmin <= fL)) { + if (t) *t = tmin; + return true; + } + + // intersection on line but not on segment + if (tmax > fL) return false; + + if (t) *t = tmax; + + return true; +} // Intersects(AABB) at length +/*----------------------------------------------------------------*/ + + +// Intersection with PLANE from origin till infinity. +template +bool TRay::Intersects(const PLANE& plane, bool bCull, TYPE *t, POINT* pPtHit) const +{ + const TYPE Vd(plane.m_vN.dot(m_vDir)); + + // ray parallel to plane + if (ISZERO(Vd)) + return false; + + // normal pointing away from ray dir + // => intersection backface if any + if (bCull && (Vd > TYPE(0))) + return false; + + const TYPE Vo(-plane.Distance(m_pOrig)); + + const TYPE _t(Vo / Vd); + + // intersection behind ray origin + if (_t < TYPE(0)) + return false; + + if (pPtHit) + (*pPtHit) = m_pOrig + (m_vDir * _t); + + if (t) + (*t) = _t; + + return true; +} // Intersects(PLANE) +// same as above, but no checks +template +inline TYPE TRay::IntersectsDist(const PLANE& plane) const +{ + const TYPE Vd(plane.m_vN.dot(m_vDir)); + const TYPE Vo(-plane.Distance(m_pOrig)); + return SAFEDIVIDE(Vo, Vd); +} // IntersectsDist(PLANE) +template +inline typename TRay::POINT TRay::Intersects(const PLANE& plane) const +{ + return m_pOrig + (m_vDir * IntersectsDist(plane)); +} // Intersects(PLANE) +/*----------------------------------------------------------------*/ + + +// Intersection with PLANE at distance fL. +template +bool TRay::Intersects(const PLANE& plane, bool bCull, TYPE fL, TYPE *t, POINT* pPtHit) const +{ + const TYPE Vd = plane.m_vN.dot(m_vDir); + + // ray parallel to plane + if (ISZERO(Vd)) + return false; + + // normal pointing away from ray dir + // => intersection backface if any + if (bCull && (Vd > TYPE(0))) + return false; + + const TYPE Vo(-plane.Distance(m_pOrig)); + + const TYPE _t(Vo / Vd); + + // intersection behind ray origin or beyond valid range + if ((_t < TYPE(0)) || (_t > fL)) + return false; + + if (pPtHit) + (*pPtHit) = m_pOrig + (m_vDir * _t); + + if (t) + (*t) = _t; + + return true; +} // Intersects(PLANE) +/*----------------------------------------------------------------*/ + + +// Intersection with another ray. +// Rays assumed to be coplanar; returns only the distance from the origin of the first ray. +// P = R.origin + s * R.dir +// http://mathworld.wolfram.com/Line-LineIntersection.html +// (works also for 2D lines if z components is set to 0) +template +bool TRay::Intersects(const TRay& ray, TYPE& s) const +{ + const POINT dir(ray.m_pOrig - m_pOrig); + const POINT n(m_vDir.cross(ray.m_vDir)); + if (!ISZERO(dir.dot(n))) + return false; // lines are not coplanar + s = dir.cross(ray.m_vDir).dot(n) / n.squaredNorm(); + return true; +} // Intersects(Ray) +// Same as above, but returns the actual intersection point: +template +bool TRay::Intersects(const TRay& ray, POINT& p) const +{ + TYPE s; + if (!Intersects(ray, s)) + return false; // lines are not coplanar + if (s < TYPE(0)) + return false; // intersection behind ray origin + p = m_pOrig + m_vDir * s; + return true; +} // Intersects(Ray) +// No coplanarity assumption; returns the shortest line segment connecting the two rays: +// P1 = R1.origin + s1 * R1.dir +// P2 = R2.origin + s2 * R2.dir +// http://paulbourke.net/geometry/pointlineplane/ +template +bool TRay::Intersects(const TRay& ray, TYPE& s1, TYPE& s2) const +{ + const TYPE d4321(ray.m_vDir.dot(m_vDir)); + const TYPE d4343(ray.m_vDir.dot(ray.m_vDir)); + const TYPE d2121(m_vDir.dot(m_vDir)); + const TYPE denom = d2121 * d4343 - d4321 * d4321; + if (ISZERO(denom)) + return false; // lines are parallel + const POINT dir(m_pOrig - ray.m_pOrig); + const TYPE d1321(dir.dot(m_vDir)); + const TYPE d1343(dir.dot(ray.m_vDir)); + const TYPE numer = d1343 * d4321 - d1321 * d4343; + s1 = numer / denom; + s2 = (d1343 + d4321 * s1) / d4343; + return true; +} // Intersects(Ray) +// Same as above, but returns only middle point of the shortest line segment: +// P = (P1 + P2) / 2 +template +bool TRay::IntersectsAprox(const TRay& ray, POINT& p) const +{ + TYPE s1, s2; + if (!Intersects(ray, s1, s2)) + return false; + const POINT P1 = m_pOrig + m_vDir * s1; + const POINT P2 = ray.m_pOrig + ray.m_vDir * s2; + p = (P1+P2)*TYPE(0.5); + return true; +} // Intersects(Ray) +// Same as above, but returns the point on the given ray. +// Returns false if intersection not in front of main ray. +template +bool TRay::IntersectsAprox2(const TRay& ray, POINT& p) const +{ + TYPE s1, s2; + if (!Intersects(ray, s1, s2)) + return false; + if (s1 < TYPE(0)) + return false; // intersection behind main ray origin + p = ray.m_pOrig + ray.m_vDir * s2; + return true; +} // Intersects(Ray) +/*----------------------------------------------------------------*/ + + +// computes the angle between the two lines +template +inline TYPE TRay::CosAngle(const TRay& ray) const +{ + ASSERT(ISEQUAL(m_vDir.norm(), TYPE(1))); + ASSERT(ISEQUAL(ray.m_vDir.norm(), TYPE(1))); + return m_vDir.dot(ray.m_vDir); +} +// tests if the two lines are coplanar (intersect) +template +inline bool TRay::Coplanar(const TRay& ray) const +{ + const POINT dir(ray.m_pOrig - m_pOrig); + const POINT n(m_vDir.cross(ray.m_vDir)); + return ISZERO(dir.dot(n)); +} +// tests if the two lines are parallel +template +inline bool TRay::Parallel(const TRay& ray) const +{ + return ISZERO(m_vDir.cross(ray.m_vDir).norm()); +} +/*----------------------------------------------------------------*/ + + +// Computes the position on the ray of the point projection. +// Returns 0 if it coincides with the ray origin, positive value if in front, and negative if behind. +template +inline TYPE TRay::Classify(const POINT& pt) const +{ + ASSERT(ISEQUAL(m_vDir.norm(), TYPE(1))); + const VECTOR a(pt - m_pOrig); + return a.dot(m_vDir); +} // Classify(POINT) +template +inline typename TRay::POINT TRay::ProjectPoint(const POINT& pt) const +{ + ASSERT(ISEQUAL(m_vDir.norm(), TYPE(1))); + return m_pOrig + m_vDir*Classify(pt); +} // ProjectPoint +/*----------------------------------------------------------------*/ + +// Computes the distance between the ray and a point. +// Returns false if the point is projecting behind the ray origin. +template +bool TRay::DistanceSq(const POINT& pt, TYPE& d) const +{ + const VECTOR a(pt - m_pOrig); + const TYPE LenACos(a.dot(m_vDir)); + // point behind the ray origin + if (LenACos < TYPE(0)) + return false; + d = (a - m_vDir*LenACos).squaredNorm(); + return true; +} // DistanceSq(POINT) +template +bool TRay::Distance(const POINT& pt, TYPE& d) const +{ + const VECTOR a(pt - m_pOrig); + const TYPE LenACos(a.dot(m_vDir)); + // point behind the ray origin + if (LenACos < TYPE(0)) + return false; + d = (a - m_vDir*LenACos).norm(); + return true; +} // Distance(POINT) +// Same as above, but returns the distance even if the point projection is behind the origin. +template +TYPE TRay::DistanceSq(const POINT& pt) const +{ + ASSERT(ISEQUAL(m_vDir.norm(), TYPE(1))); + const VECTOR a(pt - m_pOrig); + const VECTOR b(a - m_vDir); + return b.cross(a).squaredNorm(); +} // DistanceSq(POINT) +template +TYPE TRay::Distance(const POINT& pt) const +{ + return SQRT(DistanceSq(pt)); +} // Distance(POINT) +// Get the point on the ray at distance t from origin +template +typename TRay::POINT TRay::GetPoint(TYPE t) const +{ + return m_pOrig + m_vDir * t; +} // GetPoint +/*----------------------------------------------------------------*/ + + +// S T R U C T S /////////////////////////////////////////////////// + +template +bool TCylinder::Intersects(const SPHERE& sphere) const +{ + struct Check { + static bool Intersection(const RAY& ray, const SPHERE& sphere, const VECTOR& CmV, TYPE t, TYPE radius, TYPE height) { + const POINT O(ray.m_pOrig + ray.m_vDir * height); + const VECTOR a(sphere.center - O); + if (a.squaredNorm() <= SQUARE(sphere.radius)) + return true; // ray origin inside the sphere + const POINT D((CmV - ray.m_vDir * t).normalized()); + const TYPE d = a.dot(D); + if (d < TYPE(0)) + return false; // intersection behind the ray origin + const TYPE dSq((a - D*d).squaredNorm()); + const TYPE srSq = SQUARE(sphere.radius); + if (dSq <= srSq) { + const TYPE r = d - SQRT(srSq-dSq); + ASSERT(r >= TYPE(0)); + return r <= radius; // intersection before the ray end + } + return false; + } + }; + const VECTOR CmV(sphere.center - ray.m_pOrig); + const TYPE t(ray.m_vDir.dot(CmV)); + // sphere projects behind the cylinder origin + if (t < minHeight) { + if (t+sphere.radius < minHeight) + return false; + return Check::Intersection(ray, sphere, CmV, t, radius, minHeight); + } + // sphere projects after the cylinder end + if (t > maxHeight) { + if (t-sphere.radius > maxHeight) + return false; + return Check::Intersection(ray, sphere, CmV, t, radius, maxHeight); + } + const TYPE lenSq((CmV - ray.m_vDir * t).squaredNorm()); + return lenSq <= SQUARE(sphere.radius + radius); +} // Intersects +/*----------------------------------------------------------------*/ + +// Classify point to cylinder. +template +GCLASS TCylinder::Classify(const POINT& p, TYPE& t) const +{ + ASSERT(ISEQUAL(ray.m_vDir.norm(), TYPE(1))); + const VECTOR D(p - ray.m_pOrig); + t = ray.m_vDir.dot(D); + if (t < minHeight) return BACK; + if (t > maxHeight) return FRONT; + const TYPE rSq((D - ray.m_vDir*t).squaredNorm()); + const TYPE radiusSq(SQUARE(radius)); + if (rSq > radiusSq) return CULLED; + if (rSq < radiusSq) return VISIBLE; + return PLANAR; +} // Classify +/*----------------------------------------------------------------*/ + + +// S T R U C T S /////////////////////////////////////////////////// + +template +bool TConeIntersect::operator()(const SPHERE& sphere) const +{ + const VECTOR CmV(sphere.center - cone.ray.m_pOrig); + const VECTOR D(CmV + cone.ray.m_vDir * (sphere.radius*invSinAngle)); + TYPE e = D.dot(cone.ray.m_vDir); + if (e <= TYPE(0) || e*e < D.squaredNorm()*cosAngleSq) + return false; + e = CmV.dot(cone.ray.m_vDir); + if (e-sphere.radius > cone.maxHeight) + return false; + if (e < cone.minHeight) { + const TYPE lenSq = CmV.squaredNorm(); + if (e*e >= lenSq*sinAngleSq) + return lenSq <= SQUARE(sphere.radius); + } + return true; +} // Intersect +/*----------------------------------------------------------------*/ + +// Classify point to cone. +template +GCLASS TConeIntersect::Classify(const POINT& p, TYPE& t) const +{ + ASSERT(ISEQUAL(cone.ray.m_vDir.norm(), TYPE(1))); + const VECTOR D(p - cone.ray.m_pOrig); + t = cone.ray.m_vDir.dot(D); + if (ISZERO(t)) + return PLANAR; + if (t < cone.minHeight) return BACK; + if (t > cone.maxHeight) return FRONT; + ASSERT(!ISZERO(D.norm())); + const TYPE tSq(SQUARE(t)); + const TYPE dSq(cosAngleSq*D.squaredNorm()); + if (tSq < dSq) return CULLED; + if (tSq > dSq) return VISIBLE; + return PLANAR; +} // Classify +/*----------------------------------------------------------------*/ + +template +bool TestRayTriangleIntersection(unsigned iters) { + typedef SEACAVE::TTriangle Triangle; + typedef SEACAVE::TRay Ray; + typedef typename Ray::POINT Point; + typedef typename Point::Scalar Type; + constexpr Type zeroEps(std::is_same::value ? 0.01f : 0.00001f); + for (unsigned iter=0, lives=iters/100; iter(triangle, &t) && !lives--) + return false; + const Point _center(rayCenter.GetPoint(t)); + if ((_center-center).norm() > zeroEps && !lives--) + return false; + const BYTE o((BYTE)(RAND()%3)); + const Point side(((triangle[o].template cast()+triangle[(o+1)%3].template cast()) / 2.0).template cast()); + const Ray raySide(rayCenter.m_pOrig, side, true); + if (!raySide.template Intersects(triangle, &t) && !lives--) + return false; + const Point _side(raySide.GetPoint(t)); + if ((_side-side).norm() > zeroEps && !lives--) + return false; + } + return true; +} diff --git a/libs/Common/Rotation.h b/libs/Common/Rotation.h new file mode 100644 index 0000000..ebc6b85 --- /dev/null +++ b/libs/Common/Rotation.h @@ -0,0 +1,595 @@ +//////////////////////////////////////////////////////////////////// +// Rotation.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_ROTATION_H__ +#define __SEACAVE_ROTATION_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + +#ifndef _RELEASE +#define _CPC_DEBUG +#endif + +#define CPC_ERROR(msg) { SLOG(msg); ASSERT(false); } +#if TD_VERBOSE != TD_VERBOSE_OFF +#define CPC_SLOG(msg) if (VERBOSITY_LEVEL > 0) SLOG(msg) +#define CPC_LOG(t, msg) CPC_SLOG(msg) +#else +#define CPC_SLOG(msg) +#define CPC_LOG(t, msg) +#endif + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +// generic matrix struct +#define QUATERNION_EPSILON 1E-10 // DBL_EPSILON, too small (dherzog) + +// forward declaration to avoid mutual inclusion +template class TRMatrixBase; + + /** @class TQuaternion + * @test tested with TestRotationConversion.cpp + @ingroup g_geometry + @brief class for rotation with axis and angle + + Quaternions can be used for rotation around an axis through the + origin by spcecifing the normal direction vector of the axis and + an angle. + + The rotation for positive angles is clockwise, + when looking in direction of the axis. + + Contains qx=[0], qy=[1], qz=[2], qw=[3]. + + Not every quaternion describes a rotation. + Only quaternions of the form: + + qx = x * sin(phi/2) , which is the imaginary part i + + qy = y * sin(phi/2) , which is the imaginary part j + + qz = z * sin(phi/2) , which is the imaginary part k + + qw = cos(phi/2), , which is the real part + + where (x, y, z) is the normalized direction vector of the axis + and phi is the angle of rtoation. + + Some useful quaternions: + x y z w Description + 0 0 0 1 Identity quaternion, + no rotation + 1 0 0 0 180' turn around X axis + 0 1 0 0 180' turn around Y axis + 0 0 1 0 180' turn around Z axis + sqrt(0.5) 0 0 sqrt(0.5) 90' rotation around X axis + 0 sqrt(0.5) 0 sqrt(0.5) 90' rotation around Y axis + 0 0 sqrt(0.5) sqrt(0.5) 90' rotation around Z axis + -sqrt(0.5) 0 0 sqrt(0.5) -90' rotation around X axis + 0 -sqrt(0.5) 0 sqrt(0.5) -90' rotation around Y axis + 0 0 -sqrt(0.5) sqrt(0.5) -90' rotation around Z axis + + @author grest 06/2003 + */ +template +class TQuaternion : public TMatrix +{ +public: + typedef TMatrix Base; + typedef TMatrix Mat; + typedef TMatrix Vec; + typedef TMatrix Rot; + + using Base::val; + +public: + static const TQuaternion IDENTITY; + +public: + inline ~TQuaternion(); + + inline TQuaternion(); + + inline TQuaternion(const Base& q); + + inline TQuaternion(TYPE i, TYPE j, TYPE k, TYPE r); + + inline TQuaternion(const Rot& R); + + inline void SetIdentity(); + + /** + * Sets all parts of quaternion in mathematical order, + * real part first, then 1.,2. and 3. imaginary part + */ + inline void SetQuaternion(TYPE real, TYPE i, TYPE j, TYPE k); + + /** returns the Inverse rotation TQuaternion + */ + inline TQuaternion Inverse() const; + + /** inverts this, by changing the rotation axis by *=-1 + */ + inline void Invert(); + + /** makes TQuaternion-representation unique, ensuring vector lies + in upper (by real part) hemisphere. (inplace) + */ + inline void MakeUnique(); + + /** quaternion multiplication: this= this * quat + */ + inline void Mult(const TQuaternion &quat); + + /** quaternion multiplication: res = this * arg + */ + inline void Mult(const TQuaternion &arg, + TQuaternion &res) const; + + /** quaternion multiplication: this = quat * this + */ + inline void MultLeft(const TQuaternion &quat); + + /** rotates the given Vector with the quaternion ( q v q* ) + the resulting vector is given in res + @returns 0 in case of no error + @author Daniel Grest, June 2003 + */ + inline int MultVec(const Vec &vec, Vec &res) const; + + /* rets result from MultVec() + @author herzog 2005-07-15 */ + inline Vec MultVec(const Vec &vec) const; + + /** Computes this^(scale), which scales the angle from axis/angle- + representation with 'scale'. This is the same like inter-/extra- + polating between (0,0,0,1)-quaternion and this! + */ + TQuaternion Power(const TYPE & scale) const; + + /** Linear interpolation between this and given quaternion. + @note Quaternions are assumed to be unit quaternions! */ + TQuaternion InterpolateLinear(const TQuaternion &to, const TYPE & t) const; + + /** Spherical interpolation between this and given quaternion. + @note Quaternions are assumed to be unit quaternions! */ + TQuaternion Interpolate(const TQuaternion &to, const TYPE & t) const; + + /** sets the quaternion with given rotation axis and angle (in rad) + @returns 0 in case of no error + @author Daniel Grest, June 2003 + */ + inline void SetValueAsAxisRad(const Vec &axis, TYPE angle); + + /** sets the quaternion with given rotation axis and angle (in rad) + @returns 0 in case of no error + @author Daniel Grest, June 2003 + */ + + inline void SetValueAsAxisRad(TYPE axisX, TYPE axisY, TYPE axisZ, TYPE angle); + + /** Returns matrix which expresses (left) quaternion multiplication. + * A quaternions stored in an object can be understood as a 4-vector + * q =(ix iy iz r)^T. + * Left multiplying a quaternion q2 with a quaternion q1 can be + * expressed in terms of matrix multiplication as + * qRes = q1*q2 = M(q1)*q2. + * This method returns the matrix M(*this). + */ + Mat GetQuaternionMultMatrixLeft() const; + + /** Returns matrix which expresses right quaternion multiplication. + * Right multiplying a quaternion q1 with quaternion q2 can be + * expressed as qRes = q1*q2 = N(q2)*q1. + * This method returns the matrix N(*this). + */ + Mat GetQuaternionMultMatrixRight() const; + + /** Returns rotation axis. See GetRotationAxisAngle(). */ + Vec GetRotationAxis() const; + + /** Returns rotation angle notation in radians. + Note that the returned angle resides in the interval [0,PI)! */ + TYPE GetRotationAngle() const; + + /** Returns rotation in axis and angle notation (angle in radians). + Note that the returned angle resides in the interval [0,PI) and + the axis points into the according direction! + @author woelk 12 2002 */ + int GetAxisAngle(Vec& axis, TYPE& angle) const; + + /** Sets quaternion as concatenated rotations around + x,y,z-axis; this = q_x * q_y * q_z (BIAS-RMatrix conform) + @author herzog 2005-07-19 */ + int SetXYZ(TYPE radX, TYPE radY, TYPE radZ); + + /** Sets quaternion as concatenated rotations around + x,y,z-axis; this = q_z * q_y * q_x (BIAS-RMatrix conform) + @author herzog 2005-07-19 */ + int SetZYX(TYPE radX, TYPE radY, TYPE radZ); + + + /** Scales quaternion to unit length, i.e. norm equals 1. */ + inline void Normalize() { + const TYPE n = norm(*this); + if (ISZERO(n)) + *this = IDENTITY; + else + *this *= TYPE(1) / n; + } + inline bool IsNormalized() const { + return ISEQUAL(norm(*this), TYPE(1)); + } + + /** assignment operator + @author grest 06 2003 */ + TQuaternion& operator=(const TQuaternion& vec); + + /** Enforces rigid coupling constraint for this and the other given unit + quaternion (i.e. equal rotation angle resp. scalar part of both). + Computes direct simple interpolation of both unit quaternion where + the scalar parts of the resulting unit quaternion are identical. + This solution can be used as initial guess for numerical refinement. + @author esquivel 02/2012 */ + void EnforceRigidCouplingConstraint(TQuaternion &other); + + /** Enforces rigid coupling constraint for all quaternion in given vector. + @author esquivel 02/2012 */ + static void EnforceRigidCouplingConstraint(std::vector< TQuaternion > &quats); +}; // class +template const TQuaternion TQuaternion::IDENTITY(0,0,0,1); +/*----------------------------------------------------------------*/ +typedef TQuaternion QuaternionF; +typedef TQuaternion QuaternionD; +/*----------------------------------------------------------------*/ + + +// generic rotation matrix struct +#define ROTATION_MATRIX_EPSILON DBL_EPSILON + +/** @class TRMatrixBase + @ingroup g_geometry + @brief 3D rotation matrix + @author frahm, woelk **/ +template +class TRMatrixBase : public TMatrix +{ +public: + typedef TMatrix Base; + typedef Base Mat; + typedef TMatrix Vec; + typedef TQuaternion Quat; + typedef typename Base::Base BaseBase; + + using Base::val; + +public: + /** @brief Constructor setting matrix to identity */ + inline TRMatrixBase(); + inline TRMatrixBase(const BaseBase& rhs) : Base(rhs) {} + template inline TRMatrixBase(const cv::Matx& rhs) : Base(rhs) {} + + /** @brief Copy constructor from 3x3 matrix + @attention Orthonormality of matrix is enforced automatically! */ + inline TRMatrixBase(const Mat& mat); + + /** @brief Initialization from parametrized rotation (axis-angle) */ + inline TRMatrixBase(const Vec& rot); + + /** @brief Initialization from rotation axis w and angle phi (in rad) using Rodrigues' formula */ + inline TRMatrixBase(const Vec& w, const TYPE phi); + + /** @brief Initialization from quaternion */ + inline TRMatrixBase(const Quat& q); + + /** @brief Initialization with the rotation from roll/pitch/yaw (in rad) */ + inline TRMatrixBase(TYPE roll, TYPE pitch, TYPE yaw); + + /** @brief Initialization with the rotation from direction dir0 to direction dir1 */ + inline TRMatrixBase(const Vec& dir0, const Vec& dir1); + + template inline TRMatrixBase& operator = (const cv::Matx& rhs) { BaseBase::operator = (rhs); return *this; } + inline TRMatrixBase& operator = (const cv::Mat& rhs) { BaseBase::operator = (rhs); return *this; } + #ifdef _USE_EIGEN + inline TRMatrixBase& operator = (const typename Base::EMat& rhs) { Base::operator = (rhs); return *this; } + #endif + + /** @brief Set Euler angles (in rad) in order XYZ + + Set angles either in order 1. z, 2. y, 3. x and fixed axes + or in order 1. x, 2. y, 3. z and moving axes. + Angles are measured as in mathematics (counter-clockwise), i.e. + Set(x, 0, 0) results in the following matrix: + + | 1 0 0 | + | 0 cos(x) -sin(x) | + | 0 sin(x) cos(x) | + + (*this) = Rx*Ry*Rz = + + | c(y)c(z) -c(y)s(z) s(y) | + | c(z)s(x)s(y)+c(x)s(z) c(x)c(z)-s(x)s(y)s(z) -c(y)s(x) | + | -c(x)c(z)s(y)+s(x)s(z) c(z)s(x)+c(x)s(y)s(z) c(x)c(y) | + + with s(g) = sin(g), c(g) = cos(g), x = PhiX, y = PhiY and z = PhiZ + + @author frahm, woelk */ + void SetXYZ(TYPE PhiX, TYPE PhiY, TYPE PhiZ); + + + /** @brief Set Euler angles (in rad) in order XYZ from vector */ + inline void SetXYZ(const Vec& r) + { SetXYZ(r[0], r[1], r[2]); } + + /** @brief Set Euler angles (in rad) in order ZYX + + Set angles either in order 1. z, 2. y, 3. x and moving axes + or in order 1. x, 2. y, 3. z and fixed axes. + Angles are measured as in mathematics (counter-clockwise), i.e. + Set(x, 0, 0) results in the following matrix: + + | 1 0 0 | + | 0 cos(x) -sin(x) | + | 0 sin(x) cos(x) | + + (*this) = Rz*Ry*Rx = + + | c(y)c(z) s(x)s(y)c(z)-c(x)s(z) c(x)s(y)c(z)+s(x)s(z) | + | c(y)s(z) s(x)s(y)s(z)+c(x)c(z) c(x)s(y)s(z)-s(x)s(z) | + | -s(y) s(x)c(y) c(x)c(y) | + + with s(g) = sin(g), c(g) = cos(g), x = PhiX, y = PhiY and z = PhiZ + + @author frahm, woelk */ + void SetZYX(TYPE PhiX, TYPE PhiY, TYPE PhiZ); + + /** @brief Set Euler angles (in rad) in order ZYX from vector */ + inline void SetZYX(const Vec& r) + { SetZYX(r[0], r[1], r[2]); } + + /** @brief Set Euler angles (in rad) in order ZXY + + Set angles either in order 1. z, 2. x, 3. y and moving axes + or in order 1. y, 2. x, 3. z and fixed axes. + Angles are measured as in mathematics (counter-clockwise), i.e. + Set(x, 0, 0) results in the following matrix: + + | 1 0 0 | + | 0 cos(x) -sin(x) | + | 0 sin(x) cos(x) | + + (*this) = Rz*Rx*Ry = + + | c(y)c(z)-s(x)s(y)s(z), c(x)s(z), s(y)c(z)+s(x)c(y)s(z) | + | s(z)c(y)+s(x)s(y)c(z), c(x)c(z), s(y)*s(z)+s(x)-c(y)c(z) | + | -c(x)s(y) , s(x) , c(x)c(y) | + + with s(g) = sin(g), c(g) = cos(g), x = PhiX, y = PhiY and z = PhiZ + + @author haase */ + void SetZXY(TYPE PhiX, TYPE PhiY, TYPE PhiZ); + + /** @brief Set Euler angles (in rad) in order ZXY from vector */ + inline void SetZXY(const Vec& r) + { SetZXY(r[0], r[1], r[2]); } + + /** @brief Set Euler angles (in rad) in order YXZ + + Set angles either in order 1. y, 2. x, 3. z and moving axes + or in order 1. z, 2. x, 3. y and fixed axes. + Angles are measured as in mathematics (counter-clockwise), i.e. + Set(x, 0, 0) results in the following matrix: + + | 1 0 0 | + | 0 cos(x) sin(x) | + | 0 -sin(x) cos(x) | + + (*this) = Rz*Rx*Ry = + + | c(y)c(z)+s(x)s(y)s(z), c(x)s(z), -s(y)c(z)+s(x)c(y)s(z) | + | -s(z)c(y)+s(x)s(y)c(z), c(x)c(z), -s(y)*-s(z)+s(x)c(y)c(z) | + | c(x)s(y) , -s(x) , c(x)c(y) | + + with s(g) = sin(g), c(g) = cos(g), x = PhiX, y = PhiY and z = PhiZ + + @author haase */ + void SetYXZ(TYPE PhiZ, TYPE PhiX, TYPE PhiY); + + /** @brief Set Euler angles (in rad) in order YXZ from vector */ + inline void SetYXZ(const Vec& r) + { SetYXZ(r[0], r[1], r[2]); } + + /** @brief Set from rotation axis w and angle phi (in rad) + @param w Axis vector w will be normalized to length 1, so we need + |w|>1e-6 if phi != 0, otherwise an exception is thrown + @param phi Rotation angle is given in radians + @author evers, woelk */ + void Set(const Vec& w, TYPE phi); + + /** set this matrix from 3 vectors each representing a column*/ + void SetFromColumnVectors(const Vec& v0, + const Vec& v1, + const Vec& v2); + + /** set this matrix from 3 vectors, each representing a row */ + void SetFromRowVectors(const Vec& v0, + const Vec& v1, + const Vec& v2); + + /** @brief Set from rotation axis * angle (modified Rodrigues vector) + @author evers */ + void SetFromAxisAngle(const Vec& w); + + /* @brief Set rotation matrix from an orthogonal basis given in world + coordinate system (WCS) + + As R' is expressing the transformation from WCS to ICS (image + coordinate system), R is ICS to WCS and hereby represents the ICS + base vectors expressed in WCS. These are the *rows* of R because + the transformation is the scalar product. + + Assume xxx,yyy,zzz are right-hand orthogonal (RHS), e.g. the orthogonal + image coordinate system (ICS) base vectors expressed in world + coordinates (WCS). + (Inverse doesn't make sense because base would be identity then). + Normal vectors of length 1 are computed. + + @todo Warning if (xxx,yyy,zzz) are not an orthogonal basis! + + @author jw 09/2003, added exception */ + void SetFromOrthogonalBasis(const Vec &xxx, const Vec &yyy, const Vec &zzz); + + /** @brief Set rotation matrix from two vectors from an orthonormal basis + @param xh represents the first base vector and is left unchanged + @param vy should be orthogonal to xh, it is orthogonalized otherwise + + You can think of this routine as computing R from an image plane + given by two (usually orthogonal) base vectors. + If the given base vectors are not orthogonal, xh is kept and yv is + orthogonalized appropriately. + + @author jw */ + void SetFromHV(const Vec& xh, const Vec& vy); + + /** @brief Create rotation matrix that rotates from dir0 to dir1 + @param dir0 represents the first (reference) direction vector + @param dir1 represents the second (target) direction vector + @author cDc */ + TRMatrixBase& SetFromDir2Dir(const Vec& dir0, const Vec& dir1); + + /** @brief Calculates quaternion representation for this rotation matrix + @attention Scalar part of quaternion will always be non-negative + for sake of uniqueness of the resulting quaternion! + @author woelk 12/2003 + @author esquivel 03/2011 (changed algorithm, bugfix) */ + void GetQuaternion(Quat& q) const; + inline Quat GetQuaternion() const { Quat q; GetQuaternion(q); return q; } + + /** @brief Set rotation matrix from a quaternion + @author grest 06/2003 */ + void SetFromQuaternion(const Quat& q); + + /** @brief Set rotation matrix from direction and up vector + @note This is openGL conform, similar to gluLookAt(), i.e. if + direction is (0,0,-1) and up is (0,1,0) the resulting matrix is identity. + @author grest 12/2005 */ + void SetFromDirUpGL(const Vec& viewDir, const Vec& viewUp); + + /** @brief Set rotation matrix from direction and up vector */ + void SetFromDirUp(const Vec& viewDir, const Vec& viewUp); + + /** @brief Set rotation matrix from an eye point and a look-at target point and the up vector */ + void LookAt(const Vec& from, const Vec& to, const Vec& up); + + // get parametrized rotation (axis-angle) from the rotation matrix + inline void SetRotationAxisAngle(const Vec& rot); + + // modify the rotation matrix by the given parametrized delta rotation (axis-angle) + inline void Apply(const Vec& delta); + + // set rotation matrix to the given parametrized rotation (axis-angle) + inline Vec GetRotationAxisAngle() const; + + /** @brief Calculates angle and rotation axis representation for + this rotation matrix + @param angle Rotation angle is returned in radians + @author woelk 12/2003 */ + void GetRotationAxisAngle(Vec& axis, TYPE& angle) const; + + /** @brief Interface for axis component of GetRotationAxisAngle() */ + Vec GetRotationAxis() const; + + /** @brief Interface for angle component of GetRotationAxisAngle() */ + TYPE GetRotationAngle() const; + + /** @brief Get Euler angles for this rotation matrix in order XYZ + @attention Representation is not unique and has singularities at + +-pi/2. Assume for example (*this) = Rx * Ry * Rz, then we have + either Euler angles in order 1. x, 2. y, 3. z and moving axes + or in order 1. z, 2. y, 3. x and fixed axes. + @author woelk 01/2003 */ + int GetRotationAnglesXYZ(TYPE& PhiX, TYPE& PhiY, TYPE& PhiZ) const; + + /** @brief Get Euler angles for this rotation matrix in order XYZ + @see GetRotationAnglesXYZ(TYPE&, TYPE&, TYPE&) */ + inline int GetRotationAnglesXYZ(Vec& r) const + { return GetRotationAnglesXYZ(r[0], r[1], r[2]); } + + /** @brief Get Euler angles for this rotation matrix in order YZX + @attention Representation is not unique and has singularities at + +-pi/2. Assume for example (*this) = Rz * Ry * Rx, then we have + either Euler angles in order 1. x, 2. y, 3. z and fixed axes + or in order 1. z, 2. y, 3. x and moving axes. + @author woelk 01 2003 */ + int GetRotationAnglesZYX(TYPE& PhiX, TYPE& PhiY, TYPE& PhiZ) const; + + /** @brief Get Euler angles for this rotation matrix in order ZYX + @see GetRotationAnglesZYX(TYPE&, TYPE&, TYPE&) */ + inline int GetRotationAnglesZYX(Vec& r) const + { return GetRotationAnglesZYX(r[0], r[1], r[2]); } + + /** @brief Get Euler angles for this rotation matrix in order ZXY + @attention Representation is not unique and has singularities at + +-pi/2. Rotation order ZXY refers to rotation around point/vector! + @author haase 2007 */ + int GetRotationAnglesZXY(TYPE& PhiZ, TYPE& PhiX, TYPE& PhiY) const; + + /** @brief Get Euler angles for this rotation matrix in order ZXY + @see GetRotationAnglesZXY(TYPE&, TYPE&, TYPE&) */ + inline int GetRotationAnglesZXY(Vec& r) const + { return GetRotationAnglesZXY(r[2], r[0], r[1]); } + + /** @brief Get Euler angles for this rotation matrix in order YXZ + @attention Representation is not unique and has singularities at + +-pi/2. Rotation order YXZ refers to rotation with moving axes! + @author haase 2007 */ + int GetRotationAnglesYXZ(TYPE& PhiY, TYPE& PhiX, TYPE& PhiZ) const; + + /** @brief Get Euler angles for this rotation matrix in order YXZ + @see GetRotationAnglesYXZ(TYPE&, TYPE&, TYPE&) */ + inline int GetRotationAnglesYXZ(Vec& r) const + { return GetRotationAnglesYXZ(r[1], r[0], r[2]); } + + /** @brief Check that the matrix is a valid rotation matrix (orthogonal) */ + inline bool IsValid() const { + // the trace should be three and the determinant should be one + #if 1 + return (ISEQUAL((float)cv::determinant(*this), 1.f) && ISEQUAL((float)cv::trace((*this)*(*this).t()), 3.f)); + #else + return (ISEQUAL((TYPE)cv::determinant(*this), TYPE(1)) && ISEQUAL((TYPE)cv::trace((*this)*(*this).t()), TYPE(3))); + #endif + } + + /** @brief Check if this is a rotation matrix, i.e. if the determinant + is +1 and the columns are orthonormal + @param eps Numerical limit for constraint evaluation + @param verbose Show reason in case of failure */ + bool Check(const TYPE eps = std::numeric_limits::epsilon(), + int verbose = 0) const; + + /** @brief Enforce orthogonality constraint on rotation matrix and + sets determinant to +1 */ + void EnforceOrthogonality(); +}; // class +/*----------------------------------------------------------------*/ +typedef TRMatrixBase RMatrixBaseF; +typedef TRMatrixBase RMatrixBaseD; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#include "Rotation.inl" + +#endif // __SEACAVE_ROTATION_H__ + diff --git a/libs/Common/Rotation.inl b/libs/Common/Rotation.inl new file mode 100644 index 0000000..81b2d00 --- /dev/null +++ b/libs/Common/Rotation.inl @@ -0,0 +1,1563 @@ +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + + +// G L O B A L S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// C L A S S ////////////////////////////////////////////////////// + +template +inline TQuaternion::~TQuaternion() +{} + +template +inline TQuaternion::TQuaternion() + : Base() +{} + +template +inline TQuaternion::TQuaternion(const Base& q) + : Base(q) +{} + +template +inline TQuaternion::TQuaternion(TYPE i, TYPE j, TYPE k, TYPE r) + : Base(cv::Vec(i, j, k, r)) +{} + +template +inline TQuaternion::TQuaternion(const Rot& R) + : Base(((const TRMatrixBase&)R).GetQuaternion()) +{} + + +template +inline void TQuaternion::Invert() +{ + Base::operator()(0) *=-1; + Base::operator()(1) *=-1; + Base::operator()(2) *=-1; +} + +template +inline TQuaternion TQuaternion::Inverse() const +{ + TQuaternion res(*this); + res.Invert(); + return res; +} + +template +inline void TQuaternion::MakeUnique() +{ + //project quaternion to upper hemisphere (first ima.part >= 0) + TYPE *qThis = this->val; + if ((qThis[3] <= QUATERNION_EPSILON) + &&(qThis[3] >= -QUATERNION_EPSILON)) { + if ((qThis[1] <= QUATERNION_EPSILON) + &&(qThis[1] >= -QUATERNION_EPSILON)) { + if ((qThis[2] <= QUATERNION_EPSILON) + &&(qThis[2] >= -QUATERNION_EPSILON)) { + if (qThis[0] < 0.0) (*this) *= -1.0; + } else if (qThis[2] < 0.0) { + (*this) *= -1.0; + } + } else if (qThis[1] < 0.0) { + (*this) *= -1.0; + } + } else if (qThis[3] < 0.0) { + (*this) *= -1.0; + } +} + +template +inline void TQuaternion::Mult(const TQuaternion &quat) +{ + TYPE s1, s2, s3, s4, s5, s6, s7, s8, s9, t; + TYPE *qThis = this->val; + + s1 = (qThis[2] - qThis[1]) * (quat[1] - quat[2]); + s2 = (qThis[3] + qThis[0]) * (quat[3] + quat[0]); + s3 = (qThis[3] - qThis[0]) * (quat[1] + quat[2]); + s4 = (qThis[2] + qThis[1]) * (quat[3] - quat[0]); + s5 = (qThis[2] - qThis[0]) * (quat[0] - quat[1]); + s6 = (qThis[2] + qThis[0]) * (quat[0] + quat[1]); + s7 = (qThis[3] + qThis[1]) * (quat[3] - quat[2]); + s8 = (qThis[3] - qThis[1]) * (quat[3] + quat[2]); + + s9 = s6 + s7 + s8; + + t = (s5 + s9) / 2.0f; + + qThis[3] = s1 + t - s6; + qThis[0] = s2 + t - s9; + qThis[1] = s3 + t - s8; + qThis[2] = s4 + t - s7; +} + +template +inline void TQuaternion::Mult(const TQuaternion &arg, TQuaternion &res) const +{ + res = *this; + res.Mult(arg); +} + +template +inline void TQuaternion::MultLeft(const TQuaternion &quatLeft) +{ + TYPE s1, s2, s3, s4, s5, s6, s7, s8, s9, t; + TYPE *qThis = this->val; + + s1 = (quatLeft[2] - quatLeft[1]) * (qThis[1] - qThis[2]); + s2 = (quatLeft[3] + quatLeft[0]) * (qThis[3] + qThis[0]); + s3 = (quatLeft[3] - quatLeft[0]) * (qThis[1] + qThis[2]); + s4 = (quatLeft[2] + quatLeft[1]) * (qThis[3] - qThis[0]); + s5 = (quatLeft[2] - quatLeft[0]) * (qThis[0] - qThis[1]); + s6 = (quatLeft[2] + quatLeft[0]) * (qThis[0] + qThis[1]); + s7 = (quatLeft[3] + quatLeft[1]) * (qThis[3] - qThis[2]); + s8 = (quatLeft[3] - quatLeft[1]) * (qThis[3] + qThis[2]); + + s9 = s6 + s7 + s8; + + t = (s5 + s9) / 2.0f; + + qThis[3] = s1 + t - s6; + qThis[0] = s2 + t - s9; + qThis[1] = s3 + t - s8; + qThis[2] = s4 + t - s7; +} + +/** rotates the given Vector with the quaternion ( q v q* ) +the resulting vector is given in res +@returns 0 in case of no error +@author Daniel Grest, June 2003 +*/ +template +inline int TQuaternion::MultVec(const Vec &vec, Vec &res) const { + TYPE rx,ry,rz; + TYPE Vx= vec[0], Vy=vec[1], Vz=vec[2]; + const TYPE *q = this->val; + { + TYPE QwQx, QwQy, QwQz, QxQy, QxQz, QyQz; + QwQx = q[3] * q[0]; QwQy = q[3] * q[1]; QwQz = q[3] * q[2]; + QxQy = q[0] * q[1]; QxQz = q[0] * q[2]; QyQz = q[1] * q[2]; + rx = 2* (Vy * (-QwQz + QxQy) + Vz *( QwQy + QxQz)); + ry = 2* (Vx * ( QwQz + QxQy) + Vz *(-QwQx + QyQz)); + rz = 2* (Vx * (-QwQy + QxQz) + Vy *( QwQx + QyQz)); + } + TYPE QwQw, QxQx, QyQy, QzQz; + QwQw= q[3]*q[3]; QxQx = q[0]*q[0]; QyQy = q[1]*q[1]; QzQz= q[2]*q[2]; + rx+= Vx * (QwQw + QxQx - QyQy - QzQz); + ry+= Vy * (QwQw - QxQx + QyQy - QzQz); + rz+= Vz * (QwQw - QxQx - QyQy + QzQz); + res[0]=rx; res[1]=ry; res[2]=rz; + return 0; +} + +template +inline typename TQuaternion::Vec TQuaternion::MultVec(const Vec &vec) const +{ + Vec r; + MultVec(vec,r); + return r; +} + +template +inline void TQuaternion::SetIdentity() +{ + Base::operator()(0) = Base::operator()(1) = Base::operator()(2) = 0; + Base::operator()(3) = 1; +} + +template +inline void TQuaternion::SetQuaternion(TYPE real, TYPE i, + TYPE j, TYPE k) +{ + //Components are stored in Vec4 in order imaginary part 1,2,3 and real part + this->Set(i,j,k,real); +} + +template +inline void TQuaternion::SetValueAsAxisRad(const Vec &axis, TYPE angle) +{ + SetValueAsAxisRad(axis[0], axis[1], axis[2], angle); +} + +template +inline void TQuaternion::SetValueAsAxisRad(TYPE x, TYPE y, TYPE z, TYPE w) +{ + TYPE rTmp = TYPE(sqrt(x * x + y * y + z * z)); + + if(rTmp > QUATERNION_EPSILON) + { + rTmp = TYPE(sin(w / 2.0f)) / rTmp; + + Base::operator()(0) = x * rTmp; + Base::operator()(1) = y * rTmp; + Base::operator()(2) = z * rTmp; + Base::operator()(3) = TYPE(cos(w / 2.0f)); + } + else + { + SetIdentity(); + } +} + +template +typename TQuaternion::Mat TQuaternion::GetQuaternionMultMatrixLeft() const +{ + Mat res; + + res(0,0) = (*this)[3]; + res(0,1) = -(*this)[2]; + res(0,2) = (*this)[1]; + res(0,3) = (*this)[0]; + + res(1,0) = (*this)[2]; + res(1,1) = (*this)[3]; + res(1,2) = -(*this)[0]; + res(1,3) = (*this)[1]; + + res(2,0) = -(*this)[1]; + res(2,1) = (*this)[0]; + res(2,2) = (*this)[3]; + res(2,3) = (*this)[2]; + + res(3,0) = -(*this)[0]; + res(3,1) = -(*this)[1]; + res(3,2) = -(*this)[2]; + res(3,3) = (*this)[3]; + + return res; +} + +template +typename TQuaternion::Mat TQuaternion::GetQuaternionMultMatrixRight() const +{ + Mat res; + + res(0,0) = (*this)[3]; + res(0,1) = (*this)[2]; + res(0,2) = -(*this)[1]; + res(0,3) = (*this)[0]; + + res(1,0) = -(*this)[2]; + res(1,1) = (*this)[3]; + res(1,2) = (*this)[0]; + res(1,3) = (*this)[1]; + + res(2,0) = (*this)[1]; + res(2,1) = -(*this)[0]; + res(2,2) = (*this)[3]; + res(2,3) = (*this)[2]; + + res(3,0) = -(*this)[0]; + res(3,1) = -(*this)[1]; + res(3,2) = -(*this)[2]; + res(3,3) = (*this)[3]; + + return res; +} + +template +int TQuaternion::GetAxisAngle(Vec& axis, TYPE& angle) const +{ + // nicked from OpenSG + int res=0; + Vec q(cv::Vec((*this)[0], (*this)[1], (*this)[2])); + TYPE len = TYPE(norm(q)); + + if(len > QUATERNION_EPSILON) { + q *= TYPE(1.0) / len; + axis[0] = q[0]; + axis[1] = q[1]; + axis[2] = q[2]; + angle = TYPE(2.0 * acos((*this)[3])); + } else { + axis[0] = 0.0; + axis[1] = 0.0; + axis[2] = 1.0; + angle = 0.0; + } + return res; +} + +template +typename TQuaternion::Vec TQuaternion::GetRotationAxis() const +{ + Vec r(cv::Vec((*this)[0], (*this)[1], (*this)[2])); + TYPE len = TYPE(norm(r)); + if (len > QUATERNION_EPSILON) + return r * (1.0 / len); + else + return Vec(cv::Vec(0,0,1)); +} + +template +TYPE TQuaternion::GetRotationAngle() const +{ + const TYPE angle = TYPE(2 * acos((*this)[3])); + return std::isnan(angle) && ABS(ABS((*this)[3]) - TYPE(1)) < QUATERNION_EPSILON ? + TYPE(0) : + angle; +} + +template +TQuaternion TQuaternion::Power(const TYPE & scale) const +{ + TQuaternion res = *this; + if ((scale < 1e-7) && (scale > -1e-7)) { + //scale near zero -> return identity + res.SetIdentity(); + } else if ((res[3] > 0.999999) || (res[3] < -0.999999)) { + //this is near identity (numerical unstable) -> return identity + //res = *this + } else { + //else use slerp interpolation between identity and this with scale + TYPE temp1 = sin(scale * acos(res[3]))/sin(acos(res[3])); + TYPE res3 = res[3]; + res *= temp1; + res[3] += TYPE(sin((1.0-scale) * acos(res3))/sin(acos(res3))); + } + return res; +} + +template +TQuaternion TQuaternion::InterpolateLinear(const TQuaternion &to, const TYPE & t) const +{ + BIASASSERT(t >= 0.0 && t <= 1.0); + TQuaternion p = *this; + TQuaternion q = to; + //p.Normalize(); + //q.Normalize(); + if (t < 1e-7) { + //t near zero -> return p + return p; + } else if (1.0-t < 1e-7) { + //t near one -> return q + return q; + } + //else use linear interpolation between this and q + TQuaternion res; + const TYPE cosPhi = (TYPE)p.ScalarProduct(q); + const TYPE a = (TYPE)1.0 - t; + const TYPE b = (cosPhi < 0 ? -t : t); + res[0] = a*p[0] + b*q[0]; + res[1] = a*p[1] + b*q[1]; + res[2] = a*p[2] + b*q[2]; + res[3] = a*p[3] + b*q[3]; + res.Normalize(); + return res; +} + +template +TQuaternion TQuaternion:: + Interpolate(const TQuaternion &to, const TYPE & t) const +{ + BIASASSERT(t >= 0.0 && t <= 1.0); + TQuaternion p = *this; + TQuaternion q = to; + //p.Normalize(); + //q.Normalize(); + if (t < 1e-7) { + //t near zero -> return p + return p; + } else if (1.0-t < 1e-7) { + //t near one -> return q + return q; + } + TQuaternion res; + TYPE a, b; + TYPE cosPhi = (TYPE)p.ScalarProduct(q); + //adjust angle if necessary + if (cosPhi < 0) { + q.MultiplyIP(-1.0); + cosPhi = -cosPhi; + } + if (cosPhi > 0.9999 || cosPhi < -0.9999) { + // p is very close to q, do linear interpolation instead of slerp + a = (TYPE)1.0 - t; + b = (cosPhi < 0 ? -t : t); + } else { + //else use slerp interpolation between p and q + const TYPE phi = acos(cosPhi < 0 ? -cosPhi : cosPhi); + const TYPE sinPhi = sin(phi); + a = sin(((TYPE)1.0 - t) * phi) / sinPhi; + b = sin(t * phi) / sinPhi; + if (cosPhi < 0) b = -b; + } + res[0] = a*p[0] + b*q[0]; + res[1] = a*p[1] + b*q[1]; + res[2] = a*p[2] + b*q[2]; + res[3] = a*p[3] + b*q[3]; + res.Normalize(); + return res; +} + +template +int TQuaternion::SetXYZ(TYPE radX, TYPE radY, TYPE radZ) +{ + TQuaternion q_x; + TQuaternion q_y; + TQuaternion q_z; + + q_x.SetValueAsAxisRad( 1.,0.,0., radX ); + q_y.SetValueAsAxisRad( 0.,1.,0., radY ); + q_z.SetValueAsAxisRad( 0.,0.,1., radZ ); + + q_x.Mult(q_y); + q_x.Mult(q_z); + + *this = q_x; + + return 0; +} + +template +int TQuaternion::SetZYX(TYPE radX, TYPE radY, TYPE radZ) +{ + TQuaternion q_x; + TQuaternion q_y; + TQuaternion q_z; + + q_x.SetValueAsAxisRad( 1.,0.,0., radX ); + q_y.SetValueAsAxisRad( 0.,1.,0., radY ); + q_z.SetValueAsAxisRad( 0.,0.,1., radZ ); + + q_z.Mult(q_y); + q_z.Mult(q_x); + + *this = q_z; + + return 0; +} + +template +TQuaternion& TQuaternion::operator=(const TQuaternion& vec) +{ + memcpy((void *)val, (void *)vec.val, sizeof(TQuaternion)); + return *this; +} + +template +void TQuaternion::EnforceRigidCouplingConstraint(TQuaternion &other) +{ + // @todo check unit norm constraint of this and other quaternion!! + + // make this and other quaternion unique + MakeUnique(); + other.MakeUnique(); + + // get aliases for both quaternions + TQuaternion &q0 = *this, &q1 = other; + + // interpolate equal angle + q0[3] = q1[3] = TYPE(0.5) * (q0[3] + q1[3]); + + // re-enforce unit length constraint for q0 and q1 + TYPE q0norm = q0[0]*q0[0] + q0[1]*q0[1] + q0[2]*q0[2]; + TYPE q1norm = q1[0]*q1[0] + q1[1]*q1[1] + q1[2]*q1[2]; + if (q0norm > TYPE(1e-8) && q1norm > TYPE(1e-8)) { + TYPE qscale0 = (TYPE)sqrt(double((1-q0[3]*q0[3])/q0norm)); + TYPE qscale1 = (TYPE)sqrt(double((1-q1[3]*q1[3])/q1norm)); + for (int l = 0; l < 3; l++) { + q0[l] *= qscale0; + q1[l] *= qscale1; + } + } + + // @todo check unit norm constraint of resulting quaternions!! +} + +template +void TQuaternion::EnforceRigidCouplingConstraint(std::vector > &quats) +{ + // @todo check unit norm constraint of all quaternions!! + + // make all quaternions unique + const int n = quats.size(); + for (int i = 0; i < n; i++) + quats[i].MakeUnique(); + + // interpolate equal angle + double a = 0; + for (int i = 0; i < n; i++) + a += quats[i][3]; + a /= TYPE(n); + for (int i = 0; i < n; i++) + quats[i][3] = a; + + // re-enforce unit length constraint for all quaternions + for (int i = 0; i < n; i++) { + TQuaternion &q = quats[i]; + TYPE qnorm = q[0]*q[0] + q[1]*q[1] + q[2]*q[2]; + if (qnorm > TYPE(1e-8)) { + TYPE qscale = (TYPE)sqrt(double((1-q[3]*q[3])/qnorm)); + for (int l = 0; l < 3; l++) + q[l] *= qscale; + } + } + + // @todo check unit norm constraint of resulting quaternions!! +} +/*----------------------------------------------------------------*/ + + +// C L A S S ////////////////////////////////////////////////////// + +// accuracy for constraint check in Check() function calls +#define R_CONSTRAINT_ACCURACY 1e-5 //ZEROTOLERANCE() + +// if rotation angle is below this value, assume zero rotation +#define MINIMUM_ROTATION 1e-7 + +template +inline TRMatrixBase::TRMatrixBase() + : Base(Base::IDENTITY) +{} + +template +inline TRMatrixBase::TRMatrixBase(const Mat& mat) + : Base(mat) +{ + ASSERT(Check(R_CONSTRAINT_ACCURACY)); +} + +template +inline TRMatrixBase::TRMatrixBase(const Vec& w, TYPE phi) +{ + Set(w, phi); +} + +template +inline TRMatrixBase::TRMatrixBase(const Vec& rot) +{ + SetRotationAxisAngle(rot); +} + +template +inline TRMatrixBase::TRMatrixBase(const Quat& q) +{ + SetFromQuaternion(q); +} + +template +inline TRMatrixBase::TRMatrixBase(TYPE roll, TYPE pitch, TYPE yaw) +{ + SetXYZ(roll, pitch, yaw); +} +template +inline TRMatrixBase::TRMatrixBase(const Vec& dir0, const Vec& dir1) +{ + SetFromDir2Dir(dir0, dir1); +} + + +template +void TRMatrixBase::SetXYZ(TYPE PhiX, TYPE PhiY, TYPE PhiZ) +{ + #if 0 + TRMatrixBase Rx(TRMatrixBase::IDENTITY); + TRMatrixBase Ry(TRMatrixBase::IDENTITY); + TRMatrixBase Rz(TRMatrixBase::IDENTITY); + + /* set Rz, Ry, Rx as rotation matrices */ + Rz(0,0) = cos(PhiZ); + Rz(0,1) = -sin(PhiZ); + Rz(1,0) = -Rz(0,1); + Rz(1,1) = Rz(0,0); + //Rz(2,2) = 1; + + Ry(0,0) = cos(PhiY); + Ry(0,2) = sin(PhiY); + //Ry(1,1) = 1; + Ry(2,0) = -Ry(0,2); //-sin(PhiY); + Ry(2,2) = Ry(0,0); //cos(PhiY); + + //Rx(0,0) = 1; + Rx(1,1) = cos(PhiX); + Rx(1,2) = -sin(PhiX); + Rx(2,1) = -Rx(1,2); //sin(PhiX); + Rx(2,2) = Rx(1,1); //cos(PhiX); + + *this = Rx*Ry*Rz; + #else + const TYPE sin_x = sin(PhiX); + const TYPE sin_y = sin(PhiY); + const TYPE sin_z = sin(PhiZ); + const TYPE cos_x = cos(PhiX); + const TYPE cos_y = cos(PhiY); + const TYPE cos_z = cos(PhiZ); + val[0] = cos_y * cos_z; + val[1] = -cos_y * sin_z; + val[2] = sin_y; + val[3] = cos_x * sin_z + cos_z * sin_x * sin_y; + val[4] = cos_x * cos_z - sin_x * sin_y * sin_z; + val[5] = -cos_y * sin_x; + val[6] = sin_x * sin_z - cos_x * cos_z * sin_y; + val[7] = cos_z * sin_x + cos_x * sin_y * sin_z; + val[8] = cos_x * cos_y; + #endif +} + + +template +void TRMatrixBase::SetZYX(TYPE PhiX, TYPE PhiY, TYPE PhiZ) +{ + TRMatrixBase Rx(TRMatrixBase::IDENTITY); + TRMatrixBase Ry(TRMatrixBase::IDENTITY); + TRMatrixBase Rz(TRMatrixBase::IDENTITY); + + /* set Rz, Ry, Rx as rotation matrices */ + Rz(0,0) = cos(PhiZ); + Rz(0,1) = -sin(PhiZ); + Rz(1,0) = -Rz(0,1); + Rz(1,1) = Rz(0,0); + //Rz(2,2) = 1; + + Ry(0,0) = cos(PhiY); + Ry(0,2) = sin(PhiY); + //Ry(1,1) = 1; + Ry(2,0) = -Ry(0,2); //-sin(PhiY); + Ry(2,2) = Ry(0,0); //cos(PhiY); + + //Rx(0,0) = 1; + Rx(1,1) = cos(PhiX); + Rx(1,2) = -sin(PhiX); + Rx(2,1) = -Rx(1,2); //sin(PhiX); + Rx(2,2) = Rx(1,1); //cos(PhiX); + + *this = Rz*Ry*Rx; +} + + +template +void TRMatrixBase::SetYXZ(TYPE PhiY, TYPE PhiX, TYPE PhiZ) +{ + TRMatrixBase Rx(TRMatrixBase::IDENTITY); + TRMatrixBase Ry(TRMatrixBase::IDENTITY); + TRMatrixBase Rz(TRMatrixBase::IDENTITY); + + /* set Rz, Ry, Rx as rotation matrices */ + Rz(0,0) = cos(PhiZ); + Rz(0,1) = -sin(PhiZ); + Rz(1,0) = -Rz(0,1); + Rz(1,1) = Rz(0,0); + + + Ry(0,0) = cos(PhiY); + Ry(0,2) = sin(PhiY); + + Ry(2,0) = -Ry(0,2); //-sin(PhiY); + Ry(2,2) = Ry(0,0); //cos(PhiY); + + + Rx(1,1) = cos(PhiX); + Rx(1,2) = -sin(PhiX); + Rx(2,1) = -Rx(1,2); //sin(PhiX); + Rx(2,2) = Rx(1,1); //cos(PhiX); + + *this = Ry*Rx*Rz; +} + + +template +void TRMatrixBase::SetZXY(TYPE PhiX, TYPE PhiY, TYPE PhiZ) +{ + TRMatrixBase Rx(TRMatrixBase::IDENTITY); + TRMatrixBase Ry(TRMatrixBase::IDENTITY); + TRMatrixBase Rz(TRMatrixBase::IDENTITY); + + /* set Rz, Ry, Rx as rotation matrices */ + Rz(0,0) = cos(PhiZ); + Rz(0,1) = -sin(PhiZ); + Rz(1,0) = -Rz(0,1); + Rz(1,1) = Rz(0,0); + + + Ry(0,0) = cos(PhiY); + Ry(0,2) = sin(PhiY); + + Ry(2,0) = -Ry(0,2); //-sin(PhiY); + Ry(2,2) = Ry(0,0); //cos(PhiY); + + + Rx(1,1) = cos(PhiX); + Rx(1,2) = -sin(PhiX); + Rx(2,1) = -Rx(1,2); //sin(PhiX); + Rx(2,2) = Rx(1,1); //cos(PhiX); + + *this = Rz*Rx*Ry; +} + + + +template +void TRMatrixBase::Set(const Vec& wa, TYPE phi) +{ + // zero rotation results in identity matrix + if (ISZERO(phi)) { + *this = Base::IDENTITY; + return; + } + + const TYPE wnorm(norm(wa)); + if (wnorm < std::numeric_limits::epsilon()) { + CPC_ERROR("Vector "< +void TRMatrixBase::SetFromColumnVectors(const Vec& v0, const Vec& v1, const Vec& v2) +{ + val[0] = v0[0]; + val[1] = v1[0]; + val[2] = v2[0]; + val[3] = v0[1]; + val[4] = v1[1]; + val[5] = v2[1]; + val[6] = v0[2]; + val[7] = v1[2]; + val[8] = v2[2]; + ASSERT(Check(R_CONSTRAINT_ACCURACY)); +} + + +template +void TRMatrixBase::SetFromRowVectors(const Vec& v0, const Vec& v1, const Vec& v2) +{ + val[0] = v0[0]; + val[1] = v0[1]; + val[2] = v0[2]; + val[3] = v1[0]; + val[4] = v1[1]; + val[5] = v1[2]; + val[6] = v2[0]; + val[7] = v2[1]; + val[8] = v2[2]; + ASSERT(Check(R_CONSTRAINT_ACCURACY)); +} + + +template +void TRMatrixBase::SetFromAxisAngle(const Vec& w) +{ + #if 0 + const TYPE dAngle(norm(w)); + Set(w*INVERT(dAngle), dAngle); + #else + SetRotationAxisAngle(w); + #endif +} + + +template +void TRMatrixBase::GetQuaternion(Quat& quat) const +{ + ASSERT(Check(R_CONSTRAINT_ACCURACY)); + #if 1 + + // Conversion method from OpenSG + // @attention Removed old conversion in Linux build which had numerical + // issues for rotations close to 180 degree! (esquivel 03/2011) + + TYPE tr = Base::operator()(0,0) + Base::operator()(1,1) + Base::operator()(2,2); + + if (tr > 0.0) + { + // Use default formula if trace is large enough + TYPE s = sqrt(tr + 1.0); + + quat[3] = s * 0.5; + + s *= 2.0; + quat[0] = (Base::operator()(2,1) - Base::operator()(1,2)) / s; + quat[1] = (Base::operator()(0,2) - Base::operator()(2,0)) / s; + quat[2] = (Base::operator()(1,0) - Base::operator()(0,1)) / s; + } + else + { + // Use alternate formula using largest trace element + TYPE s; + unsigned i; + unsigned j; + unsigned k; + + // Find largest trace element i and its successors j, k + if(Base::operator()(1,1) > Base::operator()(0,0)) + i = 1; + else + i = 0; + if(Base::operator()(2,2) > Base::operator()(i,i)) + i = 2; + + j = (i + 1) % 3; + k = (j + 1) % 3; + + // Compute discriminator using largest trace element + s = sqrt(Base::operator()(i,i) - (Base::operator()(j,j) + Base::operator()(k,k)) + 1.0); + + // Compute quaternion entries + quat[i] = s * 0.5; + s *= 2.0; + + quat[j] = (Base::operator()(i,j) + Base::operator()(j,i)) / s; + quat[k] = (Base::operator()(i,k) + Base::operator()(k,i)) / s; + + quat[3] = (Base::operator()(k,j) - Base::operator()(j,k) ) / s; + } + + #elif 1 + + // Conversion method using derivation depending on max. denominator + // @see http://en.wikipedia.org/wiki/Rotation_representation_%28mathematics%29#Rotation_matrix_.E2.86.94_quaternion + // @deprecated + + // Determine max. denominator + TYPE d[4] = { 1 + Base::operator()(0,0) - Base::operator()(1,1) - Base::operator()(2,2), + 1 - Base::operator()(0,0) + Base::operator()(1,1) - Base::operator()(2,2), + 1 - Base::operator()(0,0) - Base::operator()(1,1) + Base::operator()(2,2), + 1 + Base::operator()(0,0) + Base::operator()(1,1) + Base::operator()(2,2) }; + int maxidx = 0; + for (int i = 1; i < 4; i++) { + if (d[maxidx] < d[i]) maxidx = i; + } + + // Compute quaternion starting with max. denominator + if (maxidx == 0) { + quat[0]=0.5*sqrt(1 + Base::operator()(0,0) - Base::operator()(1,1) - Base::operator()(2,2)); + quat[3]=0.25*(Base::operator()(2,1)-Base::operator()(1,2))/quat[0]; + quat[1]=0.25*(Base::operator()(0,1)+Base::operator()(1,0))/quat[0]; + quat[2]=0.25*(Base::operator()(0,2)+Base::operator()(2,0))/quat[0]; + } else if (maxidx == 1) { + quat[1]=0.5*sqrt(1 - Base::operator()(0,0) + Base::operator()(1,1) - Base::operator()(2,2)); + quat[3]=0.25*(Base::operator()(0,2)-Base::operator()(2,0))/quat[1]; + quat[2]=0.25*(Base::operator()(1,2)+Base::operator()(2,1))/quat[1]; + quat[0]=0.25*(Base::operator()(1,0)+Base::operator()(0,1))/quat[1]; + } else if (maxidx == 2) { + quat[2]=0.5*sqrt(1 - Base::operator()(0,0) - Base::operator()(1,1) + Base::operator()(2,2)); + quat[3]=0.25*(Base::operator()(1,0)-Base::operator()(0,1))/quat[2]; + quat[0]=0.25*(Base::operator()(2,0)+Base::operator()(0,2))/quat[2]; + quat[1]=0.25*(Base::operator()(2,1)+Base::operator()(1,2))/quat[2]; + } else { + quat[3]=0.5*sqrt(1 + Base::operator()(0,0) + Base::operator()(1,1) + Base::operator()(2,2)); + quat[0]=0.25*(Base::operator()(2,1)-Base::operator()(1,2))/quat[3]; + quat[1]=0.25*(Base::operator()(0,2)-Base::operator()(2,0))/quat[3]; + quat[2]=0.25*(Base::operator()(1,0)-Base::operator()(0,1))/quat[3]; + } + + #elif 1 + + // More compact implementation of the conversion method from OpenSG + // @deprecated + + TYPE trace = Base::operator()(0,0) + Base::operator()(1,1) + Base::operator()(2,2); + if ( trace > 0 ) { + TYPE s = 2.0 * sqrt(trace + 1.0); + quat[3] = 0.25 * s; + quat[0] = ( Base::operator()(2,1) - Base::operator()(1,2) ) / s; + quat[1] = ( Base::operator()(0,2) - Base::operator()(2,0) ) / s; + quat[2] = ( Base::operator()(1,0) - Base::operator()(0,1) ) / s; + } else { + if ( Base::operator()(0,0) > Base::operator()(1,1) && Base::operator()(0,0) > Base::operator()(2,2) ) { + TYPE s = 2.0 * sqrt( 1.0 + Base::operator()(0,0) - Base::operator()(1,1) - Base::operator()(2,2)); + quat[3] = (Base::operator()(2,1) - Base::operator()(1,2) ) / s; + quat[0] = 0.25 * s; + quat[1] = (Base::operator()(0,1) + Base::operator()(1,0) ) / s; + quat[2] = (Base::operator()(0,2) + Base::operator()(2,0) ) / s; + } else if (Base::operator()(1,1) > Base::operator()(2,2)) { + TYPE s = 2.0 * sqrt( 1.0 + Base::operator()(1,1) - Base::operator()(0,0) - Base::operator()(2,2)); + quat[3] = (Base::operator()(0,2) - Base::operator()(2,0) ) / s; + quat[0] = (Base::operator()(0,1) + Base::operator()(1,0) ) / s; + quat[1] = 0.25 * s; + quat[2] = (Base::operator()(1,2) + Base::operator()(2,1) ) / s; + } else { + TYPE s = 2.0 * sqrt( 1.0 + Base::operator()(2,2) - Base::operator()(0,0) - Base::operator()(1,1) ); + quat[3] = (Base::operator()(1,0) - Base::operator()(0,1) ) / s; + quat[0] = (Base::operator()(0,2) + Base::operator()(2,0) ) / s; + quat[1] = (Base::operator()(1,2) + Base::operator()(2,1) ) / s; + quat[2] = 0.25 * s; + } + } + + #else + + // Old conversion method + // @see http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm + // @deprecated + // @attention Computes wrong quaternion for rotation close to 180 degree! + + quat[3] = sqrt( max( 0.0, 1.0 + Base::operator()(0,0) + Base::operator()(1,1) + Base::operator()(2,2) ) ) / 2.0; + quat[0] = sqrt( max( 0.0, 1.0 + Base::operator()(0,0) - Base::operator()(1,1) - Base::operator()(2,2) ) ) / 2.0; + quat[1] = sqrt( max( 0.0, 1.0 - Base::operator()(0,0) + Base::operator()(1,1) - Base::operator()(2,2) ) ) / 2.0; + quat[2] = sqrt( max( 0.0, 1.0 - Base::operator()(0,0) - Base::operator()(1,1) + Base::operator()(2,2) ) ) / 2.0; + quat[0] = copysign( quat[0], Base::operator()(2,1) - Base::operator()(1,2) ) ; + quat[1] = copysign( quat[1], Base::operator()(0,2) - Base::operator()(2,0) ) ; + quat[2] = copysign( quat[2], Base::operator()(1,0) - Base::operator()(0,1) ) ; + + #endif + + // Normalize resulting quaternion + quat.Normalize(); + + // Correct quaternion so that scalar part is non-negative + if (quat[3] < 0) quat *= -1.0; +} + + +template +void TRMatrixBase::GetRotationAxisAngle(Vec& Axis, TYPE& Angle) const +{ + // nicked from OpenSG + GetQuaternion().GetAxisAngle(Axis, Angle); +} + + +template +typename TRMatrixBase::Vec TRMatrixBase::GetRotationAxis() const +{ + Vec Axis; + TYPE Angle; + GetRotationAxisAngle(Axis, Angle); + return Axis; +} + + +template +TYPE TRMatrixBase::GetRotationAngle() const +{ + Vec Axis; + TYPE Angle; + GetRotationAxisAngle(Axis, Angle); + return Angle; +} + + +template +int TRMatrixBase::GetRotationAnglesXYZ(TYPE& PhiX, TYPE& PhiY, TYPE& PhiZ) const +{ + #if 1 + if (Base::operator()(0,2) < 1) + { + if (Base::operator()(0,2) > -1) + { + PhiY = asin(Base::operator()(0,2)); + PhiX = atan2(-Base::operator()(1,2), Base::operator()(2,2)); + PhiZ = atan2(-Base::operator()(0,1), Base::operator()(0,0)); + } + else + { // WARNING. Not a unique solution. + PhiY = -FHALF_PI; + PhiX = -atan2(Base::operator()(1,0), Base::operator()(1,1)); + PhiZ = 0.f; // any angle works + } + } + else + { // WARNING. Not a unique solution. + PhiY = FHALF_PI; + PhiX = atan2(Base::operator()(1,0), Base::operator()(1,1)); + PhiZ = 0.f; // any angle works + } + #else + if ((Base::operator()(0,2) == 1.0)||(Base::operator()(0,2) == -1.0)) { + ASSERT("Rotation for Y-axis is +-PI/2, can not decompose X-/Z-component!" == NULL); + return -1; + } + PhiY = asin(Base::operator()(0,2)); + if (ISINFORNAN(PhiY)) return -1; + // changed this from atan to atan2 (koeser) + PhiX = atan2(-Base::operator()(1,2),Base::operator()(2,2)); + if (ISINFORNAN(PhiX)) return -2; + PhiZ = atan2(-Base::operator()(0,1),Base::operator()(0,0)); + if (ISINFORNAN(PhiZ)) return -3; + #endif +// #ifdef _CPC_DEBUG +// TRMatrixBase R; +// R.SetXYZ(PhiX, PhiY, PhiZ); +// if (R == (*this)) +// res = 0; +// else +// res = -1; +// #endif + return 0; +} + + +template +int TRMatrixBase::GetRotationAnglesZXY(TYPE& PhiX, TYPE& PhiY, TYPE& PhiZ) const +{ + if ((Base::operator()(0,2) == 1.0)||(Base::operator()(0,2) == -1.0)) { + DEBUG("Rotation for Y-axis is +-PI/2, can not decompose X-/Z-component!"); + return -1; + } + + PhiZ = atan2(-Base::operator()(0,1),Base::operator()(1,1)); + if (ISINFORNAN(PhiZ)) return -3; + + PhiX = asin(Base::operator()(2,1)); + if (ISINFORNAN(PhiX)) return -2; + PhiY = atan2(-Base::operator()(2,0),Base::operator()(2,2)); + if (ISINFORNAN(PhiY)) return -1; + + + // #ifdef _CPC_DEBUG + // TRMatrixBase R; + // R.SetZXY(PhiX, PhiY, PhiZ); + // cout<<"Erg R: "< +int TRMatrixBase::GetRotationAnglesYXZ(TYPE& PhiY, TYPE& PhiX, TYPE& PhiZ) const +{ + if ((Base::operator()(0,2) == 1.0)||(Base::operator()(0,2) == -1.0)) { + DEBUG("Rotation for Y-axis is +-PI/2, can not decompose X-/Z-component!"); + return -1; + } + + PhiZ = atan2(Base::operator()(0,2),Base::operator()(2,2)); + if (ISINFORNAN(PhiZ)) return -3; + + PhiX = asin(-Base::operator()(1,2)); + if (ISINFORNAN(PhiX)) return -2; + PhiY = atan2(Base::operator()(1,0),Base::operator()(1,1)); + if (ISINFORNAN(PhiY)) return -1; + + + // #ifdef _CPC_DEBUG + // TRMatrixBase R; + // R.SetZXY(PhiX, PhiY, PhiZ); + // cout<<"Erg R: "< +int TRMatrixBase::GetRotationAnglesZYX(TYPE& PhiX, TYPE& PhiY, TYPE& PhiZ) const +{ + if ((Base::operator()(0,2) == 1.0)||(Base::operator()(0,2) == -1.0)) { + DEBUG("Rotation for Y-axis is +-PI/2, cannot decompose X-/Z-component!"); + return -1; + } + PhiY = asin(-Base::operator()(2,0)); + if (ISINFORNAN(PhiY)) return -1; + // changed this from atan to atan2 (koeser) + PhiX = atan2(Base::operator()(2,1),Base::operator()(2,2)); + if (ISINFORNAN(PhiX)) return -2; + PhiZ = atan2(Base::operator()(1,0),Base::operator()(0,0)); + if (ISINFORNAN(PhiZ)) return -3; +// #ifdef _CPC_DEBUG +// TRMatrixBase R; +// R.SetZYX(PhiX, PhiY, PhiZ); +// if (R == (*this)) +// res = 0; +// else +// res = -1; +// #endif + return 0; +} + + +template +void TRMatrixBase::SetFromQuaternion(const Quat& qu) +{ + // fast version, see below for old code: + const TYPE nrm = 1.0/norm(qu); + const TYPE q0 = qu[0]*nrm; + const TYPE q1 = qu[1]*nrm; + const TYPE q2 = qu[2]*nrm; + const TYPE q3 = qu[3]*nrm; + + TYPE* pData = val; + + *pData++ = 1.0 - 2.0 * (q1 * q1 + q2 * q2); + *pData++ = 2.0 * (q0 * q1 - q2 * q3); + *pData++ = 2.0 * (q2 * q0 + q1 * q3); + + *pData++ = 2.0 * (q0 * q1 + q2 * q3); + *pData++ = 1.0 - 2.0 * (q2 * q2 + q0 * q0); + *pData++ = 2.0 * (q1 * q2 - q0 * q3); + + *pData++ = 2.0 * (q2 * q0 - q1 * q3); + *pData++ = 2.0 * (q1 * q2 + q0 * q3); + *pData++ = 1.0 - 2.0 * (q1 * q1 + q0 * q0); + + +#ifdef WANT_TO_COMPARE_WITH_OLD_CODE + + Quat q = qu; // de-const + q.Normalize(); + + // nicked from OpenSG + TRMatrixBase RCompare(MatrixZero); + RCompare[0][0] = 1.0 - 2.0 * (q[1] * q[1] + q[2] * q[2]); + RCompare[1][0] = 2.0 * (q[0] * q[1] + q[2] * q[3]); + RCompare[2][0] = 2.0 * (q[2] * q[0] - q[1] * q[3]); + + RCompare[0][1] = 2.0 * (q[0] * q[1] - q[2] * q[3]); + RCompare[1][1] = 1.0 - 2.0 * (q[2] * q[2] + q[0] * q[0]); + RCompare[2][1] = 2.0 * (q[1] * q[2] + q[0] * q[3]); + + RCompare[0][2] = 2.0 * (q[2] * q[0] + q[1] * q[3]); + RCompare[1][2] = 2.0 * (q[1] * q[2] - q[0] * q[3]); + RCompare[2][2] = 1.0 - 2.0 * (q[1] * q[1] + q[0] * q[0]); + + for (unsigned i=0; i<3; i++) { + for (unsigned j=0; j<3; j++) { + if (!equal(Base::operator()(i,j), RCompare[i][j], TYPE(1e-12))) { + SLOG("Matrices are not same !"<<*this<<" "< +void TRMatrixBase::SetFromOrthogonalBasis(const Vec& xxx, const Vec& yyy, const Vec& zzz) +{ + SetFromRowVectors(normalized(xxx), + normalized(yyy), + normalized(zzz)); +} + + +template +void TRMatrixBase::SetFromHV(const Vec& xxx, const Vec& yyy) +{ + // normalize + const Vec x0(normalized(xxx)); + + // compute base z in rhs: + const Vec z0(normalized(cross(xxx, yyy))); + + // compute y base orthogonal in rhs + const Vec y0(normalized(cross(z0, x0))); + + SetFromColumnVectors(x0, y0, z0); +} + + +template +TRMatrixBase& TRMatrixBase::SetFromDir2Dir(const Vec& dir0, const Vec& dir1) +{ + ASSERT(ISEQUAL(norm(dir0), TYPE(1))); + ASSERT(ISEQUAL(norm(dir1), TYPE(1))); + const TYPE cos01(CLAMP(dir1.dot(dir0), TYPE(-1), TYPE(1))); + const TYPE sin01Sq(TYPE(1) - SQUARE(cos01)); + if (sin01Sq > EPSILONTOLERANCE()) { + const Vec v(cross(dir0, dir1)); + const Mat V(CreateCrossProductMatrix3T(v)); + *this = Mat::IDENTITY + V + (V*V)*((TYPE(1)-cos01)/sin01Sq); + } else { + *this = Mat::ZERO; + if (cos01 > TYPE(0)) { + Base::operator()(0,0) = TYPE(1); + Base::operator()(1,1) = TYPE(1); + Base::operator()(2,2) = TYPE(1); + } else { + Base::operator()(0,0) = TYPE(-1); + Base::operator()(1,1) = TYPE(-1); + Base::operator()(2,2) = TYPE(-1); + } + } + return *this; +} + + +template +void TRMatrixBase::SetFromDirUpGL(const Vec& viewDir, const Vec& viewUp) +{ + ASSERT(ISEQUAL(norm(viewDir), TYPE(1))); + ASSERT(ISEQUAL(norm(viewUp), TYPE(1))); + const Vec right(normalized(cross(viewDir, viewUp))); + const Vec up(normalized(cross(right, viewDir))); + const Vec forward(viewDir * TYPE(-1)); // convert to right handed system + SetFromColumnVectors(right, up, forward); +} + +template +void TRMatrixBase::SetFromDirUp(const Vec& viewDir, const Vec& viewUp) +{ + ASSERT(ISEQUAL(norm(viewDir), TYPE(1))); + ASSERT(ISEQUAL(norm(viewUp), TYPE(1))); + const Vec right(normalized(cross(viewDir, viewUp))); + const Vec up(normalized(cross(viewDir, right))); + const Vec& forward(viewDir); + SetFromColumnVectors(right, up, forward); +} + +template +void TRMatrixBase::LookAt(const Vec& from, const Vec& to, const Vec& up) +{ + SetFromDirUp(normalized(to-from), up); +} + + +template +inline void TRMatrixBase::SetRotationAxisAngle(const Vec& rot) +{ + #if 0 + // set rotation using Rodriguez formula + *this = Mat::IDENTITY; + const TYPE thetaSq = normSq(rot); + if (thetaSq < TYPE(1e-12)) + return; + const TYPE theta = sqrt(thetaSq); + // make cross product matrix of rot + Mat J; + J(0,0) = TYPE(0); J(0,1) = -rot.z; J(0,2) = rot.y; + J(1,0) = rot.z; J(1,1) = TYPE(0); J(1,2) = -rot.x; + J(2,0) = -rot.y; J(2,1) = rot.x; J(2,2) = TYPE(0); + // compute R matrix + const Mat J2 = J * J; + const TYPE c1 = sin(theta)/theta; + const TYPE c2 = (TYPE(1)-cos(theta))/thetaSq; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + (*this)(i,j) += c1*J(i,j) + c2*J2(i,j); + #elif !defined(_USE_EIGEN) + // set rotation using Rodriguez formula + const TYPE x2 = rot.x*rot.x; + const TYPE y2 = rot.y*rot.y; + const TYPE z2 = rot.z*rot.z; + const TYPE t2 = x2+y2+z2; + if (t2 > TYPE(1e-12)) { + // We want to be careful to only evaluate the square root if the + // norm of the angle_axis vector is greater than zero. Otherwise + // we get a division by zero. + const TYPE t = sqrt(t2); + const TYPE ct = /*t==0.0?0.5:*/(TYPE(1)-cos(t))/t2; + const TYPE st = /*t==0.0?1.0:*/sin(t)/t; + val[0] = TYPE(1) - (y2 + z2)*ct; + val[1] = rot.x*rot.y*ct - rot.z*st; + val[2] = rot.z*rot.x*ct + rot.y*st; + val[3] = rot.x*rot.y*ct + rot.z*st; + val[4] = TYPE(1) - (z2 + x2)*ct; + val[5] = rot.y*rot.z*ct - rot.x*st; + val[6] = rot.z*rot.x*ct - rot.y*st; + val[7] = rot.y*rot.z*ct + rot.x*st; + val[8] = TYPE(1) - (x2 + y2)*ct; + } else { + // At zero, we switch to using the first order Taylor expansion. + val[0] = TYPE(1); + val[1] =-rot.z; + val[2] = rot.y; + val[3] = rot.z; + val[4] = TYPE(1); + val[5] =-rot.x; + val[6] =-rot.y; + val[7] = rot.x; + val[8] = TYPE(1); + } + #else + *this = Eigen::SO3(rot).get_matrix(); + #endif +} + +template +inline void TRMatrixBase::Apply(const Vec& delta) +{ + const TRMatrixBase dR(delta); + *this = dR * (*this); +} + +template +inline typename TRMatrixBase::Vec TRMatrixBase::GetRotationAxisAngle() const +{ + #if 0 + // get parametrized rotation using quaternions + Vec angle_axis; + TYPE angle; + GetQuaternion().GetAxisAngle(angle_axis, angle); + angle_axis.Normalize(); + angle_axis *= angle; + return angle_axis; + #elif 0 + // get parametrized rotation using Rodriguez formula + Vec angle_axis; + // x = k * 2 * sin(theta), where k is the axis of rotation. + angle_axis[0] = (*this)(2, 1) - (*this)(1, 2); + angle_axis[1] = (*this)(0, 2) - (*this)(2, 0); + angle_axis[2] = (*this)(1, 0) - (*this)(0, 1); + static const TYPE kZero(0); + static const TYPE kOne(1); + static const TYPE kTwo(2); + // Since the right hand side may give numbers just above 1.0 or + // below -1.0 leading to atan misbehaving, we threshold. + const TYPE costheta(MINF(MAXF(((*this)(0, 0) + (*this)(1, 1) + (*this)(2, 2) - kOne)/kTwo, TYPE(-1.0)), kOne)); + // sqrt is guaranteed to give non-negative results, so we only + // threshold above. + const TYPE sintheta(MINF(norm(angle_axis)/kTwo, kOne)); + // Use the arctan2 to get the right sign on theta + const TYPE theta(atan2(sintheta, costheta)); + // Case 1: sin(theta) is large enough, so dividing by it is not a + // problem. We do not use abs here, because while jets.h imports + // std::abs into the namespace, here in this file, abs resolves to + // the int version of the function, which returns zero always. + // + // We use a threshold much larger then the machine epsilon, because + // if sin(theta) is small, not only do we risk overflow but even if + // that does not occur, just dividing by a small number will result + // in numerical garbage. So we play it safe. + static const TYPE kThreshold(TYPE(1e-12)); + if ((sintheta > kThreshold) || (sintheta < -kThreshold)) { + angle_axis *= theta / (kTwo * sintheta); + return angle_axis; + } + // Case 2: theta ~ 0, means sin(theta) ~ theta to a good + // approximation. + if (costheta > kZero) { + angle_axis *= TYPE(0.5); + return angle_axis; + } + // Case 3: theta ~ pi, this is the hard case. Since theta is large, + // and sin(theta) is small. Dividing theta by sin(theta) will either + // give an overflow or worse still numerically meaningless results. + // Thus we use an alternate more complicated and expensive formula. + #ifndef _USE_EIGEN + // Since cos(theta) is negative, division by (1-cos(theta)) cannot + // overflow. + const TYPE inv_one_minus_costheta(kOne / (kOne - costheta)); + // We now compute the absolute value of coordinates of the axis + // vector using the diagonal entries of R. To resolve the sign of + // these entries, we compare the sign of angle_axis[i]*sin(theta) + // with the sign of sin(theta). If they are the same, then + // angle_axis[i] should be positive, otherwise negative. + for (int i = 0; i < 3; ++i) { + angle_axis[i] = theta * sqrt(((*this)(i, i) - costheta) * inv_one_minus_costheta); + if (((sintheta < kZero) && (angle_axis[i] > kZero)) || + ((sintheta > kZero) && (angle_axis[i] < kZero))) + angle_axis[i] = -angle_axis[i]; + } + #else + Eigen::AngleAxis aa; + aa.fromRotationMatrix(Base::EMatMap((TYPE*)this)); + angle_axis[0] = aa.angle() * aa.axis()[0]; + angle_axis[1] = aa.angle() * aa.axis()[1]; + angle_axis[2] = aa.angle() * aa.axis()[2]; + #endif + return angle_axis; + #else + return reinterpret_cast*>(this)->ln(); + #endif +} + + +template +bool TRMatrixBase::Check(const TYPE eps, int verbose) const +{ + // check determinant + bool ok = equal(TYPE(cv::determinant(*this)), TYPE(1), eps); + // check unit column vector length + Vec cl[3]; + for (unsigned i = 0; i < 3 && ok; i++) { + cl[i] = Base::col(i); + ok = equal(norm(cl[i]), TYPE(1), eps); + } + // check orthogonality + if (ok) + ok = equal(cl[0].dot(cl[1]), TYPE(0), eps); + if (ok) + ok = equal(cl[0].dot(cl[2]), TYPE(0), eps); + if (ok) + ok = equal(cl[1].dot(cl[2]), TYPE(0), eps); + #if TD_VERBOSE != TD_VERBOSE_OFF + // print the reason in case of failure + if (!ok && VERBOSITY_LEVEL > verbose) { + std::ostringstream res(" "); + const TYPE det(cv::determinant(*this)); + if (!equal(det, TYPE(1), eps)) { + res << "det != 1 (det=" << std::setprecision(30) << det << ") "; + } + for (unsigned i = 0; i < 3; i++) { + cl[i] = Base::col(i); + if (!equal(norm(cl[i]), TYPE(1), eps)) { + res << "col[" << i << "].NormL2() != 1) (= " + << norm(cl[i]) << ") "; + } + } + if (!equal(cl[0].dot(cl[1]), TYPE(0), eps)) { + res << "col[0].ScalarProduct(c[1]) != 0 (= " + << cl[0].dot(cl[1]) << ") "; + } + if (!equal(cl[0].dot(cl[2]), TYPE(0), eps)) { + res << "col[0].ScalarProduct(c[2]) != 0 (= " + << cl[0].dot(cl[2]) << ") "; + } + if (!equal(cl[1].dot(cl[2]), TYPE(0), eps)) { + res << "col[1].ScalarProduct(c[2]) != 0 (= " + << cl[1].dot(cl[2]) << ") "; + } + SLOG("Rotation matrix is invalid: " << res.str() << std::endl); + } + #endif + return ok; +} + +template +void TRMatrixBase::EnforceOrthogonality() +{ + #if 1 + // use SVD decomposition to find the closest orthogonal matrix + const cv::SVD svd(*this, cv::SVD::MODIFY_A|cv::SVD::FULL_UV); + *this = svd.u*svd.vt; + #elif 1 + // replaces the 3 x 3 matrix with the nearest (least squares) orthogonal matrix; + // a combination of Babylonian iteration and Berthold KP Horn's formula R = M (MT M)^-1/2 + TYPE* r = this->val; + TYPE m[9], ri[9], x[9]; + memcpy(m, r, 9*sizeof(TYPE)); + //By trial and error I found that 4 iterates always works unless your matrix is waaay off. Limit 10. + for (unsigned iterate = 0; iterate != 10; iterate++) { + //Invert r. Use inverse and not transpose since r is only approximately orthogonal (transpose will result in slow, oscillating convergence). + InvertMatrix3x3(r, ri); + + //Get x = ri m + mt r. + x[0] = m[6]*r[6]+ri[2]*m[6]+m[3]*r[3]+ri[1]*m[3]+m[0]*ri[0]+m[0]*r[0]; + x[1] = m[6]*r[7]+ri[2]*m[7]+m[3]*r[4]+ri[1]*m[4]+m[0]*r[1]+ri[0]*m[1]; + x[2] = m[6]*r[8]+ri[2]*m[8]+m[3]*r[5]+ri[1]*m[5]+m[0]*r[2]+ri[0]*m[2]; + x[3] = r[6]*m[7]+ri[5]*m[6]+m[3]*ri[4]+r[3]*m[4]+m[0]*ri[3]+r[0]*m[1]; + x[4] = m[7]*r[7]+ri[5]*m[7]+m[4]*ri[4]+m[4]*r[4]+m[1]*ri[3]+m[1]*r[1]; + x[5] = m[7]*r[8]+ri[5]*m[8]+m[4]*r[5]+ri[4]*m[5]+m[2]*ri[3]+m[1]*r[2]; + x[6] = m[6]*ri[8]+r[6]*m[8]+m[3]*ri[7]+m[0]*ri[6]+r[3]*m[5]+r[0]*m[2]; + x[7] = m[7]*ri[8]+r[7]*m[8]+m[4]*ri[7]+m[1]*ri[6]+r[4]*m[5]+r[1]*m[2]; + x[8] = m[8]*ri[8]+m[8]*r[8]+m[5]*ri[7]+m[2]*ri[6]+m[5]*r[5]+m[2]*r[2]; + + //Invert into ri. + InvertMatrix3x3(x, ri); + + //Replace r with 2 m ri, where ri is as replaced above. + r[0] = 2*(m[2]*ri[6]+m[1]*ri[3]+m[0]*ri[0]); + r[1] = 2*(m[2]*ri[7]+m[1]*ri[4]+m[0]*ri[1]); + r[2] = 2*(m[2]*ri[8]+m[1]*ri[5]+m[0]*ri[2]); + r[3] = 2*(m[5]*ri[6]+ri[3]*m[4]+ri[0]*m[3]); + r[4] = 2*(m[5]*ri[7]+m[4]*ri[4]+ri[1]*m[3]); + r[5] = 2*(m[5]*ri[8]+m[4]*ri[5]+ri[2]*m[3]); + r[6] = 2*(ri[6]*m[8]+ri[3]*m[7]+ri[0]*m[6]); + r[7] = 2*(ri[7]*m[8]+ri[4]*m[7]+ri[1]*m[6]); + r[8] = 2*(m[8]*ri[8]+ri[5]*m[7]+ri[2]*m[6]); + + //Check how off it is. + x[0] = ABS(r[0]*r[0] + r[3]*r[3] + r[6]*r[6] - TYPE(1)); + x[1] = ABS(r[1]*r[1] + r[4]*r[4] + r[7]*r[7] - TYPE(1)); + x[2] = ABS(r[2]*r[2] + r[5]*r[5] + r[8]*r[8] - TYPE(1)); + x[3] = ABS(r[0]*r[1] + r[3]*r[4] + r[6]*r[7]); + x[4] = ABS(r[1]*r[2] + r[4]*r[5] + r[7]*r[8]); + x[5] = ABS(r[2]*r[0] + r[5]*r[3] + r[8]*r[6]); + + //Max norm, by all means, really make it as accurate as possible. + TYPE maxElement(-FLT_MAX); + for (unsigned n=6; n != 0; ) + if(x[--n] > maxElement) + maxElement = x[n]; + if (maxElement < TYPE(1e-15)) return; + } + #else + // Matrix Orthogonalization + // by Eric Raible from "Graphics Gems", Academic Press, 1990 + // + // Reorthogonalize matrix R - that is find an orthogonal matrix that is + // "close" to R by computing an approximation to the orthogonal matrix + // + // T -1/2 + // RC = R(R R) + // T -1 + // [RC is orthogonal because (RC) = (RC) ] + // -1/2 + // To compute C, we evaluate the Taylor expansion of F(x) = (I + x) + // (where x = C - I) about x=0. + // This gives C = I - (1/2)x + (3/8)x^2 - (5/16)x^3 + ... + // By default, limit should be 8. + const unsigned max_limit = 8; + static const TYPE coef[10] = { 1, -1/2., 3/8., -5/16., 35/128., -63/256., 231/1024., -429/2048., 6435/32768., -12155/65536. }; + Base& R(*this); + const Base RtR(R.t()*R); + const Base X(RtR - Matrix3x3::IDENTITY); /* RtR - I */ + Base X_power(Matrix3x3::IDENTITY); /* X^0 */ + Base Sum(Matrix3x3::IDENTITY); /* coef[0] * X^0 */ + const unsigned limit(MINF(max_limit, (unsigned)10)); + for (unsigned power=1; power +inline bool IsRotationMatrix(const TMatrix& R) { + ASSERT(sizeof(TMatrix) == sizeof(TRMatrixBase)); + return ((const TRMatrixBase&)R).IsValid(); +} // IsRotationMatrix +template +inline bool IsRotationMatrix(const Eigen::Matrix& R) { + // the trace should be three and the determinant should be one + return (ISEQUAL(R.determinant(), TYPE(1)) && ISEQUAL((R*R.transpose()).trace(), TYPE(3))); +} // IsRotationMatrix +/*----------------------------------------------------------------*/ + +// enforce matrix orthogonality +template +inline void EnsureRotationMatrix(TMatrix& R) { + ASSERT(sizeof(TMatrix) == sizeof(TRMatrixBase)); + ((TRMatrixBase&)R).EnforceOrthogonality(); +} // EnsureRotationMatrix +/*----------------------------------------------------------------*/ + + +// compute the distance on SO(3) between the two given rotations +// using log(R) as in: "Efficient and Robust Large-Scale Rotation Averaging", 2013 +// same result as above, but returns directly the angle +template +inline TYPE ComputeAngleSO3(const TMatrix& I) { + return TYPE(norm(((const TRMatrixBase&)I).GetRotationAxisAngle())); +} // ComputeAngleSO3 +template +FORCEINLINE TYPE ComputeAngleSO3(const TMatrix& R1, const TMatrix& R2) { + return ComputeAngleSO3(TMatrix(R1*R2.t())); +} // ComputeAngleSO3 +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE diff --git a/libs/Common/SML.cpp b/libs/Common/SML.cpp new file mode 100644 index 0000000..56409fd --- /dev/null +++ b/libs/Common/SML.cpp @@ -0,0 +1,419 @@ +//////////////////////////////////////////////////////////////////// +// SML.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" +#include "SML.h" + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define SML_AUTOVALUES_OFF 0 +#define SML_AUTOVALUES_ON 1 +#ifndef SML_AUTOVALUES +#define SML_AUTOVALUES SML_AUTOVALUES_ON +#endif + +#define SML_SECTIONOPENBRACKET _T("{") +#define SML_SECTIONCLOSEBRACKET _T("}") +#define SML_NAMEOPENBRACKET _T("[") +#define SML_NAMECLOSEBRACKET _T("]") +#define SML_NAMETOKEN _T("=") +#define SML_VALUETOKEN _T("\n") +#define SML_INDENT _T("\t") + +#define SML_TOKEN_SECTIONOPENBRACKET SML_SECTIONOPENBRACKET[0] +#define SML_TOKEN_SECTIONCLOSEBRACKET SML_SECTIONCLOSEBRACKET[0] +#define SML_TOKEN_NAMEOPENBRACKET SML_NAMEOPENBRACKET[0] +#define SML_TOKEN_NAMECLOSEBRACKET SML_NAMECLOSEBRACKET[0] +#define SML_TOKEN_NAMETOKEN SML_NAMETOKEN[0] +#define SML_TOKEN_VALUETOKEN SML_VALUETOKEN[0] +#define SML_TOKEN_INDENT SML_INDENT[0] +#define SML_TOKEN_IGNORECHARS _T("\n\r\t ") + + +// S T R U C T S /////////////////////////////////////////////////// + +/*-----------------------------------------------------------* + * SML class implementation * + *-----------------------------------------------------------*/ + +/** + * Constructor + */ +SML::SML(const String& name) + : + m_strName(name), + m_fncInitItem(NULL), + m_fncSaveItem(NULL), + m_fncReleaseItem(NULL), + m_fncItemData(NULL) +{ +} + +/** + * Destructor + */ +SML::~SML() +{ + Release(); +} +/*----------------------------------------------------------------*/ + +/** + * Released all used memory for this nod and its children + */ +void SML::Release() +{ + m_arrChildren.ReleaseDelete(); + if (m_fncReleaseItem != NULL) { + for (SMLITEMMAP::iterator it=GetBegin(); it!=GetEnd(); ++it) + m_fncReleaseItem(it->second, m_fncItemData); + } + SMLITEMMAP::Release(); +} +/*----------------------------------------------------------------*/ + + +/** + * Load the values from a file; + * all children are generated if they do not exist + */ +bool SML::Load(const String& fileName) +{ + File oStream(fileName, File::READ, File::OPEN); + if (!oStream.isOpen()) + return false; + return Load(oStream); +} +bool SML::Load(ISTREAM& oStream) +{ + // create the section token filter and mem-file + MemFile memFile; + TokenIStream filter(&oStream); + filter.setTrimTokens(SML_TOKEN_IGNORECHARS); + // and parse section + return ParseSection(filter, memFile); +} +bool SML::ParseSection(TokenIStream& filter, MemFile& memFile) +{ + while (true) { + // find first section start (NameOpenBracket) + filter.setToken(SML_TOKEN_NAMEOPENBRACKET); + filter.readLine(memFile); + + // parse values before the new section or end of section + const size_f_t posMemFile = memFile.getPos(); + MemFile valuesMemFile; + TokenIStream sectionFilter(&memFile, SML_TOKEN_SECTIONCLOSEBRACKET); + const size_f_t lenValues = sectionFilter.readLine(valuesMemFile); + if (ParseValues(valuesMemFile) == false) + return false; // Parse Error: invalid values + ASSERT(valuesMemFile.getSize() == 0); + + // if end of section found, return + if (!sectionFilter.isEOS()) { + // if a name open bracket was found before the EOS, + // then restore it for the parent next ParseSection() + memFile.setPos(posMemFile+(lenValues+1)*sizeof(TCHAR)); + if (!filter.isEOS()) + filter.restoreToken(); + break; + } + else { + ASSERT(memFile.getSize()-posMemFile == lenValues*(size_f_t)sizeof(TCHAR)); + ASSERT(posMemFile == 0 || memFile.getSize()-posMemFile == memFile.getSizeLeft() || memFile.getSizeLeft() == 0); + memFile.setSize(0); + } + + // if no more data, return + if (filter.isEOS()) + break; + + // parse child section name + filter.setToken(SML_TOKEN_NAMECLOSEBRACKET); + const size_t lenName = filter.readLine(memFile); + ASSERT(!filter.isEOS()); + if (lenName == 0) + return false; // Parse Error: invalid section name + const String strChildName((LPCTSTR)memFile.getData(), lenName); + memFile.growSize(-((size_f_t)(lenName*sizeof(TCHAR)))); + ASSERT(memFile.getSize() == 0); + // create the child with the given name + const IDX idxChild = CreateChildUnique(strChildName); + LPSML const pChild = m_arrChildren[idxChild]; + pChild->SetFncItem(m_fncInitItem, m_fncSaveItem, m_fncReleaseItem, m_fncItemData); + + // parse child section + filter.setToken(SML_TOKEN_SECTIONOPENBRACKET); + filter.readLine(memFile); + filter.trimBackLine(memFile); + ASSERT(memFile.getSize() == 0); + if (pChild->ParseSection(filter, memFile) == false) + return false; + } + return true; +} +bool SML::ParseValues(MemFile& valuesMemFile) +{ + if (valuesMemFile.getSize() == 0) + return true; + // loop through all name/value pairs + MemFile memLine; + TokenIStream filterLine(&valuesMemFile); + filterLine.setTrimTokens(SML_TOKEN_IGNORECHARS); + filterLine.setToken(SML_TOKEN_VALUETOKEN); + MemFile memValue; + TokenIStream filterValue(&memLine); + filterValue.setTrimTokens(SML_TOKEN_IGNORECHARS); + filterValue.setToken(SML_TOKEN_NAMETOKEN); + do { + // parse value name and value + filterLine.readLine(memLine); + filterLine.trimBackLine(memLine); + filterLine.trimFrontLine(memLine); + if (memLine.isEOF()) + continue; // empty line, return + // parse value name + const size_t lenNameValueReal = (size_t)memLine.getSizeLeft(); + const size_t lenName = filterValue.readLine(memValue); + #if SML_AUTOVALUES == SML_AUTOVALUES_ON + String szName; + if (filterValue.trimBackLine(memValue) == lenName || lenNameValueReal == lenName) { + // no name found, auto generate the name + szName = _T("Item") + String::ToString(size()); + } else { + // read the name + szName = (LPCTSTR)memValue.getData(); + memValue.setSize(0); + } + #else + filterValue.trimBackLine(memValue); + String szName = (LPCTSTR)memValue.getData(); + ASSERT(!filterValue.isEOS() && !szName.IsEmpty()); + if (filterValue.isEOS() || szName.IsEmpty()) { + memValue.setSize(0); + memLine.setSize(0); + filterValue.setPos(0); + continue; // Parse Error: invalid syntax ('=' not found) + } + ASSERT(szName.size() == memValue.getSizeLeft()); + memValue.setSize(0); + #endif + SMLVALUE& val = operator[](szName); + // parse value + filterValue.read(memValue); + LPCTSTR szValue = filterValue.trimFrontLine(memValue); + val.val = szValue; + ASSERT((size_f_t)_tcslen(szValue) == memValue.getSizeLeft()); + memValue.setSize(0); + memLine.setSize(0); + filterValue.setPos(0); + } while (!filterLine.isEOS()); + // all file processed, safe to reset it to 0 + valuesMemFile.setSize(0); + return true; +} +/*----------------------------------------------------------------*/ + + +/** + * Write to a file the values of this node and its children. + * Set to false the second parameter in order not to save the empty children. + */ +bool SML::Save(const String& fileName, SAVEFLAG flags) const +{ + File oStream(fileName, File::WRITE, File::CREATE | File::TRUNCATE); + if (!oStream.isOpen()) + return false; + return Save(oStream, flags); +} +bool SML::Save(OSTREAM& oStream, SAVEFLAG flags) const +{ + // save all values and all children + return SaveIntern(oStream, _T(""), flags); +} +bool SML::SaveBracketize(OSTREAM& oStream, const String& strIndent, SAVEFLAG flags) const +{ + // save its name + oStream.print(_T("%s" SML_NAMEOPENBRACKET "%s" SML_NAMECLOSEBRACKET "\n"), strIndent.c_str(), m_strName.c_str()); + oStream.print(_T("%s" SML_SECTIONOPENBRACKET "\n"), strIndent.c_str()); + // save all values and all children + SaveIntern(oStream, strIndent+SML_TOKEN_INDENT, flags); + // close bracket + oStream.print(_T("%s" SML_SECTIONCLOSEBRACKET "\n"), strIndent.c_str()); + return true; +} +bool SML::SaveIntern(OSTREAM& oStream, const String& strIndent, SAVEFLAG flags) const +{ + const Flags& flgs = (const Flags&)flags; + if (flgs.isAnySet(SORT | SORTINV)) { + // sort values alphabetically and save them + StringArr lines(0, GetSize()); + for (SMLITEMMAP::const_iterator item=GetBegin(); item!=GetEnd(); ++item) + if (!m_fncSaveItem || m_fncSaveItem((*item).second, m_fncItemData)) + lines.InsertSort(String::FormatString(_T("%s%s " SML_NAMETOKEN " %s" SML_VALUETOKEN), strIndent.c_str(), (*item).first.c_str(), (*item).second.val.c_str()), + (flgs.isAnySet(SORT) ? String::CompareAlphabetically : String::CompareAlphabeticallyInv)); + FOREACH(l, lines) + oStream.print(lines[l]); + } else { + // save all values directly + for (SMLITEMMAP::const_iterator item=GetBegin(); item!=GetEnd(); ++item) + if (!m_fncSaveItem || m_fncSaveItem((*item).second, m_fncItemData)) + oStream.print(_T("%s%s " SML_NAMETOKEN " %s" SML_VALUETOKEN), strIndent.c_str(), (*item).first.c_str(), (*item).second.val.c_str()); + } + // save now all children + bool bFirst = IsEmpty(); + FOREACH(i, m_arrChildren) { + const SML* pSML = m_arrChildren[i]; + // skip empty children + if (!flgs.isSet(SAVEEMPTY) && pSML->IsEmpty() && pSML->m_arrChildren.IsEmpty()) + continue; + // insert a new line to separate from the above section (only for visual aspect) + if (bFirst) + bFirst = false; + else + oStream.print(_T("\n")); + // save child + pSML->SaveBracketize(oStream, strIndent, flags); + } + return true; +} +/*----------------------------------------------------------------*/ + + +/** + * Create and insert a new child + */ +IDX SML::CreateChild(const String& strChildName) +{ + return InsertChild(new SML(strChildName)); +} +IDX SML::CreateChildUnique(const String& strChildName) +{ + return InsertChildUnique(new SML(strChildName)); +} +/*----------------------------------------------------------------*/ + + +/** + * Insert a new child + */ +IDX SML::InsertChild(const LPSML pSML) +{ + return m_arrChildren.InsertSort(pSML, SML::Compare); +} +IDX SML::InsertChildUnique(const LPSML pSML) +{ + const std::pair res(m_arrChildren.InsertSortUnique(pSML, SML::Compare)); + if (res.second) + delete pSML; + return res.first; +} +/*----------------------------------------------------------------*/ + + +/** + * Remove an existing child + */ +void SML::RemoveChild(IDX idx) +{ + m_arrChildren.RemoveAtMove(idx); +} +/*----------------------------------------------------------------*/ + +/** + * Remove and destroy an existing child + */ +void SML::DestroyChild(IDX idx) +{ + delete m_arrChildren[idx]; + RemoveChild(idx); +} +/*----------------------------------------------------------------*/ + + +/** + * Retrieve an child by its name; NO_INDEX if unexistent + */ +IDX SML::GetChild(const String& name) const +{ + return m_arrChildren.FindFirst(&name, SML::CompareName); +} +/*----------------------------------------------------------------*/ + + +/** + * Retrieve an item; NULL if unexistent + */ +const SMLVALUE* SML::GetValue(const String& key) const +{ + const_iterator it = Find(key); + if (it == GetEnd()) + return NULL; + return &(it->second); +} +/*----------------------------------------------------------------*/ + + +/** + * Insert or retrieve an item; initialize it if necessary + */ +SMLVALUE& SML::GetValue(const String& key) +{ + bool bExisted; + SMLVALUE& val = (*Insert(key, bExisted)).second; + if (!bExisted && m_fncInitItem != NULL) + m_fncInitItem(key, val, m_fncItemData); + return val; +} +/*----------------------------------------------------------------*/ + + +/** + * Retrieve an unnamed item by index + */ +const SMLVALUE& SML::GetValue(IDX idx) const +{ + ASSERT(idx < this->size()); + const String szName(_T("Item") + String::ToString(idx)); + return this->at(szName); +} +/*----------------------------------------------------------------*/ + + +/** + * Reset items' init and release functions + */ +void SML::SetFncItem(TFncInitItem fncInit, TFncSaveItem fncSave, TFncReleaseItem fncRelease, void* data) +{ + m_fncInitItem = fncInit; + m_fncSaveItem = fncSave; + m_fncReleaseItem = fncRelease; + m_fncItemData = data; +} +/*----------------------------------------------------------------*/ + + +/** + * Compare two SML nodes by name + */ +int STCALL SML::Compare(const void* l, const void* r) +{ + return _tcscmp((*((const SML**)l))->GetName(), (*((const SML**)r))->GetName()); +} +/*----------------------------------------------------------------*/ + +/** + * Compare a name and a SML node name + */ +int STCALL SML::CompareName(const void* l, const void* r) +{ + return _tcscmp((*((const SML**)l))->GetName(), ((const String*)r)->c_str()); +} +/*----------------------------------------------------------------*/ diff --git a/libs/Common/SML.h b/libs/Common/SML.h new file mode 100644 index 0000000..c016d93 --- /dev/null +++ b/libs/Common/SML.h @@ -0,0 +1,116 @@ +//////////////////////////////////////////////////////////////////// +// SML.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_SML_H__ +#define __SEACAVE_SML_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Filters.h" + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +typedef struct SMLVALUE_TYPE { + String val; //item's value + void* data; //user data + SMLVALUE_TYPE() : data(NULL) {} + SMLVALUE_TYPE(const SMLVALUE_TYPE& r) : data(r.data) { ((SMLVALUE_TYPE&)r).data = NULL; } + ~SMLVALUE_TYPE() { delete data; } +} SMLVALUE; + +class SML; +typedef SML* LPSML; +typedef cList LPSMLARR; + +//typedef cHashTable SMLITEMMAP; +typedef cMapWrap SMLITEMMAP; + +/************************************************************************************** + * Simple Markup Language + * -------------- + * fast and easy to use markup language; + * contains a map of (name, value) pairs + **************************************************************************************/ + +class GENERAL_API SML : public SMLITEMMAP +{ +public: + typedef void (STCALL *TFncInitItem)(const String&, SMLVALUE&, void*); + typedef bool (STCALL *TFncSaveItem)(const SMLVALUE&, void*); + typedef void (STCALL *TFncReleaseItem)(SMLVALUE&, void*); + + enum SAVEFLAG { + NONE = 0, + SAVEEMPTY = (1 << 0), //save empty children + SORT = (1 << 1), //sort all entries alphabetically + SORTINV = (1 << 2), //sort all entries inverse alphabetically + }; + +public: + SML(const String& =String()); + ~SML(); + + void Release(); + + // main methods + bool Load(const String&); + bool Load(ISTREAM&); + bool Save(const String&, SAVEFLAG=NONE) const; + bool Save(OSTREAM&, SAVEFLAG=NONE) const; + IDX CreateChild(const String& =String()); + IDX CreateChildUnique(const String& =String()); + IDX InsertChild(const LPSML); + IDX InsertChildUnique(const LPSML); + void RemoveChild(IDX); + inline void RemoveChild(const String& name) { RemoveChild(GetChild(name)); } + void DestroyChild(IDX); + inline void DestroyChild(const String& name) { DestroyChild(GetChild(name)); } + IDX GetChild(const String&) const; + const SMLVALUE* GetValue(const String&) const; + SMLVALUE& GetValue(const String&); + const SMLVALUE& GetValue(IDX) const; + inline SMLVALUE& operator[] (const String& key) { return GetValue(key); } + + // misc methods + inline const String& GetName() const { return m_strName; } + inline LPSML GetChild(IDX idx) const { return m_arrChildren[idx]; } + inline const LPSMLARR& GetArrChildren() const { return m_arrChildren; } + inline LPSMLARR& GetArrChildren() { return m_arrChildren; } + void SetFncItem(TFncInitItem, TFncSaveItem, TFncReleaseItem, void* data=NULL); + +public: + static int STCALL Compare(const void*, const void*); + static int STCALL CompareName(const void*, const void*); + +private: + typedef TokenInputStream TokenIStream; + + bool ParseSection(TokenIStream&, MemFile&); + bool ParseValues(MemFile&); + bool SaveBracketize(OSTREAM&, const String&, SAVEFLAG) const; + bool SaveIntern(OSTREAM&, const String&, SAVEFLAG) const; + +private: + const String m_strName; // node name + LPSMLARR m_arrChildren; // the array with all the sub-nodes + TFncInitItem m_fncInitItem; // callback function used to initialize each item + TFncSaveItem m_fncSaveItem; // callback function used to save each item + TFncReleaseItem m_fncReleaseItem; // callback function used to release each item + void* m_fncItemData; +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_SML_H__ diff --git a/libs/Common/Sampler.inl b/libs/Common/Sampler.inl new file mode 100644 index 0000000..e22b21f --- /dev/null +++ b/libs/Common/Sampler.inl @@ -0,0 +1,274 @@ +// Copyright (c) 2012-2015 OpenMVG. +// Copyright (c) 2012-2015 Pierre MOULON. +// Copyright (c) 2015 Romuald Perrot. +// Copyright (c) 2015 cDc@seacave. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +namespace Sampler { + +// Sampling functors +// These functors computes weight associated to each pixels +// +// For a (relative) sampling position x (in [0,1]) between two (consecutive) points : +// +// A .... x ......... B +// +// w[0] is the weight associated to A +// w[1] is the weight associated to B +// +// Note: The following functors generalize the sampling to more than two neighbors +// They all contains the width variable that specify the number of neighbors used for sampling +// +// All contain the operator() with the following definition: +// +// @brief Computes weight associated to neighboring pixels +// @author Romuald Perrot +// @param x Sampling position +// @param[out] weight Sampling factors associated to the neighboring +// @note weight must be at least width length +// Linear sampling (ie: linear interpolation between two pixels) +template +struct Linear { + typedef TYPE Type; + + enum { halfWidth = 1 }; + enum { width = halfWidth*2 }; + + inline Linear() {} + + inline void operator() (const TYPE x, TYPE* const weight) const { + weight[0] = TYPE(1) - x; + weight[1] = x; + } +}; + +// Cubic interpolation between 4 pixels +// +// Interpolation weight is for A,B,C and D pixels given a x position as illustrated as follow : +// +// A B x C D +// +// Cubic Convolution Interpolation for Digital Image Processing , R. Keys, eq(4) +// +// The cubic filter family with the two free parameters usually named B and C: +// if |x|<1 +// ((12-9B-6C)|x|^3 + (-18+12B+6C)|x|^2 + (6-2B))/6 +// if 1<=|x|<2 +// ((-B-6C)|x|^3 + (6B+30C)|x|^2 + (-12B-48C)|x| + (8B+24C))/6 +template +struct Cubic { + typedef TYPE Type; + + enum { halfWidth = 2 }; + enum { width = halfWidth*2 }; + + // Sharpness coefficient used to control sharpness of the cubic curve (between 0.5 to 0.75) + // cubic(0,1/2): 0.5 gives better mathematically result (ie: approximation at 3 order precision - Catmull-Rom) + const TYPE sharpness; + inline Cubic(const TYPE& _sharpness=TYPE(0.5)) : sharpness(_sharpness) {} + + inline void operator() (const TYPE x, TYPE* const weight) const { + // remember : + // A B x C D + + // weight[0] -> weight for A + // weight[1] -> weight for B + // weight[2] -> weight for C + // weight[3] -> weight for D + + weight[0] = CubicInter12(x + TYPE(1)); + weight[1] = CubicInter01(x); + weight[2] = CubicInter01(TYPE(1) - x); + weight[3] = CubicInter12(TYPE(2) - x); + } + + // Cubic interpolation for x (in [0,1]) + inline TYPE CubicInter01(const TYPE x) const { + // A = -sharpness + // f(x) = (A + 2) * x^3 - (A + 3) * x^2 + 1 + return ((TYPE(2)-sharpness)*x - (TYPE(3)-sharpness))*x*x + TYPE(1); + } + // Cubic interpolation for x (in [1,2]) + inline TYPE CubicInter12(const TYPE x) const { + // A = -sharpness + // f(x) = A * x^3 - 5 * A * x^2 + 8 * A * x - 4 * A + return (((TYPE(5)-x)*x - TYPE(8))*x + TYPE(4))*sharpness; + } +}; + +// Sampler spline16 -> Interpolation on 4 points used for 2D ressampling (16 = 4x4 sampling) +// Cubic interpolation with 0-derivative at edges (ie at A and D points) +// See Helmut Dersch for more details +// +// Some refs : +// - http://forum.doom9.org/archive/index.php/t-147117.html +// - http://avisynth.nl/index.php/Resampling +// - http://www.ipol.im/pub/art/2011/g_lmii/ +// +// The idea is to consider 3 cubic splines (f1,f2,f3) in the sampling interval : +// +// A f1 B f2 C f3 D +// +// with curves defined as follow : +// f1(x) = a1 x^3 + b1 x^2 + c1 x + d1 +// f2(x) = a2 x^3 + b2 x^2 + c2 x + d2 +// f3(x) = a3 x^3 + b2 x^2 + c3 x + d3 +// +// We want to compute spline coefs for A,B,C,D assuming that: +// y0 = coef[A] = f1(-1) +// y1 = coef[B] = f1(0) = f2(0) +// y2 = coef[C] = f2(1) = f3(1) +// y3 = coef[D] = f3(2) +// +// coef are computed using the following constraints : +// Curve is continuous, ie: +// f1(0) = f2(0) +// f2(1) = f3(1) +// First derivative are equals, ie: +// f1'(0) = f2'(0) +// f2'(1) = f3'(1) +// Second derivative are equals, ie: +// f1''(0) = f2''(0) +// f2''(1) = f3''(0) +// Curve is, at boundary, with second derivative set to zero (it's a constraint introduced by Dersch), ie: +// f1''(-1) = 0 +// f3''(2) = 0 +// +// Then, you can solve for (a1,a2,a3,b1,b2,b3,c1,c2,c3,d1,d2,d3) +// +// for ex, for curve f2 you find : +// +// d2 = y1 // easy since y1 = f2(0) +// c2 = - 7/15 y0 - 1/5 y1 + 4/5 y2 - 2/15 y3 +// b2 = 4/5 y0 - 9/5 y1 + 6/5 y2 - 1/5 y3 +// a2 = - 1/3 y0 + y1 - y2 + 1/3 y3 +// +// +// When you have coefs, you just have to express your curve as a linear combination of the control points, fort ex +// with f2 : +// +// +// f2(x) = w0(x) * y0 + w1(x) + y1 + w2(x) * y2 + w3(x) * y3 +// +// with : +// +// w0(x) = - 1/3 * x^3 + 4/5 * x^2 - 7/15 * x +// w1(x) = x^3 - 9/5 * x^2 - 1/5 * x + 1 +// w2(x) = -x^3 + 6/5 * x^2 + 4/5 * x +// w3(x) = 1/3 * x^3 - 1/5 * x^2 - 2/15 * x +// +// substituting boundary conditions gives the correct coefficients for y0,y1,y2,y3 giving the final sampling scheme +template +struct Spline16 { + typedef TYPE Type; + + enum { halfWidth = 2 }; + enum { width = halfWidth*2 }; + + inline Spline16() {} + + inline void operator() (const TYPE x, TYPE* const weight) const { + weight[0] = ((TYPE(-1) / TYPE(3) * x + TYPE(4) / TYPE(5)) * x - TYPE(7) / TYPE(15)) * x; + weight[1] = ((x - TYPE(9) / TYPE(5)) * x - TYPE(1) / TYPE(5)) * x + TYPE(1); + weight[2] = ((TYPE(6) / TYPE(5) - x) * x + TYPE(4) / TYPE(5)) * x; + weight[3] = ((TYPE(1) / TYPE(3) * x - TYPE(1) / TYPE(5)) * x - TYPE(2) / TYPE(15)) * x; + } +}; + +// Sampler spline 36 +// Same as spline 16 but on 6 neighbors (used for 6x6 frame) +template +struct Spline36 { + typedef TYPE Type; + + enum { halfWidth = 3 }; + enum { width = halfWidth*2 }; + + inline Spline36() {} + + inline void operator() (const TYPE x, TYPE* const weight) const { + weight[0] = ((TYPE(1) / TYPE(11) * x - TYPE(45) / TYPE(209)) * x + TYPE(26) / TYPE(209)) * x; + weight[1] = ((TYPE(-6) / TYPE(11) * x + TYPE(270) / TYPE(209)) * x - TYPE(156) / TYPE(209)) * x; + weight[2] = ((TYPE(13) / TYPE(11) * x - TYPE(453) / TYPE(209)) * x - TYPE(3) / TYPE(209)) * x + TYPE(1); + weight[3] = ((TYPE(-13) / TYPE(11) * x + TYPE(288) / TYPE(209)) * x + TYPE(168) / TYPE(209)) * x; + weight[4] = ((TYPE(6) / TYPE(11) * x - TYPE(72) / TYPE(209)) * x - TYPE(42) / TYPE(209)) * x; + weight[5] = ((TYPE(-1) / TYPE(11) * x + TYPE(12) / TYPE(209)) * x + TYPE(7) / TYPE(209)) * x; + } +}; + +// Sampler spline 64 +// Same as spline 16 but on 8 neighbors (used for 8x8 frame) +template +struct Spline64 { + typedef TYPE Type; + + enum { halfWidth = 4 }; + enum { width = halfWidth*2 }; + + inline Spline64() {} + + inline void operator() (const TYPE x, TYPE* const weight) const { + weight[0] = ((TYPE(-1) / TYPE(41) * x + TYPE(168) / TYPE(2911)) * x - TYPE(97) / TYPE(2911)) * x; + weight[1] = ((TYPE(6) / TYPE(41) * x - TYPE(1008) / TYPE(2911)) * x + TYPE(582) / TYPE(2911)) * x; + weight[2] = ((TYPE(-24) / TYPE(41) * x + TYPE(4032) / TYPE(2911)) * x - TYPE(2328) / TYPE(2911)) * x; + weight[3] = ((TYPE(49) / TYPE(41) * x - TYPE(6387) / TYPE(2911)) * x - TYPE(3) / TYPE(2911)) * x + TYPE(1); + weight[4] = ((TYPE(-49) / TYPE(41) * x + TYPE(4050) / TYPE(2911)) * x + TYPE(2340) / TYPE(2911)) * x; + weight[5] = ((TYPE(24) / TYPE(41) * x - TYPE(1080) / TYPE(2911)) * x - TYPE(624) / TYPE(2911)) * x; + weight[6] = ((TYPE(-6) / TYPE(41) * x + TYPE(270) / TYPE(2911)) * x + TYPE(156) / TYPE(2911)) * x; + weight[7] = ((TYPE(1) / TYPE(41) * x - TYPE(45) / TYPE(2911)) * x - TYPE(26) / TYPE(2911)) * x; + } +}; + +// Sample image at a specified position +// @param image to be sampled +// @param sampler used to make the sampling +// @param pt X and Y-coordinate of sampling +// @return sampled value +template +inline TYPE Sample(const IMAGE& image, const SAMPLER& sampler, const POINT& pt) +{ + typedef typename SAMPLER::Type T; + + // integer position of sample (x,y) + const int grid_x(FLOOR2INT(pt.x)); + const int grid_y(FLOOR2INT(pt.y)); + + // compute difference between exact pixel location and sample + const T dx(pt.x-(T)grid_x); + const T dy(pt.y-(T)grid_y); + + // get sampler weights + T coefs_x[SAMPLER::width]; + sampler(dx, coefs_x); + T coefs_y[SAMPLER::width]; + sampler(dy, coefs_y); + + // Sample a grid around specified grid point + TYPE res(0); + for (int i = 0; i < SAMPLER::width; ++i) { + // get current i value + // +1 for correct scheme (draw it to be convinced) + const int cur_i(grid_y + 1 + i - SAMPLER::halfWidth); + // handle out of range + if (cur_i < 0 || cur_i >= image.rows) + continue; + for (int j = 0; j < SAMPLER::width; ++j) { + // get current j value + // +1 for the same reason + const int cur_j(grid_x + 1 + j - SAMPLER::halfWidth); + // handle out of range + if (cur_j < 0 || cur_j >= image.cols) + continue; + // sample input image and weight according to sampler + const T w = coefs_x[j] * coefs_y[i]; + const TYPE pixel = image(cur_i, cur_j); + res += pixel * w; + } + } + return res; +} + +} // namespace Sampler diff --git a/libs/Common/Semaphore.h b/libs/Common/Semaphore.h new file mode 100644 index 0000000..1281c51 --- /dev/null +++ b/libs/Common/Semaphore.h @@ -0,0 +1,170 @@ +//////////////////////////////////////////////////////////////////// +// Semaphore.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_SEMAPHORE_H__ +#define __SEACAVE_SEMAPHORE_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Thread.h" + +#ifndef _MSC_VER +#ifdef _SUPPORT_CPP11 +#include +#include +#else +#include "CriticalSection.h" +#include +#endif +#endif + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class Semaphore +{ +#ifdef _MSC_VER +public: + Semaphore(unsigned count=0) { + h = CreateSemaphore(NULL, count, MAXLONG, NULL); + }; + ~Semaphore() { + CloseHandle(h); + }; + + void Clear(unsigned count=0) { + CloseHandle(h); + h = CreateSemaphore(NULL, count, MAXLONG, NULL); + } + + void Signal() { + ReleaseSemaphore(h, 1, NULL); + } + void Signal(unsigned count) { + ASSERT(count > 0); + ReleaseSemaphore(h, count, NULL); + } + + void Wait() { + WaitForSingleObject(h, INFINITE); + } + bool Wait(uint32_t millis) { + return WaitForSingleObject(h, millis) == WAIT_OBJECT_0; + } + +protected: + HANDLE h; + +#elif !defined(_SUPPORT_CPP11) +// pthread implementation +public: + Semaphore(unsigned c=0) : count(c) { pthread_cond_init(&cond, NULL); } + ~Semaphore() { pthread_cond_destroy(&cond); } + + void Clear(unsigned c=0) { + cs.Clear(); + pthread_cond_destroy(&cond); + pthread_cond_init(&cond, NULL); + count = c; + } + + void Signal() { + Lock l(cs); + ++count; + pthread_cond_signal(&cond); + } + void Signal(unsigned c) { + ASSERT(c > 0); + for (unsigned i=0; i lock{mtx}; + count = c; + } + + void Signal() { + std::lock_guard lock{mtx}; + ++count; + cv.notify_one(); + } + void Signal(unsigned c) { + ASSERT(c > 0); + for (unsigned i=0; i lock{mtx}; + cv.wait(lock, [&] { return count > 0; }); + --count; + } + bool Wait(uint32_t millis) { + std::unique_lock lock{mtx}; + if (!cv.wait_for(lock, std::chrono::milliseconds(millis), [&] { return count > 0; })) + return false; + --count; + return true; + } + +protected: + std::condition_variable cv; + std::mutex mtx; + unsigned count; +#endif + +private: + Semaphore(const Semaphore&); + Semaphore& operator=(const Semaphore&); +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_SEMAPHORE_H__ diff --git a/libs/Common/SharedPtr.h b/libs/Common/SharedPtr.h new file mode 100644 index 0000000..3488802 --- /dev/null +++ b/libs/Common/SharedPtr.h @@ -0,0 +1,209 @@ +//////////////////////////////////////////////////////////////////// +// SharedPtr.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_SHAREDPTR_H__ +#define __SEACAVE_SHAREDPTR_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +/************************************************************************************** + * CSharedPtr template + * --------------- + * shared smart pointer + **************************************************************************************/ + +template +struct SharedRef { + typedef TYPE TYPE_REF; + + #ifdef SEACAVE_NO_MULTITHREAD + TYPE_REF val; + #else + volatile TYPE_REF val; + #endif + + inline SharedRef() {} + inline SharedRef(TYPE_REF v) : val(v) {} + + inline TYPE_REF Inc() { + #ifdef SEACAVE_NO_MULTITHREAD + ASSERT(val >= 0); + return ++val; + #else + return Thread::safeInc(val); + #endif + } + inline TYPE_REF Dec() { + #ifdef SEACAVE_NO_MULTITHREAD + ASSERT(val > 0); + return --val; + #else + return Thread::safeDec(val); + #endif + } + + #ifdef _USE_BOOST + // serialize + template + void serialize(Archive& ar, const unsigned int /*version*/) { + #ifdef SEACAVE_NO_MULTITHREAD + ar & val; + #else + ar & (TYPE_REF&)val; + #endif + } + #endif +}; +/*----------------------------------------------------------------*/ + + +template +class CSharedPtr +{ +protected: + typedef SharedRef TYPE_REF; + typedef TYPE* TYPE_PTR; + +public: + inline CSharedPtr() : m_pointer(NULL), m_pNoRef(NULL) + { // empty construct + } + + inline explicit CSharedPtr(TYPE_PTR _Ptr) : m_pointer(_Ptr), m_pNoRef(_Ptr ? new TYPE_REF(1) : NULL) + { // construct from object pointer + } + + inline CSharedPtr(const CSharedPtr& _Right) : m_pointer(_Right.m_pointer), m_pNoRef(_Right.m_pNoRef) + { // construct by assuming pointer from _Right CSharedPtr + IncRef(); + } + + inline ~CSharedPtr() + { // destroy the object + DecRef(); + } + + CSharedPtr& operator=(const CSharedPtr& _Right) + { // assign compatible _Right (assume pointer) + if (this != &_Right) + { + ASSERT(m_pointer != _Right.m_pointer || m_pNoRef == _Right.m_pNoRef); + DecRef(); + m_pointer = _Right.m_pointer; + m_pNoRef = _Right.m_pNoRef; + IncRef(); + } + return (*this); + } + + CSharedPtr& operator=(TYPE_PTR _Ptr) + { // assign compatible _Right (assume pointer) + if (m_pointer != _Ptr) + { + DecRef(); + m_pointer = _Ptr; + m_pNoRef = (_Ptr ? new TYPE_REF(1) : NULL); + } + return (*this); + } + + inline TYPE& operator*() const + { // return designated value + ASSERT(m_pointer); + return (*m_pointer); + } + + inline TYPE* operator->() const + { // return pointer to class object + ASSERT(m_pointer); + return m_pointer; + } + + inline operator TYPE_PTR() const + { // return pointer to class object + return m_pointer; + } + + inline bool operator==(const CSharedPtr& _Right) const + { // return pointer to class object + return (m_pointer == _Right.m_pointer); + } + + inline bool operator!=(const CSharedPtr& _Right) const + { // return pointer to class object + return (m_pointer != _Right.m_pointer); + } + + inline bool operator==(const void* _Right) const + { // return pointer to class object + return (m_pointer == _Right); + } + + inline bool operator!=(const void* _Right) const + { // return pointer to class object + return (m_pointer != _Right); + } + + void Release() + { // release pointer + DecRef(); + m_pointer = NULL; + m_pNoRef = NULL; + } + +protected: + inline void IncRef() + { + if (m_pointer == NULL) + return; + ASSERT(m_pNoRef); + m_pNoRef->Inc(); + } + + inline void DecRef() + { + if (m_pointer == NULL) + return; + ASSERT(m_pNoRef); + if (m_pNoRef->Dec() == 0) + { + delete m_pointer; + m_pointer = NULL; + delete m_pNoRef; + m_pNoRef = NULL; + } + } + + TYPE_PTR m_pointer; // the wrapped object pointer + TYPE_REF* m_pNoRef; // number of references to this pointer + +#ifdef _USE_BOOST +protected: + // implement BOOST serialization + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & m_pointer; + ar & m_pNoRef; + } +#endif +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_SHAREDPTR_H__ diff --git a/libs/Common/Sphere.h b/libs/Common/Sphere.h new file mode 100644 index 0000000..f5d79e5 --- /dev/null +++ b/libs/Common/Sphere.h @@ -0,0 +1,56 @@ +//////////////////////////////////////////////////////////////////// +// Sphere.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_SPHERE_H__ +#define __SEACAVE_SPHERE_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +// Basic sphere class +template +class TSphere +{ + STATIC_ASSERT(DIMS > 1 && DIMS <= 3); + +public: + typedef TYPE Type; + typedef Eigen::Matrix POINT; + + POINT center; // sphere center point + TYPE radius; // sphere radius + + //--------------------------------------- + + inline TSphere() {} + inline TSphere(const POINT& c, TYPE r) : center(c), radius(r) {} + inline TSphere(const POINT& p1, const POINT& p2, const POINT& p3); + + inline void Set(const POINT& c, TYPE r); + inline void Set(const POINT& p1, const POINT& p2, const POINT& p3); + + inline void Enlarge(TYPE); + inline void EnlargePercent(TYPE); + + inline GCLASS Classify(const POINT&) const; +}; // class TSphere +/*----------------------------------------------------------------*/ + +#include "Sphere.inl" +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_SPHERE_H__ diff --git a/libs/Common/Sphere.inl b/libs/Common/Sphere.inl new file mode 100644 index 0000000..6a46826 --- /dev/null +++ b/libs/Common/Sphere.inl @@ -0,0 +1,80 @@ +//////////////////////////////////////////////////////////////////// +// Sphere.inl +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +template +inline TSphere::TSphere(const POINT& p1, const POINT& p2, const POINT& p3) +{ + Set(p1, p2, p3); +} // constructor +/*----------------------------------------------------------------*/ + + +template +inline void TSphere::Set(const POINT& c, TYPE r) +{ + center = c; + radius = r; +} +template +inline void TSphere::Set(const POINT& p1, const POINT& p2, const POINT& p3) +{ + // compute relative distances + TYPE A((p1 - p2).squaredNorm()); + TYPE B((p2 - p3).squaredNorm()); + TYPE C((p3 - p1).squaredNorm()); + + // re-orient triangle (make A longest side) + const POINT *a(&p3), *b(&p1), *c(&p2); + if (B < C) std::swap(B, C), std::swap(b, c); + if (A < B) std::swap(A, B), std::swap(a, b); + + // if obtuse + if (B + C <= A) { + // just use longest diameter + radius = SQRT(A) / TYPE(2); + center = (*b + *c) / TYPE(2); + } else { + // otherwise circumscribe (http://en.wikipedia.org/wiki/Circumscribed_circle) + const TYPE cos_a(SQUARE(B + C - A) / (B*C*TYPE(4))); + radius = SQRT(A / ((TYPE(1) - cos_a)*TYPE(4))); + const POINT alpha(*a - *c), beta(*b - *c); + const POINT alphaXbeta(alpha.cross(beta)); + center = (beta * alpha.squaredNorm() - alpha * beta.squaredNorm()).cross(alphaXbeta) / + (alphaXbeta.squaredNorm() * TYPE(2)) + *c; + } +} +/*----------------------------------------------------------------*/ + + +template +inline void TSphere::Enlarge(TYPE x) +{ + radius += x; +} +template +inline void TSphere::EnlargePercent(TYPE x) +{ + radius *= x; +} // Enlarge +/*----------------------------------------------------------------*/ + + +// Classify point to sphere. +template +inline GCLASS TSphere::Classify(const POINT& p) const +{ + if ((center - p).squaredNorm() > SQUARE(radius)) + return CULLED; + return VISIBLE; +} +/*----------------------------------------------------------------*/ diff --git a/libs/Common/Streams.h b/libs/Common/Streams.h new file mode 100644 index 0000000..3c02237 --- /dev/null +++ b/libs/Common/Streams.h @@ -0,0 +1,244 @@ +//////////////////////////////////////////////////////////////////// +// Streams.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_STREAMS_H__ +#define __SEACAVE_STREAMS_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "AutoPtr.h" + + +// D E F I N E S /////////////////////////////////////////////////// + +#define SIZE_NA ((size_f_t)-1) +#define STREAM_ERROR ((size_t)-1) + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class InputStream; +typedef InputStream ISTREAM; + +class OutputStream; +typedef OutputStream OSTREAM; + +class IOStream; +typedef IOStream IOSTREAM; + +class GENERAL_API NOINITVTABLE Stream { +public: + virtual ~Stream() { } + + // Identify stream type + virtual InputStream* getInputStream(int) { return NULL; } + virtual OutputStream* getOutputStream(int) { return NULL; } + virtual IOStream* getIOStream(int, int) { return NULL; } + + // Get the length of the stream. + // SIZE_NA if there were errors. + virtual size_f_t getSize() const = 0; + // Position the stream at the given offset from the beginning. + virtual bool setPos(size_f_t pos) = 0; + // Get the current position in the stream (offset from the beginning). + // SIZE_NA if there were errors. + virtual size_f_t getPos() const = 0; +}; + +class GENERAL_API NOINITVTABLE InputStream : public Stream { +public: + virtual ~InputStream() { } + /** + * Call this function until it returns 0 to get all bytes. + * @return The number of bytes read. len reflects the number of bytes + * actually read from the stream source in this call. + * STREAM_ERROR if there were errors. + */ + virtual size_t read(void* buf, size_t len) = 0; + + enum { LAYER_ID_IN=0 }; + InputStream* getInputStream(int typ=LAYER_ID_IN) override { return (typ == LAYER_ID_IN ? this : (InputStream*)NULL); } +}; + +class GENERAL_API NOINITVTABLE OutputStream : public Stream { +public: + virtual ~OutputStream() { } + + /** + * @return The actual number of bytes written. len bytes will always be + * consumed, but fewer or more bytes may actually be written, + * for example if the stream is being compressed. + * STREAM_ERROR if there were errors. + */ + virtual size_t write(const void* buf, size_t len) = 0; + inline size_t print(LPCTSTR szFormat, ...) { + va_list args; + va_start(args, szFormat); + TCHAR szBuffer[2048]; + const size_t len((size_t)_vsntprintf(szBuffer, 2048, szFormat, args)); + if (len > 2048) { + const size_t count((size_t)_vsctprintf(szFormat, args)); + ASSERT(count != (size_t)-1); + CAutoPtrArr szBufferDyn(new TCHAR[count+1]); + _vsntprintf(szBufferDyn, count+1, szFormat, args); + va_end(args); + return write(szBufferDyn, count); + } + va_end(args); + return write(szBuffer, len); + } + inline size_t print(const std::string& str) { + return write(str.c_str(), str.length()); + } + template + inline OutputStream& operator<<(const T& val) { + std::ostringstream ostr; + ostr << val; + print(ostr.str()); + return *this; + } + /** + * This must be called before destroying the object to make sure all data + * is properly written. Note that some implementations + * might not need it... + * + * @return The actual number of bytes written. + * STREAM_ERROR if there were errors. + */ + virtual size_t flush() = 0; + + enum { LAYER_ID_OUT=0 }; + OutputStream* getOutputStream(int typ=LAYER_ID_OUT) override { return (typ == LAYER_ID_OUT ? this : (OutputStream*)NULL); } +}; + +class GENERAL_API NOINITVTABLE IOStream : public InputStream, public OutputStream { +public: + InputStream* getInputStream(int typ=LAYER_ID_IN) override { return InputStream::getInputStream(typ); } + OutputStream* getOutputStream(int typ=LAYER_ID_OUT) override { return OutputStream::getOutputStream(typ); } + IOStream* getIOStream(int typIn=LAYER_ID_IN, int typOut=LAYER_ID_OUT) override { + return ((InputStream*)this)->getInputStream(typIn) != NULL && + ((OutputStream*)this)->getOutputStream(typOut) != NULL ? this : (IOStream*)NULL; + } +}; +/*----------------------------------------------------------------*/ + + + +template +class LayerInputStream : public InputStream { +public: + LayerInputStream(InputStream* aStream) : s(aStream) { ASSERT(s != NULL); } + virtual ~LayerInputStream() noexcept { if (managed) delete s; } + + InputStream* getInputStream(int typ=InputStream::LAYER_ID_IN) override { return s->getInputStream(typ); } + + size_t read(void* wbuf, size_t len) override { + return s->read(wbuf, len); + } + + size_f_t getSize() const override { + return s->getSize(); + } + + bool setPos(size_f_t wpos) override { + return s->setPos(wpos); + } + + size_f_t getPos() const override { + return s->getPos(); + } + +protected: + InputStream* const s; +}; + +template +class LayerOutputStream : public OutputStream { +public: + LayerOutputStream(OutputStream* aStream) : s(aStream) {} + virtual ~LayerOutputStream() noexcept { if (managed) delete s; } + + OutputStream* getOutputStream(int typ=OutputStream::LAYER_ID_OUT) override { return s->getOutputStream(typ); } + + size_t write(const void* buf, size_t len) override { + return s->write(buf, len); + } + + size_t flush() override { + return s->flush(); + } + + size_f_t getSize() const override { + return s->getSize(); + } + + bool setPos(size_f_t wpos) override { + return s->setPos(wpos); + } + + size_f_t getPos() const override { + return s->getPos(); + } + +protected: + OutputStream* const s; +}; +/*----------------------------------------------------------------*/ + + + +template +class LayerIOStream : public IOStream { +public: + LayerIOStream(InputStream* aStream) : s(aStream), sIn(aStream), sOut(NULL) { ASSERT(aStream != NULL); } + LayerIOStream(OutputStream* aStream) : s(aStream), sIn(NULL), sOut(aStream) { ASSERT(aStream != NULL); } + LayerIOStream(InputStream* streamIn, OutputStream* streamOut) : s(NULL), sIn(streamIn), sOut(streamOut) { ASSERT(sIn != NULL || sOut != NULL); } + virtual ~LayerIOStream() noexcept { if (managed) { delete sIn; delete sOut; } } + + InputStream* getInputStream(int typ=InputStream::LAYER_ID_IN) override { return (sIn ? sIn->getInputStream(typ) : NULL); } + OutputStream* getOutputStream(int typ=OutputStream::LAYER_ID_OUT) override { return (sOut ? sOut->getOutputStream(typ) : NULL); } + + size_f_t getSize() const override { + return s->getSize(); + } + + bool setPos(size_f_t wpos) override { + return s->setPos(wpos); + } + + size_f_t getPos() const override { + return s->getPos(); + } + + size_t read(void* wbuf, size_t len) override { + return sIn->read(wbuf, len); + } + + size_t write(const void* buf, size_t len) override { + return sOut->write(buf, len); + } + + size_t flush() override { + return sOut->flush(); + } + + operator InputStream* () const { return sIn; } + operator OutputStream* () const { return sOut; } + +protected: + Stream* const s; + InputStream* const sIn; + OutputStream* const sOut; +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_STREAMS_H__ diff --git a/libs/Common/Strings.h b/libs/Common/Strings.h new file mode 100644 index 0000000..aae49c8 --- /dev/null +++ b/libs/Common/Strings.h @@ -0,0 +1,239 @@ +//////////////////////////////////////////////////////////////////// +// Strings.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_STRING_H__ +#define __SEACAVE_STRING_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Streams.h" + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +/// String class: enhanced std::string +class GENERAL_API String : public std::string +{ +public: + typedef std::string Base; + +public: + inline String() {} + inline String(LPCTSTR sz) : Base(sz) {} + inline String(const Base& str) : Base(str) {} + inline String(size_t n, value_type v) : Base(n, v) {} + inline String(LPCTSTR sz, size_t count) : Base(sz, count) {} + inline String(LPCTSTR sz, size_t offset, size_t count) : Base(sz, offset, count) {} + #ifdef _SUPPORT_CPP11 + inline String(Base&& rhs) : Base(std::forward(rhs)) {} + inline String(String&& rhs) : Base(std::forward(rhs)) {} + inline String(const String& rhs) : Base(rhs) {} + + inline String& operator=(Base&& rhs) { Base::operator=(std::forward(rhs)); return *this; } + inline String& operator=(String&& rhs) { Base::operator=(std::forward(rhs)); return *this; } + #endif + inline String& operator=(TCHAR rhs) { Base::operator=(rhs); return *this; } + inline String& operator=(LPCTSTR rhs) { Base::operator=(rhs); return *this; } + inline String& operator=(const String& rhs) { Base::operator=(rhs); return *this; } + + inline String& operator+=(TCHAR rhs) { *this = (*this) + rhs; return *this; } + inline String& operator+=(LPCTSTR rhs) { *this = (*this) + rhs; return *this; } + inline String& operator+=(const Base& rhs) { *this = (*this) + rhs; return *this; } + inline String& operator+=(const String& rhs) { *this = (*this) + rhs; return *this; } + + inline void Release() { return clear(); } + inline bool IsEmpty() const { return empty(); } + + inline operator LPCTSTR() const { return c_str(); } + + String& Format(LPCTSTR szFormat, ...) { + va_list args; + va_start(args, szFormat); + TCHAR szBuffer[2048]; + const size_t len((size_t)_vsntprintf(szBuffer, 2048, szFormat, args)); + if (len > 2048) { + *this = FormatStringSafe(szFormat, args); + va_end(args); + } else { + va_end(args); + this->assign(szBuffer, len); + } + return *this; + } + String& FormatSafe(LPCTSTR szFormat, ...) { + va_list args; + va_start(args, szFormat); + const size_t len((size_t)_vsctprintf(szFormat, args)); + ASSERT(len != (size_t)-1); + TCHAR* szBuffer(new TCHAR[len]); + _vsntprintf(szBuffer, len, szFormat, args); + va_end(args); + this->assign(szBuffer, len); + delete[] szBuffer; + return *this; + } + static String FormatString(LPCTSTR szFormat, ...) { + va_list args; + va_start(args, szFormat); + TCHAR szBuffer[2048]; + const size_t len((size_t)_vsntprintf(szBuffer, 2048, szFormat, args)); + if (len > 2048) { + const String str(FormatStringSafe(szFormat, args)); + va_end(args); + return str; + } + va_end(args); + return String(szBuffer, len); + } + static inline String FormatStringSafe(LPCTSTR szFormat, va_list args) { + const size_t len((size_t)_vsctprintf(szFormat, args)); + ASSERT(len != (size_t)-1); + TCHAR* szBuffer(new TCHAR[len]); + _vsntprintf(szBuffer, len, szFormat, args); + String str(szBuffer, len); + delete[] szBuffer; + return str; + } + + inline void ToUpper(String& out) const { + out.resize(size()); + std::transform(begin(), end(), out.begin(), [](TCHAR c) { return (TCHAR)std::toupper(c); }); + } + inline String ToUpper() const { + String str; + ToUpper(str); + return str; + } + + inline void ToLower(String& out) const { + out.resize(size()); + std::transform(begin(), end(), out.begin(), [](TCHAR c) { return (TCHAR)std::tolower(c); }); + } + inline String ToLower() const { + String str; + ToLower(str); + return str; + } + + inline void Save(OSTREAM& oStream) const { + const WORD nSize = (WORD)size(); + oStream.write(&nSize, sizeof(WORD)); + oStream.write(c_str(), nSize); + } + inline void Load(ISTREAM& oStream) { + WORD nSize; + oStream.read(&nSize, sizeof(WORD)); + if (nSize == 0) { + clear(); + return; + } + char* pBuffer = new char[nSize]; + oStream.read(pBuffer, nSize); + assign(pBuffer, nSize); + delete[] pBuffer; + } + + template + static String ToString(const T& val) { + std::ostringstream os; + os << val; + return os.str(); + } + template + static String ToStringHex(const T& val) { + std::ostringstream os; + os << std::hex << val; + return os.str(); + } + + template + static void FromString(const String& str, T& val) { + std::istringstream is(str); + is >> val; + } + template + static T FromString(const String& str, const T& def) { + T val(def); + FromString(str, val); + return val; + } + template + static T FromString(const String& str) { + T val; + FromString(str, val); + return val; + } + template + inline void From(T& val) const { + FromString(*this, val); + } + template + inline T From(const T& def) const { + T val(def); + From(val); + return val; + } + template + inline T From() const { + T val; + From(val); + return val; + } + + static int CompareAlphabetically(const void* elem, const void* key) { return _tcscmp(((const String*)elem)->c_str(), ((const String*)key)->c_str()); } + static int CompareAlphabeticallyInv(const void* elem, const void* key) { return _tcscmp(((const String*)key)->c_str(), ((const String*)elem)->c_str()); } + +#ifdef _USE_BOOST +protected: + // implement BOOST serialization + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & boost::serialization::base_object(*this); + } +#endif +}; +/*----------------------------------------------------------------*/ + +inline String operator+(const String& lhs, TCHAR rhs) { return std::operator+(lhs, rhs); } +inline String operator+(const String& lhs, LPCTSTR rhs) { return std::operator+(lhs, rhs); } +inline String operator+(const String& lhs, const std::string& rhs) { return std::operator+(lhs, rhs); } +inline String operator+(TCHAR lhs, const String& rhs) { return std::operator+(lhs, rhs); } +inline String operator+(LPCTSTR lhs, const String& rhs) { return std::operator+(lhs, rhs); } +inline String operator+(const std::string& lhs, const String& rhs) { return std::operator+(lhs, rhs); } +inline String operator+(const String& lhs, const String& rhs) { return std::operator+(lhs, rhs); } +#ifdef _SUPPORT_CPP11 +inline String operator+(String&& lhs, TCHAR rhs) { return std::operator+(std::forward(lhs), rhs); } +inline String operator+(String&& lhs, LPCTSTR rhs) { return std::operator+(std::forward(lhs), rhs); } +inline String operator+(String&& lhs, const std::string& rhs) { return std::operator+(std::forward(lhs), rhs); } +inline String operator+(TCHAR lhs, String&& rhs) { return std::operator+(lhs, std::forward(rhs)); } +inline String operator+(LPCTSTR lhs, String&& rhs) { return std::operator+(lhs, std::forward(rhs)); } +inline String operator+(const std::string& lhs, String&& rhs) { return std::operator+(lhs, std::forward(rhs)); } +inline String operator+(const String& lhs, String&& rhs) { return std::operator+(lhs, std::forward(rhs)); } +inline String operator+(String&& lhs, const String& rhs) { return std::operator+(std::forward(lhs), rhs); } +inline String operator+(String&& lhs, String&& rhs) { return std::operator+(std::forward(lhs), std::forward(rhs)); } +#endif +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + + +namespace std { +//namespace tr1 { +// Specializations for unordered containers +template <> struct hash : public hash{}; +//} // namespace tr1 +template <> struct equal_to : public equal_to{}; +} // namespace std + +#endif // __SEACAVE_STRING_H__ diff --git a/libs/Common/Thread.h b/libs/Common/Thread.h new file mode 100644 index 0000000..9ccacb3 --- /dev/null +++ b/libs/Common/Thread.h @@ -0,0 +1,435 @@ +//////////////////////////////////////////////////////////////////// +// Thread.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_THREAD_H__ +#define __SEACAVE_THREAD_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#ifdef _MSC_VER +#include +#ifdef _SUPPORT_CPP11 +#include +#else +#include +#endif +#else +#include +#include +#include +#include +#endif + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +/************************************************************************************** + * Thread + * -------------- + * basic thread control + **************************************************************************************/ + +class GENERAL_API Thread +{ +public: + #ifdef _ENVIRONMENT64 + typedef int64_t safe_t; + #else + typedef int32_t safe_t; + #endif + + enum Priority { + IDLE = -2, + LOW = -1, + NORMAL = 0, + HIGH = 1 + }; + + #ifdef _MSC_VER + + typedef HANDLE thread_t; + typedef void* (STCALL *FncStart)(void*); + + typedef struct STARTER_DATA { + FncStart fnStarter; + void* pData; + STARTER_DATA(FncStart _fnStarter, void* _pData) : fnStarter(_fnStarter), pData(_pData) {} + } StarterData; + + Thread() : threadHandle(NULL), threadId(0) {} + virtual ~Thread() { stop(); } + + inline bool isRunning() const { return (threadHandle != NULL); } + + bool start(FncStart pfnStarter, void* pData=NULL) { + join(); + return ((threadHandle = CreateThread(NULL, 0, &starterConvertor, new StarterData(pfnStarter, pData), 0, &threadId)) != NULL); + } + inline bool start() { + return start(&starter, this); + } + void stop() { + if (threadHandle) { + TerminateThread(threadHandle, -1); + CloseHandle(threadHandle); + threadHandle = NULL; + threadId = 0; + } + } + void join() { + if (threadHandle == NULL) + return; + WaitForSingleObject(threadHandle, INFINITE); + CloseHandle(threadHandle); + threadHandle = NULL; + threadId = 0; + } + + inline void setThreadPriority(Priority p) const { ::SetThreadPriority(threadHandle, convertPriority(p)); } + inline Priority getThreadPriority() const { return convertPriority((PriorityOS)::GetThreadPriority(threadHandle)); } + static inline void setThreadPriority(thread_t th, Priority p) { ::SetThreadPriority(th, convertPriority(p)); } + static inline Priority getThreadPriority(thread_t th) { return convertPriority((PriorityOS)::GetThreadPriority(th)); } + + static inline void sleep(uint32_t millis) { ::Sleep(millis); } + static inline void yield() { ::Sleep(0); } + static inline thread_t currentThread() { return ::GetCurrentThread(); } + static uint32_t hardwareConcurrency() { + SYSTEM_INFO info={{0}}; + GetSystemInfo(&info); + return info.dwNumberOfProcessors; + } + + STATIC_ASSERT(sizeof(int32_t)==sizeof(LONG)); + static inline int32_t safeInc(volatile int32_t& v) { return InterlockedIncrement((volatile LONG*)&v); }; + static inline int32_t safeDec(volatile int32_t& v) { return InterlockedDecrement((volatile LONG*)&v); }; + static inline int32_t safeExchange(volatile int32_t& target, int32_t value) { return InterlockedExchange((volatile LONG*)&target, value); }; + static inline int32_t safeCompareExchange(volatile int32_t& target, int32_t comp, int32_t value) { return InterlockedCompareExchange((volatile LONG*)&target, value, comp); }; + + STATIC_ASSERT(sizeof(int64_t)==sizeof(LONGLONG)); + static inline int64_t safeInc(volatile int64_t& v) { return InterlockedIncrement64((volatile LONGLONG*)&v); }; + static inline int64_t safeDec(volatile int64_t& v) { return InterlockedDecrement64((volatile LONGLONG*)&v); }; + static inline int64_t safeExchange(volatile int64_t& target, int64_t value) { return InterlockedExchange64((volatile LONGLONG*)&target, value); }; + static inline int64_t safeCompareExchange(volatile int64_t& target, int64_t comp, int64_t value) { return InterlockedCompareExchange64((volatile LONGLONG*)&target, value, comp); }; + + #else //_MSC_VER + + typedef pthread_t thread_t; + typedef void* (STCALL *FncStart)(void*); + + Thread() : threadHandle(0) {} + virtual ~Thread() { stop(); } + + inline bool isRunning() const { return (threadHandle != 0); } + + bool start(FncStart pfnStarter, void* pData=NULL) { + join(); + return (pthread_create(&threadHandle, NULL, pfnStarter, pData) == 0); + } + bool start() { + return start(&starter, this); + } + void stop() { + if (threadHandle != 0) { + pthread_detach(threadHandle); + threadHandle = 0; + } + } + void join() { + if (threadHandle) { + pthread_join(threadHandle, 0); + threadHandle = 0; + } + } + + inline void setThreadPriority(Priority p) const { setThreadPriority(threadHandle, p); } + inline Priority getThreadPriority() const { return getThreadPriority(threadHandle); } + static void setThreadPriority(thread_t th, Priority p) { + struct sched_param param; + param.sched_priority = convertPriority(p); + pthread_setschedparam(th, SCHED_OTHER, ¶m); + } + static Priority getThreadPriority(thread_t th) { + struct sched_param param; + int policy; + pthread_getschedparam(th, &policy, ¶m); + return convertPriority((PriorityOS)param.sched_priority); + } + + static void sleep(uint32_t millis) { ::usleep(millis*1000); } + static void yield() { ::sched_yield(); } + static inline thread_t currentThread(){ return pthread_self(); } + static uint32_t hardwareConcurrency() { return sysconf(_SC_NPROCESSORS_ONLN); } + + static inline int32_t safeInc(volatile int32_t& v) { return __sync_add_and_fetch(&v, 1); } + static inline int32_t safeDec(volatile int32_t& v) { return __sync_sub_and_fetch(&v, 1); } + static inline int32_t safeExchange(volatile int32_t& target, int32_t value) { return __sync_val_compare_and_swap(&target, target, value); } + static inline int32_t safeCompareExchange(volatile int32_t& target, int32_t comp, int32_t value) { return __sync_val_compare_and_swap(&target, comp, value); } + + static inline int64_t safeInc(volatile int64_t& v) { return __sync_add_and_fetch(&v, 1); } + static inline int64_t safeDec(volatile int64_t& v) { return __sync_sub_and_fetch(&v, 1); } + static inline int64_t safeExchange(volatile int64_t& target, int64_t value) { return __sync_val_compare_and_swap(&target, target, value); } + static inline int64_t safeCompareExchange(volatile int64_t& target, int64_t comp, int64_t value) { return __sync_val_compare_and_swap(&target, comp, value); } + + #endif //_MSC_VER + + static unsigned getMaxThreads(unsigned threads) { + if (threads == 1) + return 1; + const unsigned maxThreads = hardwareConcurrency(); + if (threads > 0 && threads < maxThreads) + return threads; + return maxThreads; + } + +protected: + virtual void run() {} + +protected: + #ifdef _MSC_VER + + enum PriorityOS { + OS_IDLE = THREAD_PRIORITY_IDLE, + OS_LOW = THREAD_PRIORITY_BELOW_NORMAL, + OS_NORMAL = THREAD_PRIORITY_NORMAL, + OS_HIGH = THREAD_PRIORITY_ABOVE_NORMAL + }; + + static DWORD WINAPI starterConvertor(void* p) + { + CAutoPtr pData((StarterData*)p); + return (DWORD)reinterpret_cast(pData->fnStarter(pData->pData)); + } + + #else //_MSC_VER + + enum PriorityOS { + OS_IDLE = 19, + OS_LOW = 10, + OS_NORMAL = 0, + OS_HIGH = -10 + }; + + #endif //_MSC_VER + + static inline PriorityOS convertPriority(Priority p) { + switch (p) { + case IDLE: return OS_IDLE; + case LOW: return OS_LOW; + case NORMAL: return OS_NORMAL; + case HIGH: return OS_HIGH; + } + return OS_NORMAL; + } + static inline Priority convertPriority(PriorityOS p) { + switch (p) { + case OS_IDLE: return IDLE; + case OS_LOW: return LOW; + case OS_NORMAL: return NORMAL; + case OS_HIGH: return HIGH; + } + return NORMAL; + } + + static void* starter(void* p) + { + ((Thread*)p)->run(); + return NULL; + } + +protected: + thread_t threadHandle; + #ifdef _MSC_VER + DWORD threadId; + #endif +}; +/*----------------------------------------------------------------*/ + + + +/************************************************************************************** + * ThreadPool + * -------------- + * basic thread pool + **************************************************************************************/ + +class GENERAL_API ThreadPool +{ +public: + typedef uint32_t size_type; + typedef Thread value_type; + typedef value_type* iterator; + typedef const value_type* const_iterator; + typedef value_type& reference; + typedef const value_type& const_reference; + typedef CLISTDEFIDX(Thread,size_type) Threads; + +public: + inline ThreadPool() {} + inline ThreadPool(size_type nThreads) : _threads(nThreads>0?nThreads:Thread::hardwareConcurrency()) {} + inline ThreadPool(size_type nThreads, Thread::FncStart pfnStarter, void* pData=NULL) : _threads(nThreads>0?nThreads:Thread::hardwareConcurrency()) { start(pfnStarter, pData); } + inline ~ThreadPool() { join(); } + + #ifdef _SUPPORT_CPP11 + inline ThreadPool(ThreadPool&& rhs) : _threads(std::forward(rhs._threads)) { + } + + inline ThreadPool& operator=(ThreadPool&& rhs) { + _threads.Swap(rhs._threads); + return *this; + } + #endif + + // wait for all running threads to finish and resize threads array + void resize(size_type nThreads) { + join(); + _threads.resize(nThreads); + } + // start all threads with the given function + bool start(Thread::FncStart pfnStarter, void* pData=NULL) { + for (Thread& thread: _threads) + if (!thread.start(pfnStarter, pData)) + return false; + return true; + } + // wait for all running threads to finish + void join() { + for (Thread& thread: _threads) + thread.join(); + } + // stop all threads + void stop() { + for (Thread& thread: _threads) + thread.stop(); + } + // wait for all running threads to finish and release threads array + void Release() { + join(); + _threads.Release(); + } + + inline bool empty() const { return _threads.empty(); } + inline size_type size() const { return _threads.size(); } + inline const_iterator cbegin() const { return _threads.cbegin(); } + inline const_iterator cend() const { return _threads.cend(); } + inline const_iterator begin() const { return _threads.begin(); } + inline const_iterator end() const { return _threads.end(); } + inline iterator begin() { return _threads.begin(); } + inline iterator end() { return _threads.end(); } + inline const_reference operator[](size_type index) const { return _threads[index]; } + inline reference operator[](size_type index) { return _threads[index]; } + +protected: + Threads _threads; +}; +/*----------------------------------------------------------------*/ + + + +/************************************************************************************** + * Process + * -------------- + * basic process control + **************************************************************************************/ + +class GENERAL_API Process +{ +public: + enum Priority { + IDLE = -3, + LOW = -2, + BELOWNORMAL = -1, + NORMAL = 0, + ABOVENORMAL = 1, + HIGH = 2, + REALTIME = 3 + }; + + #ifdef _MSC_VER + + typedef HANDLE process_t; + + static inline process_t getCurrentProcessID() { return ::GetCurrentProcess(); } + static inline void setProcessPriority(process_t id, Priority p) { ::SetPriorityClass(id, convertPriority(p)); } + static inline Priority getProcessPriority(process_t id) { return convertPriority((PriorityOS)::GetPriorityClass(id)); } + + #else //_MSC_VER + + typedef id_t process_t; + + static inline process_t getCurrentProcessID() { return ::getpid(); } + static inline void setProcessPriority(process_t id, Priority p) { ::setpriority(PRIO_PROCESS, id, convertPriority(p)); } + static inline Priority getProcessPriority(process_t id) { return convertPriority((PriorityOS)::getpriority(PRIO_PROCESS, id)); } + + #endif //_MSC_VER + + static inline void setCurrentProcessPriority(Priority p) { setProcessPriority(getCurrentProcessID(), p); } + static inline Priority getCurrentProcessPriority() { return getProcessPriority(getCurrentProcessID()); } + +protected: + + #ifdef _MSC_VER + + enum PriorityOS { + OS_IDLE = IDLE_PRIORITY_CLASS, + OS_LOW = PROCESS_MODE_BACKGROUND_BEGIN, + OS_BELOWNORMAL = BELOW_NORMAL_PRIORITY_CLASS, + OS_NORMAL = NORMAL_PRIORITY_CLASS, + OS_ABOVENORMAL = ABOVE_NORMAL_PRIORITY_CLASS, + OS_HIGH = HIGH_PRIORITY_CLASS, + OS_REALTIME = REALTIME_PRIORITY_CLASS + }; + + #else //_MSC_VER + + enum PriorityOS { + OS_IDLE = 19, + OS_LOW = 15, + OS_BELOWNORMAL = 10, + OS_NORMAL = 0, + OS_ABOVENORMAL = -10, + OS_HIGH = -15, + OS_REALTIME = -20 + }; + + #endif //_MSC_VER + + static inline PriorityOS convertPriority(Priority p) { + switch (p) { + case IDLE: return OS_IDLE; + case LOW: return OS_LOW; + case BELOWNORMAL: return OS_BELOWNORMAL; + case NORMAL: return OS_NORMAL; + case ABOVENORMAL: return OS_ABOVENORMAL; + case HIGH: return OS_HIGH; + case REALTIME: return OS_REALTIME; + } + return OS_NORMAL; + } + static inline Priority convertPriority(PriorityOS p) { + switch (p) { + case OS_IDLE: return IDLE; + case OS_LOW: return LOW; + case OS_BELOWNORMAL: return BELOWNORMAL; + case OS_NORMAL: return NORMAL; + case OS_ABOVENORMAL: return ABOVENORMAL; + case OS_HIGH: return HIGH; + case OS_REALTIME: return REALTIME; + } + return NORMAL; + } +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_THREAD_H__ diff --git a/libs/Common/Timer.cpp b/libs/Common/Timer.cpp new file mode 100644 index 0000000..56e6af0 --- /dev/null +++ b/libs/Common/Timer.cpp @@ -0,0 +1,165 @@ +//////////////////////////////////////////////////////////////////// +// Timer.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" +#include "Timer.h" + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +/** + * Constructor + */ +Timer::Timer(uint8_t nHH, uint8_t nMM) +{ + m_fElapsedTime = 1.f; + m_fTimeScale = 1.f; + #ifdef FIX_FPS + m_fCurrentFramesTime = 0.f; + m_nCurrentFrames = 0; + m_nLastSecFrames = 0; + #endif // FIX_FPS + + // set clock + m_fClock = 0.f; + if (nHH > 23) + nHH = 0; + if (nMM > 59) + nMM = 0; + m_nHH = nHH; + m_nMM = nMM; + m_nSS = 0; + + m_nCrntTime = GetSysTime(); +} // constructor +/*----------------------------------------------------------------*/ + + +Timer::~Timer() +{ +} +/*----------------------------------------------------------------*/ + + +/** + * Counts the actual time and adjusts clock. + */ +void Timer::Update() +{ + const SysType timeNow = GetSysTime(); + m_fElapsedTime = SysTime2Time(timeNow - m_nCrntTime); + m_nCrntTime = timeNow; + + #ifdef FIX_FPS + // compute FPS + ++m_nCurrentFrames; + if ((m_fCurrentFramesTime += m_fElapsedTime) >= 1.f) { + m_nLastSecFrames = (uint32_t)((Type)m_nCurrentFrames/m_fCurrentFramesTime); + m_fCurrentFramesTime = 0.f; + m_nCurrentFrames = 0; + } + #endif // FIX_FPS + + // adjust clock by seconds passed + m_fClock += (m_fElapsedTime * m_fTimeScale); + if (m_fClock >= 1.f) { + m_nSS++; + m_fClock = 0.f; + } + if (m_nSS >= 60) { + m_nMM++; + m_nSS = 0; + } + if (m_nMM >= 60) { + m_nHH++; + m_nMM = 0; + } + if (m_nHH >= 24) + m_nHH = 0; +} // Update +/*----------------------------------------------------------------*/ + + +/** + * Sets the clock to any given starting time. + */ +void Timer::SetClock(uint8_t nHH, uint8_t nMM) +{ + // set clock + if (nHH > 23) + nHH = 0; + if (nMM > 59) + nMM = 0; + + m_nHH = nHH; + m_nMM = nMM; +} // SetClock +/*----------------------------------------------------------------*/ + + +/** + * Gives you a time string with hours, minutes and seconds as return + * and hours and/or minutes as reference parameters. + */ +LPTSTR Timer::GetClock(uint8_t* nHH, uint8_t* nMM, LPTSTR szChar) const +{ + if (nHH != NULL) + *nHH = m_nHH; + if (nMM != NULL) + *nMM = m_nMM; + + if (szChar == NULL) + return NULL; + + _sntprintf(szChar, 32, "%.2d:%.2d:%.2d", m_nHH, m_nMM, m_nSS); + return szChar; +} // GetClock +/*----------------------------------------------------------------*/ + + +#ifdef TIMER_OLDSUPPORT +// Check performance counter +bool HasPerfCounter() +{ + SysType nPerfCnt; + return QueryPerformanceFrequency((LARGE_INTEGER*)&nPerfCnt) == TRUE; +} +const bool Timer::ms_bPerfFlag = HasPerfCounter(); +/*----------------------------------------------------------------*/ +#endif + + +// Get system time factor used to convert to milliseconds +Timer::Type GetSysTimeFactor() +{ + #ifdef _MSC_VER + Timer::SysType nPerfCnt; + const BOOL bPerfFlag = QueryPerformanceFrequency((LARGE_INTEGER*)&nPerfCnt); + #ifdef TIMER_OLDSUPPORT + // either QueryPerformanceCounter or GetTickCount + return (bPerfFlag ? 1000.f / nPerfCnt : 1.f); + #else + ASSERT(bPerfFlag); + return 1000.f / nPerfCnt; + #endif + #else // _MSC_VER + return 0.001f; + #endif // _MSC_VER +} +const Timer::Type Timer::ms_fTimeFactor = GetSysTimeFactor(); +/*----------------------------------------------------------------*/ + +Timer::Type Timer::GetTimeFactor() +{ + return ms_fTimeFactor; +} +/*----------------------------------------------------------------*/ diff --git a/libs/Common/Timer.h b/libs/Common/Timer.h new file mode 100644 index 0000000..8caac8d --- /dev/null +++ b/libs/Common/Timer.h @@ -0,0 +1,141 @@ +//////////////////////////////////////////////////////////////////// +// Timer.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_TIMER_H__ +#define __SEACAVE_TIMER_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + +#ifdef _MSC_VER +//#define TIMER_OLDSUPPORT +#endif +#define FIX_FPS + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class GENERAL_API Timer +{ +public: + typedef int64_t SysType; + typedef float Type; + +public: + Timer(uint8_t nHH=0, uint8_t nMM=0); + ~Timer(); + + void Update(); + + void SetClock(uint8_t nHH, uint8_t nMM); + LPTSTR GetClock(uint8_t* nHH, uint8_t* nMM, LPTSTR szChar) const; + + #ifdef FIX_FPS + inline uint32_t GetFPS() const { return m_nLastSecFrames; } + #else + inline uint32_t GetFPS() const { return (uint32_t)(1.f / m_fElapsedTime); } + #endif // FIX_FPS + + inline Type GetRealCurrent() const { return SysTime2Time(m_nCrntTime); } + inline Type GetCurrent() const { return m_fTimeScale * SysTime2Time(m_nCrntTime); } + inline Type GetRealElapsed() const { return m_fElapsedTime; } + inline Type GetElapsed() const { return m_fTimeScale * m_fElapsedTime; } + inline Type GetDelay(Type d) const { return m_fTimeScale * d; } + inline Type GetScale() const { return m_fTimeScale; } + inline void SetScale(Type fFactor) { m_fTimeScale = fFactor; } + +private: + SysType m_nCrntTime; // current time + Type m_fElapsedTime; // time elapsed since previous frame + Type m_fTimeScale; // slowdown or speedup + uint8_t m_nHH; // clock time hours + uint8_t m_nMM; // clock time minutes + uint8_t m_nSS; // clock time seconds + Type m_fClock; // sum up milliseconds + #ifdef FIX_FPS + Type m_fCurrentFramesTime; // time elapsed for the current frames + uint32_t m_nCurrentFrames; // counter of the current frames + uint32_t m_nLastSecFrames; // counter of the frames in the last second + #endif // FIX_FPS + +public: + // get current time in system units + static inline SysType GetSysTime() { + #ifdef _MSC_VER + #ifdef TIMER_OLDSUPPORT + if (!ms_bPerfFlag) + return GetTickCount(); + #endif + SysType nTime; + QueryPerformanceCounter((LARGE_INTEGER*)&nTime); + return nTime; + #else + timeval tv; + gettimeofday(&tv, NULL); + return (tv.tv_sec * 1000000) + tv.tv_usec; + #endif // _MSC_VER + } + // get milliseconds scaling factor for time + static Type GetTimeFactor(); + // get current time in milliseconds + static inline Type GetTimeMs() { + return GetTimeFactor() * GetSysTime(); + } + // get current time in seconds + static inline Type GetTime() { + return 0.001f * GetTimeFactor() * GetSysTime(); + } + // convert given time to milliseconds + static inline Type SysTime2TimeMs(SysType t) { + return GetTimeFactor() * t; + } + // convert given time to seconds + static inline Type SysTime2Time(SysType t) { + return 0.001f * GetTimeFactor() * t; + } + +protected: + #ifdef TIMER_OLDSUPPORT + static const bool ms_bPerfFlag; // flag for timer to use + #endif + static const Type ms_fTimeFactor; // milliseconds scaling factor for time +}; +/*----------------------------------------------------------------*/ + + +class GENERAL_API AutoTimer +{ +public: + typedef Timer::Type Type; +public: + AutoTimer(Type& duration) : m_duration(duration) { m_duration = Timer::GetTime(); } + ~AutoTimer() { m_duration = Timer::GetTime() - m_duration; } +protected: + Type& m_duration; +}; + +class GENERAL_API AutoAddTimer +{ +public: + typedef Timer::Type Type; +public: + AutoAddTimer(Type& duration) : m_duration(duration) { m_lastTime = Timer::GetTime(); } + ~AutoAddTimer() { m_duration += Timer::GetTime() - m_lastTime; } +protected: + Type& m_duration; + Type m_lastTime; +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_TIMER_H__ diff --git a/libs/Common/Types.cpp b/libs/Common/Types.cpp new file mode 100644 index 0000000..7d7b8c9 --- /dev/null +++ b/libs/Common/Types.cpp @@ -0,0 +1,78 @@ +//////////////////////////////////////////////////////////////////// +// Types.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" +#include "Types.h" + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + + +// G L O B A L S /////////////////////////////////////////////////// + +#ifndef _MSC_VER +int _vscprintf(LPCSTR format, va_list pargs) { + va_list argcopy; + va_copy(argcopy, pargs); + const int retval(vsnprintf(NULL, 0, format, argcopy)); + va_end(argcopy); + return retval; +} +#endif +/*----------------------------------------------------------------*/ + + +namespace SEACAVE { + +const ColorType::value_type ColorType::ONE(255); +const ColorType::alt_type ColorType::ALTONE(1.f); + +const ColorType::value_type ColorType::ONE(255); +const ColorType::alt_type ColorType::ALTONE(1.f); + +const ColorType::value_type ColorType::ONE(1.f); +const ColorType::alt_type ColorType::ALTONE(255); + +const ColorType::value_type ColorType::ONE(1.0); +const ColorType::alt_type ColorType::ALTONE(255); +/*----------------------------------------------------------------*/ + + +// print matrix +template +String cvMat2String(const TYPE* M, uint32_t rows, uint32_t cols, uint32_t step, LPCSTR format) { + String str; + char buf[32]; + if (step == 0) + step = rows; + for (uint32_t i=0; i(), (uint32_t)M.rows, (uint32_t)M.cols, (uint32_t)M.step1(), format); + case CV_64F: return cvMat2String(M.ptr(), (uint32_t)M.rows, (uint32_t)M.cols, (uint32_t)M.step1(), format); + } + return String(); +} +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + + +// C L A S S ////////////////////////////////////////////////////// + diff --git a/libs/Common/Types.h b/libs/Common/Types.h new file mode 100644 index 0000000..9339f26 --- /dev/null +++ b/libs/Common/Types.h @@ -0,0 +1,2825 @@ +//////////////////////////////////////////////////////////////////// +// Types.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_TYPES_H__ +#define __SEACAVE_TYPES_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#ifdef _MSC_VER +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif +#ifdef _SUPPORT_CPP11 +#ifdef __clang__ +#include +#else +#include +#endif +#include +#include +#include +#else +#include +#endif +#ifdef _SUPPORT_CPP17 +#if !defined(__GNUC__) || (__GNUC__ > 7) +#include +#endif +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _USE_OPENMP +#include +#endif + +// Function delegate functionality +#ifdef _SUPPORT_CPP11 +#include "FastDelegateCPP11.h" +#define DELEGATE fastdelegate::delegate +#define DELEGATEBIND(DLGT, FNC) DLGT::from< FNC >() +#define DELEGATEBINDCLASS(DLGT, FNC, OBJ) DLGT::from(*OBJ, FNC) +#else +#include "FastDelegate.h" +#include "FastDelegateBind.h" +#define DELEGATE fastdelegate::FastDelegate +#define DELEGATEBIND(DLGT, FNC) fastdelegate::bind(FNC) +#define DELEGATEBINDCLASS(DLGT, FNC, OBJ) fastdelegate::bind(FNC, OBJ) +#endif + +// include usual boost libraries +#ifdef _USE_BOOST +#if 1 +// disable exception support +#define BOOST_NO_UNREACHABLE_RETURN_DETECTION +#define BOOST_EXCEPTION_DISABLE +#define BOOST_NO_EXCEPTIONS +#endif +#ifdef BOOST_NO_EXCEPTIONS +#include +#endif +#define BOOST_NO_UNREACHABLE_RETURN_DETECTION +// include headers that implement serialization support +#include +#include +#include +#include +#include +#include +#include +#include +#if (BOOST_VERSION / 100000) > 1 || (BOOST_VERSION / 100 % 1000) > 55 +#include +#include +#endif +#include +// include headers that define an input and output archive +#include +#include +// include headers that define memory pools +#include +#include +#endif + +#ifdef _USE_EIGEN +#if defined(_MSC_VER) +#pragma warning (push) +#pragma warning (disable : 4244) // 'argument': conversion from '__int64' to 'int', possible loss of data +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(_MSC_VER) +#pragma warning (pop) +#endif +#endif + +#pragma push_macro("free") +#undef free +#pragma push_macro("DEBUG") +#undef DEBUG +#include +#if CV_MAJOR_VERSION > 2 || CV_MINOR_VERSION > 3 +#include +#endif +#include +#ifdef HAVE_OPENCV_GPU +#if CV_MAJOR_VERSION > 2 +#include +namespace cv { namespace gpu = cuda; } +#else +#include +#endif +#endif +#pragma pop_macro("DEBUG") +#pragma pop_macro("free") + +#ifdef _USE_SSE +#include +#include +#endif + + +// D E F I N E S /////////////////////////////////////////////////// + +// In order to create a singleton class +// add this define in your class; +#define DECLARE_SINGLETON(SClass) \ + protected: SClass(); SClass(const SClass&); \ + public: static inline SClass& GetInstance() { static SClass instance; return instance; } +// or add this define and declare GetInstance() method in the .cpp +// so that the singleton is unique even across modules; +#define DEFINE_SINGLETON(SClass) \ + protected: SClass(); SClass(const SClass&); \ + public: static SClass& GetInstance(); +// or add this declare ms_pInstance; +#define DECLARE_SINGLETONPTR(SClass) \ + SClass* SClass::ms_pInstance(NULL); \ + SClass*& SClass::GetInstanceRef() { return ms_pInstance; } \ + SClass* SClass::GetInstancePtr() { ASSERT(ms_pInstance); return ms_pInstance; } \ + SClass& SClass::GetInstance() { ASSERT(ms_pInstance); return *ms_pInstance; } +// and this define in the class definition. +#define DEFINE_SINGLETONPTR(SClass) \ + protected: SClass(); SClass(const SClass&); static SClass* ms_pInstance; \ + public: static SClass*& GetInstanceRef(); \ + public: static SClass* GetInstancePtr(); \ + public: static SClass& GetInstance(); + +#ifndef CALL_MEMBER_FN +#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember)) +#endif + +#ifndef __PROCESS__ +# ifdef _MSC_VER +# define __PROCESS__ ((unsigned)GetCurrentProcessId()) +# else +# define __PROCESS__ ((unsigned)getpid()) +# endif +#endif + +#ifndef __THREAD__ +# ifdef _MSC_VER +# define __THREAD__ ((unsigned)GetCurrentThreadId()) +# elif defined(__APPLE__) +# include +inline pid_t GetCurrentThreadId() { uint64_t tid64; pthread_threadid_np(NULL, &tid64); return (pid_t)tid64; } +# define __THREAD__ ((unsigned)GetCurrentThreadId()) +# else +# include +# define __THREAD__ ((unsigned)((pid_t)syscall(SYS_gettid))) +# endif +#endif + +#ifndef STCALL +# if defined(_MSC_VER) +# define STCALL __cdecl +# elif defined(__OS2__) +# if defined (__GNUC__) && __GNUC__ < 4 +# define STCALL _cdecl +# else +# /* On other compilers on OS/2, we use the _System calling convention */ +# /* to be compatible with every compiler */ +# define STCALL _System +# endif +# elif defined(__GNUC__) +# define STCALL __attribute__((__cdecl__)) +# else +# define STCALL +# endif +#endif // STCALL + + +// T Y P E D E F I N E S ///////////////////////////////////////// + +// +// Type defines + +#ifndef _MSC_VER +typedef int32_t HRESULT; + +typedef unsigned char BYTE; +typedef unsigned short WORD; +typedef unsigned int DWORD; +typedef uint64_t QWORD; + +typedef char CHAR; +typedef CHAR* LPSTR; +typedef const CHAR* LPCSTR; +typedef CHAR TCHAR; +typedef LPSTR LPTSTR; +typedef LPCSTR LPCTSTR; + +#define _tcslen strlen +#define _tcscpy strcpy +#define _tcsncpy strncpy +#define _tcschr strchr +#define _tcsrchr strrchr +#define _tcscmp strcmp +#define _tcsncmp strncmp +#define _tcsicmp strcasecmp +#define _tcsnicmp strncasecmp +#define _tcsncicmp(s1,s2,s) strncasecmp(s1, s2, (s)*sizeof(TCHAR)) +#define _stprintf sprintf +#define _sntprintf snprintf +#define _vsntprintf vsnprintf +#define _vsctprintf _vscprintf + +int _vscprintf(LPCSTR format, va_list pargs); + +#define _T(s) s +#endif //_MSC_VER + +#define DECLARE_NO_INDEX(...) std::numeric_limits<__VA_ARGS__>::max() + +#ifndef MAKEWORD +#define MAKEWORD(a, b) ((WORD)(((BYTE)(((DWORD)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD)(b)) & 0xff))) << 8)) +#endif +#ifndef MAKELONG +#define MAKELONG(a, b) ((DWORD)(((WORD)(((DWORD)(a)) & 0xffff)) | ((DWORD)((WORD)(((DWORD)(b)) & 0xffff))) << 16)) +#endif +#ifndef LOWORD +#define LOWORD(l) ((WORD)(((DWORD)(l)) & 0xffff)) +#endif +#ifndef HIWORD +#define HIWORD(l) ((WORD)((((DWORD)(l)) >> 16) & 0xffff)) +#endif +#ifndef LOBYTE +#define LOBYTE(w) ((BYTE)(((WORD)(w)) & 0xff)) +#endif +#ifndef HIBYTE +#define HIBYTE(w) ((BYTE)((((WORD)(w)) >> 8) & 0xff)) +#endif + +#ifndef MAX_PATH +#define MAX_PATH 260 +#endif + +#ifndef NULL +#define NULL 0 +#endif + +#ifdef max +#undef max +#endif +#ifdef min +#undef min +#endif + +#ifndef MINF +#define MINF std::min +#endif +#ifndef MAXF +#define MAXF std::max +#endif + +#ifndef RAND +#define RAND std::rand +#endif + +namespace SEACAVE { + +// signed and unsigned types of the size of the architecture +// (32 or 64 bit for x86 and respectively x64) +#ifdef _ENVIRONMENT64 +typedef int64_t int_t; +typedef uint64_t uint_t; +#else +typedef int32_t int_t; +typedef uint32_t uint_t; +#endif + +// type used for the size of the files +typedef int64_t size_f_t; + +// type used as the default floating number precision +typedef double REAL; + +// invalid index +constexpr uint32_t NO_ID = DECLARE_NO_INDEX(uint32_t); + +template +struct RealType { typedef typename std::conditional::value, TYPE, REALTYPE>::type type; }; + +template +inline T MINF3(const T& x1, const T& x2, const T& x3) { + return MINF(MINF(x1, x2), x3); +} +template +inline T MAXF3(const T& x1, const T& x2, const T& x3) { + return MAXF(MAXF(x1, x2), x3); +} + +template +FORCEINLINE T RANDOM() { return T(RAND())/RAND_MAX; } + +template +union TAliasCast { + T1 f; + T2 i; + inline TAliasCast() {} + inline TAliasCast(T1 v) : f(v) {} + inline TAliasCast(T2 v) : i(v) {} + inline TAliasCast& operator = (T1 v) { f = v; return *this; } + inline TAliasCast& operator = (T2 v) { i = v; return *this; } + inline operator T1 () const { return f; } +}; +typedef TAliasCast CastF2I; +typedef TAliasCast CastD2I; + +} // namespace SEACAVE + +#if defined(_MSC_VER) +# define __LITTLE_ENDIAN 0 +# define __BIG_ENDIAN 1 +# define __PDP_ENDIAN 2 +# define __BYTE_ORDER __LITTLE_ENDIAN +#elif defined(__APPLE__) +# include +#elif defined(__GNUC__) +# include +#endif + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Strings.h" +#include "AutoPtr.h" +#include "List.h" +#include "Thread.h" +#include "SharedPtr.h" +#include "Queue.h" +#include "Hash.h" +#include "Timer.h" +#include "CriticalSection.h" +#include "Semaphore.h" +#include "Util.h" +#include "File.h" +#include "MemFile.h" +#include "LinkLib.h" + +namespace SEACAVE { + +typedef class GENERAL_API CSharedPtr FilePtr; + +typedef class GENERAL_API CSharedPtr ISTREAMPTR; +typedef ISTREAM* LPISTREAM; + +typedef class GENERAL_API CSharedPtr OSTREAMPTR; +typedef OSTREAM* LPOSTREAM; + +typedef class GENERAL_API CSharedPtr IOSTREAMPTR; +typedef IOSTREAM* LPIOSTREAM; + +typedef class GENERAL_API cList VoidArr; +typedef class GENERAL_API cList LPCTSTRArr; +typedef class GENERAL_API cList StringArr; +typedef class GENERAL_API cList IDXArr; +typedef class GENERAL_API cList Unsigned8Arr; +typedef class GENERAL_API cList UnsignedArr; +typedef class GENERAL_API cList Unsigned32Arr; +typedef class GENERAL_API cList Unsigned64Arr; +typedef class GENERAL_API cList SizeArr; +typedef class GENERAL_API cList IntArr; +typedef class GENERAL_API cList BoolArr; +typedef class GENERAL_API cList FloatArr; +typedef class GENERAL_API cList DoubleArr; + +} // namespace SEACAVE + +#include "Log.h" +#include "EventQueue.h" +#include "SML.h" +#include "ConfigTable.h" +#include "HTMLDoc.h" + + +// D E F I N E S /////////////////////////////////////////////////// + +// +// Constant defines + +// everything went smooth +#define _OK ((HRESULT)0L) + +// just reports no errors +#define _CANCEL 0x82000000 + +// general error message +#define _FAIL 0x82000001 + +// specific error messages +#define _CREATEAPI 0x82000002 +#define _CREATEDEVICE 0x82000003 +#define _CREATEBUFFER 0x82000004 +#define _INVALIDPARAM 0x82000005 +#define _INVALIDID 0x82000006 +#define _BUFFERSIZE 0x82000007 +#define _BUFFERLOCK 0x82000008 +#define _NOTCOMPATIBLE 0x82000009 +#define _OUTOFMEMORY 0x8200000a +#define _FILENOTFOUND 0x8200000b +#define _INVALIDFILE 0x8200000c +#define _NOSHADERSUPPORT 0x8200000d +#define _NOSERVERFOUND 0x8200000e +#define _WOULDBLOCK 0x8200000f + +#ifndef SUCCEEDED +#define SUCCEEDED(hr) (((HRESULT)(hr)) >= 0) +#endif +#ifndef FAILED +#define FAILED(hr) (((HRESULT)(hr)) < 0) +#endif + + +// D E F I N E S /////////////////////////////////////////////////// + +#define RGBA(r, g, b, a) ((DWORD)(((a) << 24) | ((r) << 16) | ((g) << 8) | (b))) +#define RGBC(clr) (RGBA((BYTE)((clr).fR*255), (BYTE)((clr).fG*255), (BYTE)((clr).fB*255), (BYTE)((clr).fA*255))) +#define RGB24TO8(r,g,b) ((BYTE)((((WORD)r)*30+((WORD)g)*59+((WORD)b)*11)/100)) +#define RGB24TO16(r,g,b) ((((WORD)(((BYTE)(r))>>3))<<11) | (((WORD)(((BYTE)(g))>>2))<<5) | ((WORD)(((BYTE)(b))>>3))) +#define RGB16TOR(rgb) (((BYTE)(((WORD)(rgb))>>11))<<3) +#define RGB16TOG(rgb) (((BYTE)((((WORD)(rgb))&0x07E0)>>5))<<2) +#define RGB16TOB(rgb) (((BYTE)(((WORD)(rgb))&0x001F))<<3) + +#define TIMER_START() SEACAVE::Timer::SysType timerStart = SEACAVE::Timer::GetSysTime() +#define TIMER_UPDATE() timerStart = SEACAVE::Timer::GetSysTime() +#define TIMER_GET() SEACAVE::Timer::SysTime2TimeMs(SEACAVE::Timer::GetSysTime() - timerStart) +#define TIMER_GET_INT() ((SEACAVE::Timer::SysType)TIMER_GET()) +#define TIMER_GET_FORMAT() SEACAVE::Util::formatTime(TIMER_GET_INT()) + + +// D E F I N E S /////////////////////////////////////////////////// + +#ifndef CHECK +#define CHECK(exp) { if (!(exp)) { VERBOSE("Check failed: " #exp); abort(); } } +#endif +#ifndef ABORT +#define ABORT(msg) { VERBOSE("error: " #msg); exit(-1); } +#endif + +#ifndef _USE_MATH_DEFINES +/** e */ +#ifndef M_E +#define M_E 2.7182818284590452353602874713527 +#endif +/** ln(2) */ +#ifndef M_LN2 +#define M_LN2 0.69314718055994530941723212145818 +#endif +/** ln(10) */ +#ifndef M_LN10 +#define M_LN10 2.3025850929940456840179914546844 +#endif +/** pi */ +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832795 +#endif +/** pi/2 */ +#ifndef M_PI_2 +#define M_PI_2 1.5707963267948966192313216916398 +#endif +/** 1/pi */ +#ifndef M_1_PI +#define M_1_PI 0.31830988618379067153776752674503 +#endif +/** 2/pi */ +#ifndef M_2_PI +#define M_2_PI 0.63661977236758134307553505349006 +#endif +/** 2*sqrt(pi) */ +#ifndef M_2_SQRTPI +#define M_2_SQRTPI 1.1283791670955125738961589031216 +#endif +/** sqrt(2) */ +#ifndef M_SQRT2 +#define M_SQRT2 1.4142135623730950488016887242097 +#endif +/** sqrt(1/2) */ +#ifndef M_SQRT1_2 +#define M_SQRT1_2 0.70710678118654752440084436210485 +#endif +#endif + +// constants +#define TWO_PI 6.283185307179586476925286766559 +#define PI 3.1415926535897932384626433832795 +#define HALF_PI 1.5707963267948966192313216916398 +#define SQRT_2PI 2.506628274631000502415765284811 +#define INV_TWO_PI 0.15915494309189533576888376337251 +#define INV_PI 0.31830988618379067153776752674503 +#define INV_HALF_PI 0.63661977236758134307553505349006 +#define INV_SQRT_2PI 0.39894228040143267793994605993439 +#define D2R(d) ((d)*(PI/180.0)) // degree to radian +#define R2D(r) ((r)*(180.0/PI)) // radian to degree +#define SQRT_2 1.4142135623730950488016887242097 +#define SQRT_3 1.7320508075688772935274463415059 +#define LOG_2 0.30102999566398119521373889472449 +#define LN_2 0.69314718055994530941723212145818 +#define ZERO_TOLERANCE (1e-7) +#define INV_ZERO (1e+14) + +// float constants +#define FTWO_PI ((float)TWO_PI) +#define FPI ((float)PI) +#define FHALF_PI ((float)HALF_PI) +#define FSQRT_2PI ((float)SQRT_2PI) +#define FINV_TWO_PI ((float)INV_TWO_PI) +#define FINV_PI ((float)INV_PI) +#define FINV_HALF_PI ((float)INV_HALF_PI) +#define FINV_SQRT_2PI ((float)INV_SQRT_2PI) +#define FD2R(d) ((d)*(FPI/180.f)) // degree to radian +#define FR2D(r) ((r)*(180.f/FPI)) // radian to degree +#define FSQRT_2 ((float)SQRT_2) +#define FSQRT_3 ((float)SQRT_3) +#define FLOG_2 ((float)LOG_2) +#define FLN_2 ((float)LN_2) +#define FZERO_TOLERANCE 0.0001f +#define FINV_ZERO 1000000.f + +#define GCLASS unsigned +#define FRONT 0 +#define BACK 1 +#define PLANAR 2 +#define CLIPPED 3 +#define CULLED 4 +#define VISIBLE 5 + + +// M A C R O S ///////////////////////////////////////////////////// + +#define FLOOR SEACAVE::Floor2Int +#define FLOOR2INT SEACAVE::Floor2Int +#define CEIL SEACAVE::Ceil2Int +#define CEIL2INT SEACAVE::Ceil2Int +#define ROUND SEACAVE::Round2Int +#define ROUND2INT SEACAVE::Round2Int +#define SIN std::sin +#define ASIN std::asin +#define COS std::cos +#define ACOS std::acos +#define TAN std::tan +#define ATAN std::atan +#define ATAN2 std::atan2 +#define POW std::pow +#define POWI SEACAVE::powi +#define LOG2I SEACAVE::log2i + + +namespace SEACAVE { + +// F U N C T I O N S /////////////////////////////////////////////// + +template +struct MakeIdentity { using type = T; }; +template +using MakeSigned = typename std::conditional::value,std::make_signed,SEACAVE::MakeIdentity>::type; + +template +constexpr T1 Cast(const T2& v) { + return static_cast(v); +} + +template +constexpr T& NEGATE(T& a) { + return (a = -a); +} +template +constexpr T SQUARE(const T& a) { + return a * a; +} +template +constexpr T CUBE(const T& a) { + return a * a * a; +} +template +inline T SQRT(const T& a) { + return T(sqrt(a)); +} +template +inline T EXP(const T& a) { + return T(exp(a)); +} +template +inline T LOGN(const T& a) { + return T(log(a)); +} +template +inline T LOG10(const T& a) { + return T(log10(a)); +} +template +constexpr T powi(T base, unsigned exp) { + T result(1); + while (exp) { + if (exp & 1) + result *= base; + exp >>= 1; + base *= base; + } + return result; +} +constexpr int log2i(unsigned val) { + int ret = -1; + while (val) { + val >>= 1; + ++ret; + } + return ret; +} +template constexpr inline int log2i() { return 1+log2i<(N>>1)>(); } +template <> constexpr inline int log2i<0>() { return -1; } +template <> constexpr inline int log2i<1>() { return 0; } +template <> constexpr inline int log2i<2>() { return 1; } + +template +inline T arithmeticSeries(T n, T a1=1, T d=1) { + return (n*(a1*2+(n-1)*d))/2; +} +template +constexpr T factorial(T n) { + T ret = 1; + while (n > 1) + ret *= n--; + return ret; +} +template +constexpr T combinations(const T& n, const T& k) { + ASSERT(n >= k); + #if 1 + T num = n; + const T den = factorial(k); + for (T i=n-k+1; i +inline float FPOW2(float p) { + if (bSafe && p < -126.f) { + return 0.f; + } else { + ASSERT(p >= -126.f); + CastF2I v; + v.i = static_cast((1 << 23) * (p + 126.94269504f)); + return v.f; + } +} +template +inline float FEXP(float v) { + return FPOW2(1.44269504f * v); +} + +// Inverse of the square root +// Compute a fast 1 / sqrtf(v) approximation +inline float RSQRT(float v) { + #ifdef _FAST_INVSQRT + // This code supposedly originates from Id-software + const float halfV = v * 0.5f; + (int32_t&)v = 0x5f3759df - (((int32_t&)v) >> 1); + // Iterations of the Newton's method + v = v * (1.5f - halfV * v * v); + v = v * (1.5f - halfV * v * v); + return v * (1.5f - halfV * v * v); + #else + return 1.f / SQRT(v); + #endif +} +inline double RSQRT(const double& x) { + #ifdef _FAST_INVSQRT + double v = x; + const double halfV = v * 0.5; + (int64_t&)v = 0x5fe6ec85e7de30daLL - (((int64_t&)v) >> 1); + // Iterations of the Newton's method + v = v * (1.5 - halfV * v * v); + v = v * (1.5 - halfV * v * v); + v = v * (1.5 - halfV * v * v); + return v * (1.5 - halfV * v * v); + #else + return 1.0 / SQRT(x); + #endif +} + +// approximate tanh +template +inline T TANH(const T& x) { + const T x2 = x*x; + #if 0 + // Taylor series expansion (very inaccurate) + return x*(1.0 + x2*(-T(1)/T(3) + x2*(T(2)/T(15) + x2*(-T(17)/T(315) + x2*(T(62)/T(2835) - x2*(T(1382)/T(155925))))))); + #else + // Lambert's continued fraction + const T den = (((x2+T(378))*x2+T(17325))*x2+T(135135))*x; + const T div = ((x2*T(28)+T(3150))*x2+T(62370))*x2+T(135135); + return den/div; + #endif +} +/*----------------------------------------------------------------*/ + + +// Cubic root functions +// cube root approximation using bit hack for 32-bit float (5 decimals) +// (exploits the properties of IEEE 754 floating point numbers +// by leveraging the fact that their binary representation is close to a log2 representation) +inline float cbrt5(float x) { + #if 0 + CastF2I c(x); + c.i = ((c.i-(127<<23))/3+(127<<23)); + #else + TAliasCast c(x); + c.i = c.i/3 + 709921077u; + #endif + return c.f; +} +// cube root approximation using bit hack for 64-bit float +// adapted from Kahan's cbrt (5 decimals) +inline double cbrt5(double x) { + TAliasCast c(0.0), d(x); + c.i[1] = d.i[1]/3 + 715094163u; + return c.f; +} +// iterative cube root approximation using Halley's method +// faster convergence than Newton's method: (R/(a*a)+a*2)/3 +template +FORCEINLINE T cbrt_halley(const T& a, const T& R) { + const T a3 = a*a*a; + const T a3R = a3+R; + return a * (a3R + R) / (a3 + a3R); +} +// fast cubic root (variable precision) +template +FORCEINLINE T fast_cbrt(const T& x) { + return cbrt_halley(fast_cbrt(x), x); +} +template<> +FORCEINLINE double fast_cbrt(const double& x) { + return cbrt_halley((double)cbrt5((float)x), x); +} +template<> +FORCEINLINE float fast_cbrt(const float& x) { + return cbrt_halley(cbrt5(x), x); +} +// default cubic root function +FORCEINLINE float CBRT(float x) { + #ifdef _FAST_CBRT + return fast_cbrt(x); + #else + return POW(x, 1.0f/3.0f); + #endif +} +FORCEINLINE double CBRT(const double& x) { + #ifdef _FAST_CBRT + return fast_cbrt(x); + #else + return POW(x, 1.0/3.0); + #endif +} +/*----------------------------------------------------------------*/ + + +#if defined(__GNUC__) + +FORCEINLINE int PopCnt(uint32_t bb) { + return __builtin_popcount(bb); +} +FORCEINLINE int PopCnt(uint64_t bb) { + return __builtin_popcountll(bb); +} +FORCEINLINE int PopCnt15(uint64_t bb) { + return __builtin_popcountll(bb); +} +FORCEINLINE int PopCntSparse(uint64_t bb) { + return __builtin_popcountll(bb); +} + +#elif defined(_USE_SSE) && defined(_M_AMD64) // 64 bit windows + +FORCEINLINE int PopCnt(uint32_t bb) { + return (int)_mm_popcnt_u32(bb); +} +FORCEINLINE int PopCnt(uint64_t bb) { + return (int)_mm_popcnt_u64(bb); +} +FORCEINLINE int PopCnt15(uint64_t bb) { + return (int)_mm_popcnt_u64(bb); +} +FORCEINLINE int PopCntSparse(uint64_t bb) { + return (int)_mm_popcnt_u64(bb); +} + +#else + +// general purpose population count +template +constexpr int PopCnt(T bb) +{ + STATIC_ASSERT(std::is_integral::value && std::is_unsigned::value); + return std::bitset(bb).count(); +} +template<> +inline int PopCnt(uint64_t bb) { + const uint64_t k1 = (uint64_t)0x5555555555555555; + const uint64_t k2 = (uint64_t)0x3333333333333333; + const uint64_t k3 = (uint64_t)0x0F0F0F0F0F0F0F0F; + const uint64_t k4 = (uint64_t)0x0101010101010101; + bb -= (bb >> 1) & k1; + bb = (bb & k2) + ((bb >> 2) & k2); + bb = (bb + (bb >> 4)) & k3; + return (bb * k4) >> 56; +} +// faster version assuming not more than 15 bits set, used in mobility +// eval, posted on CCC forum by Marco Costalba of Stockfish team +inline int PopCnt15(uint64_t bb) { + unsigned w = unsigned(bb >> 32), v = unsigned(bb); + v -= (v >> 1) & 0x55555555; // 0-2 in 2 bits + w -= (w >> 1) & 0x55555555; + v = ((v >> 2) & 0x33333333) + (v & 0x33333333); // 0-4 in 4 bits + w = ((w >> 2) & 0x33333333) + (w & 0x33333333); + v += w; // 0-8 in 4 bits + v *= 0x11111111; + return int(v >> 28); +} +// version faster on sparsely populated bitboards +inline int PopCntSparse(uint64_t bb) { + int count = 0; + while (bb) { + count++; + bb &= bb - 1; + } + return count; +} + +#endif +/*----------------------------------------------------------------*/ + + +#ifdef _FAST_FLOAT2INT +// fast float to int conversion +// (xs routines at stereopsis: http://www.stereopsis.com/sree/fpu2006.html by Sree Kotay) +const double _float2int_doublemagic = 6755399441055744.0; //2^52 * 1.5, uses limited precision to floor +const double _float2int_doublemagicdelta = (1.5e-8); +const double _float2int_doublemagicroundeps = (.5f-_float2int_doublemagicdelta); //almost .5f = .5f - 1e^(number of exp bit) +FORCEINLINE int CRound2Int(const double& x) { + const CastD2I c(x + _float2int_doublemagic); + ASSERT(int32_t(floor(x+.5)) == c.i); + return c.i; +} +#endif +template +FORCEINLINE INTTYPE Floor2Int(float x) { + #ifdef _FAST_FLOAT2INT + return CRound2Int(double(x)-_float2int_doublemagicroundeps); + #else + return static_cast(floor(x)); + #endif +} +template +FORCEINLINE INTTYPE Floor2Int(double x) { + #ifdef _FAST_FLOAT2INT + return CRound2Int(x-_float2int_doublemagicroundeps); + #else + return static_cast(floor(x)); + #endif +} +template +FORCEINLINE INTTYPE Ceil2Int(float x) { + #ifdef _FAST_FLOAT2INT + return CRound2Int(double(x)+_float2int_doublemagicroundeps); + #else + return static_cast(ceil(x)); + #endif +} +template +FORCEINLINE INTTYPE Ceil2Int(double x) { + #ifdef _FAST_FLOAT2INT + return CRound2Int(x+_float2int_doublemagicroundeps); + #else + return static_cast(ceil(x)); + #endif +} +template +FORCEINLINE INTTYPE Round2Int(float x) { + #ifdef _FAST_FLOAT2INT + return CRound2Int(double(x)+_float2int_doublemagicdelta); + #else + return static_cast(floor(x+.5f)); + #endif +} +template +FORCEINLINE INTTYPE Round2Int(double x) { + #ifdef _FAST_FLOAT2INT + return CRound2Int(x+_float2int_doublemagicdelta); + #else + return static_cast(floor(x+.5)); + #endif +} +/*----------------------------------------------------------------*/ + + +// INTERPOLATION + +// Linear interpolation +inline float lerp(float u, float v, float x) +{ + return u + (v - u) * x; +} +template +inline Type lerp(const Type& u, const Type& v, float x) +{ + return u + (v - u) * x; +} + +// Cubic interpolation +inline float cerp(float u0, float u1, float u2, float u3, float x) +{ + const float p((u3 - u2) - (u0 - u1)); + const float q((u0 - u1) - p); + const float r(u2 - u0); + return x * (x * (x * p + q) + r) + u1; +} +template +inline Type cerp(const Type& u0, const Type& u1, const Type& u2, const Type& u3, float x) +{ + const Type p((u3 - u2) - (u0 - u1)); + const Type q((u0 - u1) - p); + const Type r(u2 - u0); + return x * (x * (x * p + q) + r) + u1; +} +/*----------------------------------------------------------------*/ + + +// S T R U C T S /////////////////////////////////////////////////// + +#ifdef _USE_SSE + +// define utile functions to deal with SSE operations + +struct ALIGN(16) sse_vec4f { + union { + float v[4]; + struct { + float x; + float y; + float z; + float w; + }; + }; + inline sse_vec4f() {} + inline sse_vec4f(const float* p) : x(p[0]), y(p[1]), z(p[2]), w(p[3]) {} + inline sse_vec4f(float f0, float f1, float f2, float f3) : x(f0), y(f1), z(f2), w(f3) {} + inline operator const float*() const {return v;} + inline operator float*() {return v;} +}; + +struct ALIGN(16) sse_vec2d { + union { + double v[2]; + struct { + double x; + double y; + }; + }; + inline sse_vec2d() {} + inline sse_vec2d(const double* p) : x(p[0]), y(p[1]) {} + inline sse_vec2d(const double& f0, const double& f1) : x(f0), y(f1) {} + inline operator const double*() const {return v;} + inline operator double*() {return v;} +}; + +struct sse_f_t { + typedef __m128 sse_t; + typedef const sse_t& arg_sse_t; + typedef float real_t; + inline sse_f_t() {} + inline sse_f_t(const sse_t& p) : v(p) {} + inline sse_f_t(real_t p) : v(load1(p)) {} + inline sse_f_t(const real_t* p) : v(load(p)) {} + inline sse_f_t(real_t f0, real_t f1, real_t f2, real_t f3) : v(set(f0,f1,f2,f3)) {} + inline operator sse_t() const {return v;} + inline operator sse_t&() {return v;} + inline sse_t operator ==(sse_t s) const {return cmpeq(v,s);} + inline sse_t operator =(sse_t s) {return v=s;} + inline sse_t operator +(sse_t s) const {return add(v,s);} + inline sse_t operator +=(sse_t s) {return v=add(v,s);} + inline sse_t operator -(sse_t s) const {return sub(v,s);} + inline sse_t operator -=(sse_t s) {return v=sub(v,s);} + inline sse_t operator *(sse_t s) const {return mul(v,s);} + inline sse_t operator *=(sse_t s) {return v=mul(v,s);} + inline sse_t operator /(sse_t s) const {return div(v,s);} + inline sse_t operator /=(sse_t s) {return v=div(v,s);} + inline void get(real_t* p) const {store(p,v);} + static inline sse_t zero() {return _mm_setzero_ps();} + static inline sse_t load1(real_t p) {return _mm_load1_ps(&p);} + static inline sse_t load(const real_t* p) {return _mm_load_ps(p);} + static inline sse_t loadu(const real_t* p) {return _mm_loadu_ps(p);} + static inline sse_t set(real_t f0, real_t f1, real_t f2, real_t f3) {return _mm_set_ps(f0,f1,f2,f3);} + static inline void store(real_t *p, sse_t s){_mm_store_ps(p,s);} + static inline void storeu(real_t *p, sse_t s){_mm_storeu_ps(p,s);} + static inline sse_t add(sse_t s1, sse_t s2) {return _mm_add_ps(s1,s2);} + static inline sse_t sub(sse_t s1, sse_t s2) {return _mm_sub_ps(s1,s2);} + static inline sse_t mul(sse_t s1, sse_t s2) {return _mm_mul_ps(s1,s2);} + static inline sse_t div(sse_t s1, sse_t s2) {return _mm_div_ps(s1,s2);} + static inline sse_t min(sse_t s1, sse_t s2) {return _mm_min_ps(s1,s2);} + static inline sse_t max(sse_t s1, sse_t s2) {return _mm_max_ps(s1,s2);} + static inline sse_t cmpeq(sse_t s1, sse_t s2){return _mm_cmpeq_ps(s1,s2);} + static inline sse_t sqrt(sse_t s) {return _mm_sqrt_ps(s);} + static inline sse_t rsqrt(sse_t s) {return _mm_rsqrt_ps(s);} + static inline int floor2int(real_t f) {return _mm_cvtt_ss2si(_mm_load_ss(&f));} + #ifdef _WIN32 + static inline real_t sum(sse_t s) {return (s.m128_f32[0]+s.m128_f32[2])+(s.m128_f32[1]+s.m128_f32[3]);} + static inline real_t sum3(sse_t s) {return (s.m128_f32[0]+s.m128_f32[2])+s.m128_f32[1];} + #else + static inline real_t sum(sse_t s) {real_t *f = (real_t*)(&s); return (f[0]+f[2])+(f[1]+f[3]);} + static inline real_t sum3(sse_t s) {real_t *f = (real_t*)(&s); return (f[0]+f[2])+f[1];} + #endif + /* + static inline real_t dot(sse_t s1, sse_t s2) { + sse_t temp = _mm_dp_ps(s1, s2, 0xF1); + real_t* f = (real_t*)(&temp); return f[0]; + } + */ + static real_t dot(const real_t* a, const real_t* b, size_t size) { + const real_t* const end = a+size; + const size_t iters = (size>>2); + real_t fres = 0.f; + if (iters) { + const real_t* const e = a+(iters<<2); + sse_t mres = zero(); + do { + mres = _mm_add_ps(mres, _mm_mul_ps(_mm_loadu_ps(a), _mm_loadu_ps(b))); + a += 4; b += 4; + } while (a < e); + fres = sum(mres); + } + while (a>1); + real_t fres = 0.0; + if (iters) { + const real_t* const e = a+(iters<<1); + sse_t mres = zero(); + do { + mres = _mm_add_pd(mres, _mm_mul_pd(_mm_loadu_pd(a), _mm_loadu_pd(b))); + a += 2; b += 2; + } while (a < e); + fres = sum(mres); + } + while (a +inline bool ISFINITE(const _Tp* x, size_t n) { for (size_t i=0; i +inline bool ISINSIDE(_Tp v,_Tp l0,_Tp l1) { ASSERT(l0 +inline bool ISINSIDES(_Tp v,_Tp l0,_Tp l1) { return l0 < l1 ? ISINSIDE(v, l0, l1) : ISINSIDE(v, l1, l0); } + +template +inline _Tp CLAMP(_Tp v, _Tp l0, _Tp l1) { ASSERT(l0<=l1); return MINF(MAXF(v, l0), l1); } +template +inline _Tp CLAMPS(_Tp v, _Tp l0, _Tp l1) { return l0 <= l1 ? CLAMP(v, l0, l1) : CLAMP(v, l1, l0); } + +template +inline _Tp SIGN(_Tp x) { if (x > _Tp(0)) return _Tp(1); if (x < _Tp(0)) return _Tp(-1); return _Tp(0); } + +template +inline _Tp ABS(_Tp x) { return std::abs(x); } + +template +constexpr _Tp ZEROTOLERANCE() { return _Tp(0); } +template<> +constexpr float ZEROTOLERANCE() { return FZERO_TOLERANCE; } +template<> +constexpr double ZEROTOLERANCE() { return ZERO_TOLERANCE; } + +template +constexpr _Tp EPSILONTOLERANCE() { return std::numeric_limits<_Tp>::epsilon(); } +template<> +constexpr float EPSILONTOLERANCE() { return 0.00001f; } +template<> +constexpr double EPSILONTOLERANCE() { return 1e-10; } + +inline bool ISZERO(float x) { return ABS(x) < FZERO_TOLERANCE; } +inline bool ISZERO(double x) { return ABS(x) < ZERO_TOLERANCE; } + +inline bool ISEQUAL(float x, float v) { return ABS(x-v) < FZERO_TOLERANCE; } +inline bool ISEQUAL(double x, double v) { return ABS(x-v) < ZERO_TOLERANCE; } + +inline float INVZERO(float) { return FINV_ZERO; } +inline double INVZERO(double) { return INV_ZERO; } +template +inline _Tp INVZERO(_Tp) { return std::numeric_limits<_Tp>::max(); } + +template +inline _Tp INVERT(_Tp x) { return (x==_Tp(0) ? INVZERO(x) : _Tp(1)/x); } + +template +inline _Tp SAFEDIVIDE(_Tp x, _Tp y) { return (y==_Tp(0) ? INVZERO(y) : x/y); } +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + + +#include "Random.h" +#include "HalfFloat.h" + + +namespace SEACAVE { + +// P R O T O T Y P E S ///////////////////////////////////////////// + +template class TMatrix; +template class TAABB; +template class TRay; +template class TPlane; + +// 2D point struct +template +class TPoint2 : public cv::Point_ +{ +public: + typedef TYPE Type; + typedef cv::Point_ Base; + typedef cv::Size Size; + typedef cv::Vec cvVec; + typedef cv::Matx Vec; + typedef cv::Matx VecT; + #ifdef _USE_EIGEN + EIGEN_MAKE_ALIGNED_OPERATOR_NEW_IF_VECTORIZABLE_FIXED_SIZE(TYPE,2) + typedef Eigen::Matrix EVec; + typedef Eigen::Map CEVecMap; + typedef Eigen::Map EVecMap; + #endif + + using Base::x; + using Base::y; + + static const TPoint2 ZERO; + static const TPoint2 INF; + +public: + inline TPoint2() {} + inline TPoint2(const Size& rhs) : Base(rhs) {} + template inline TPoint2(const cv::Point_& rhs) : Base((Type)rhs.x,(Type)rhs.y) {} + template inline TPoint2(const cv::Matx& rhs) : Base(rhs(0),rhs(1)) {} + template inline TPoint2(const cv::Matx& rhs) : Base(rhs(0),rhs(1)) {} + #ifdef _USE_EIGEN + inline TPoint2(const EVec& rhs) { operator EVecMap () = rhs; } + #endif + explicit inline TPoint2(const TYPE& _x) : Base(_x,_x) {} + inline TPoint2(const TYPE& _x, const TYPE& _y) : Base(_x,_y) {} + explicit inline TPoint2(const cv::Point3_& pt) : Base(pt.x/pt.z,pt.y/pt.z) {} + + template inline TPoint2& operator = (const cv::Point_& rhs) { Base::operator = (rhs); return *this; } + template inline TPoint2& operator = (const cv::Matx& rhs) { operator Vec& () = rhs; return *this; } + template inline TPoint2& operator = (const cv::Matx& rhs) { operator VecT& () = rhs; return *this; } + #ifdef _USE_EIGEN + inline TPoint2& operator = (const EVec& rhs) { operator EVecMap () = rhs; return *this; } + #endif + + // conversion to another data type + template inline operator TPoint2 () const { return TPoint2((T)x,(T)y); } + + // pointer to the first element access + inline const TYPE* ptr() const { return &x; } + inline TYPE* ptr() { return &x; } + + // 1D element access + inline const TYPE& operator [](size_t i) const { ASSERT(i>=0 && i<2); return ptr()[i]; } + inline TYPE& operator [](size_t i) { ASSERT(i>=0 && i<2); return ptr()[i]; } + + // Access point as Size equivalent + inline operator const Size& () const { return *((const Size*)this); } + inline operator Size& () { return *((Size*)this); } + + // Access point as vector equivalent + inline operator const Vec& () const { return *((const Vec*)this); } + inline operator Vec& () { return *((Vec*)this); } + + // Access point as transposed vector equivalent + inline operator const VecT& () const { return *((const VecT*)this); } + inline operator VecT& () { return *((VecT*)this); } + + #ifdef _USE_EIGEN + // Access point as Eigen equivalent + inline operator EVec () const { return CEVecMap((const TYPE*)this); } + // Access point as Eigen::Map equivalent + inline operator const CEVecMap () const { return CEVecMap((const TYPE*)this); } + inline operator EVecMap () { return EVecMap((TYPE*)this); } + #endif + + #ifdef _USE_BOOST + // serialize + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & boost::serialization::base_object(*this); + } + #endif +}; +template const TPoint2 TPoint2::ZERO(0,0); +template const TPoint2 TPoint2::INF(std::numeric_limits::infinity(),std::numeric_limits::infinity()); +/*----------------------------------------------------------------*/ +typedef TPoint2 Point2i; +typedef TPoint2 Point2hf; +typedef TPoint2 Point2f; +typedef TPoint2 Point2d; +/*----------------------------------------------------------------*/ + + +// 3D point struct +template +class TPoint3 : public cv::Point3_ +{ +public: + typedef TYPE Type; + typedef cv::Point3_ Base; + typedef cv::Vec cvVec; + typedef cv::Matx Vec; + typedef cv::Matx VecT; + #ifdef _USE_EIGEN + EIGEN_MAKE_ALIGNED_OPERATOR_NEW_IF_VECTORIZABLE_FIXED_SIZE(TYPE,3) + typedef Eigen::Matrix EVec; + typedef Eigen::Map CEVecMap; + typedef Eigen::Map EVecMap; + #endif + + using Base::x; + using Base::y; + using Base::z; + + static const TPoint3 ZERO; + static const TPoint3 INF; + +public: + inline TPoint3() {} + template inline TPoint3(const cv::Point3_& rhs) : Base((Type)rhs.x,(Type)rhs.y,(Type)rhs.z) {} + template inline TPoint3(const cv::Matx& rhs) : Base(rhs(0),rhs(1),rhs(2)) {} + template inline TPoint3(const cv::Matx& rhs) : Base(rhs(0),rhs(1),rhs(2)) {} + #ifdef _USE_EIGEN + inline TPoint3(const EVec& rhs) { operator EVecMap () = rhs; } + #endif + explicit inline TPoint3(const TYPE& _x) : Base(_x,_x,_x) {} + inline TPoint3(const TYPE& _x, const TYPE& _y, const TYPE& _z) : Base(_x,_y,_z) {} + template inline TPoint3(const cv::Point_& pt, const T& _z=T(1)) : Base(pt.x,pt.y,_z) {} + template inline TPoint3(const cv::Point_& pt, const T2& _z) : Base(pt.x,pt.y,_z) {} + + template inline TPoint3& operator = (const cv::Point3_& rhs) { Base::operator = (rhs); return *this; } + template inline TPoint3& operator = (const cv::Matx& rhs) { operator Vec& () = rhs; return *this; } + template inline TPoint3& operator = (const cv::Matx& rhs) { operator VecT& () = rhs; return *this; } + #ifdef _USE_EIGEN + inline TPoint3& operator = (const EVec& rhs) { operator EVecMap () = rhs; return *this; } + #endif + + // conversion to another data type + template inline operator TPoint3 () const { return TPoint3((T)x,(T)y,(T)z); } + + // pointer to the first element access + inline const TYPE* ptr() const { return &x; } + inline TYPE* ptr() { return &x; } + + // 1D element access + inline const TYPE& operator [](BYTE i) const { ASSERT(i<3); return ptr()[i]; } + inline TYPE& operator [](BYTE i) { ASSERT(i<3); return ptr()[i]; } + + // Access point as vector equivalent + inline operator const Vec& () const { return *reinterpret_cast(this); } + inline operator Vec& () { return *reinterpret_cast(this); } + + // Access point as transposed vector equivalent + inline operator const VecT& () const { return *reinterpret_cast(this); } + inline operator VecT& () { return *reinterpret_cast(this); } + + #ifdef _USE_EIGEN + // Access point as Eigen equivalent + inline operator EVec () const { return CEVecMap((const TYPE*)this); } + // Access point as Eigen::Map equivalent + inline operator const EVecMap () const { return CEVecMap((const TYPE*)this); } + inline operator EVecMap () { return EVecMap((TYPE*)this); } + #endif + + // rotate point using the given parametrized rotation (axis-angle) + inline void RotateAngleAxis(const TPoint3& rot) { return (*this) = RotateAngleAxis((*this), rot); } + static TPoint3 RotateAngleAxis(const TPoint3& X, const TPoint3& rot); + + #ifdef _USE_BOOST + // serialize + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & boost::serialization::base_object(*this); + } + #endif +}; +template const TPoint3 TPoint3::ZERO(0,0,0); +template const TPoint3 TPoint3::INF(std::numeric_limits::infinity(),std::numeric_limits::infinity(),std::numeric_limits::infinity()); +/*----------------------------------------------------------------*/ +typedef TPoint3 Point3i; +typedef TPoint3 Point3hf; +typedef TPoint3 Point3f; +typedef TPoint3 Point3d; +/*----------------------------------------------------------------*/ + + +// matrix struct +template +class TMatrix : public cv::Matx +{ +public: + typedef TYPE Type; + typedef cv::Matx Base; + typedef cv::Vec Vec; + #ifdef _USE_EIGEN + EIGEN_MAKE_ALIGNED_OPERATOR_NEW_IF_VECTORIZABLE_FIXED_SIZE(TYPE,m*n) + typedef Eigen::Matrix1?Eigen::RowMajor:Eigen::Default)> EMat; + typedef Eigen::Map CEMatMap; + typedef Eigen::Map EMatMap; + #endif + + using Base::val; + using Base::channels; + + enum { elems = m*n }; + + static const TMatrix ZERO; + static const TMatrix IDENTITY; + static const TMatrix INF; + +public: + inline TMatrix() {} + template inline TMatrix(const cv::Matx& rhs) : Base(rhs) {} + template inline TMatrix(const cv::Point_& rhs) : Base(rhs.x, rhs.y) {} + template inline TMatrix(const cv::Point3_& rhs) : Base(rhs.x, rhs.y, rhs.z) {} + inline TMatrix(const cv::Mat& rhs) : Base(rhs) {} + #ifdef _USE_EIGEN + inline TMatrix(const EMat& rhs) { operator EMatMap () = rhs; } + #endif + + TMatrix(TYPE v0); //!< 1x1 matrix + TMatrix(TYPE v0, TYPE v1); //!< 1x2 or 2x1 matrix + TMatrix(TYPE v0, TYPE v1, TYPE v2); //!< 1x3 or 3x1 matrix + TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3); //!< 1x4, 2x2 or 4x1 matrix + TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4); //!< 1x5 or 5x1 matrix + TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5); //!< 1x6, 2x3, 3x2 or 6x1 matrix + TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5, TYPE v6); //!< 1x7 or 7x1 matrix + TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5, TYPE v6, TYPE v7); //!< 1x8, 2x4, 4x2 or 8x1 matrix + TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5, TYPE v6, TYPE v7, TYPE v8); //!< 1x9, 3x3 or 9x1 matrix + TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5, TYPE v6, TYPE v7, TYPE v8, TYPE v9); //!< 1x10, 2x5 or 5x2 or 10x1 matrix + TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, + TYPE v4, TYPE v5, TYPE v6, TYPE v7, + TYPE v8, TYPE v9, TYPE v10, TYPE v11); //!< 1x12, 2x6, 3x4, 4x3, 6x2 or 12x1 matrix + TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, + TYPE v4, TYPE v5, TYPE v6, TYPE v7, + TYPE v8, TYPE v9, TYPE v10, TYPE v11, + TYPE v12, TYPE v13); //!< 1x14, 2x7, 7x2 or 14x1 matrix + TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, + TYPE v4, TYPE v5, TYPE v6, TYPE v7, + TYPE v8, TYPE v9, TYPE v10, TYPE v11, + TYPE v12, TYPE v13, TYPE v14, TYPE v15); //!< 1x16, 4x4 or 16x1 matrix + explicit TMatrix(const TYPE* vals); //!< initialize from a plain array + + TMatrix(const TMatrix& a, const TMatrix& b, cv::Matx_AddOp) : Base(a, b, cv::Matx_AddOp()) {} + TMatrix(const TMatrix& a, const TMatrix& b, cv::Matx_SubOp) : Base(a, b, cv::Matx_SubOp()) {} + template TMatrix(const TMatrix& a, TYPE2 alpha, cv::Matx_ScaleOp) : Base(a, alpha, cv::Matx_ScaleOp()) {} + TMatrix(const TMatrix& a, const TMatrix& b, cv::Matx_MulOp) : Base(a, b, cv::Matx_MulOp()) {} + TMatrix(const TMatrix& a, const TMatrix& b, cv::Matx_DivOp) : Base(a, b, cv::Matx_DivOp()) {} + template TMatrix(const TMatrix& a, const TMatrix& b, cv::Matx_MatMulOp) : Base(a, b, cv::Matx_MatMulOp()) {} + TMatrix(const TMatrix& a, cv::Matx_TOp) : Base(a, cv::Matx_TOp()) {} + + template inline TMatrix& operator = (const cv::Matx& rhs) { Base::operator = (rhs); return *this; } + inline TMatrix& operator = (const cv::Mat& rhs) { Base::operator = (rhs); return *this; } + #ifdef _USE_EIGEN + inline TMatrix& operator = (const EMat& rhs) { operator EMatMap () = rhs; return *this; } + #endif + + inline bool IsEqual(const Base&) const; + inline bool IsEqual(const Base&, TYPE eps) const; + + // 1D element access + inline const TYPE& operator [](size_t i) const { ASSERT(i(this); } + inline operator Vec& () { return *reinterpret_cast(this); } + + #ifdef _USE_EIGEN + // Access point as Eigen equivalent + inline operator EMat () const { return CEMatMap((const TYPE*)val); } + // Access point as Eigen::Map equivalent + inline operator CEMatMap() const { return CEMatMap((const TYPE*)val); } + inline operator EMatMap () { return EMatMap((TYPE*)val); } + #endif + + // calculate right null-space of this matrix ([n,n-m]) + inline TMatrix RightNullSpace(int flags = 0) const; + // calculate right/left null-vector of this matrix ([n/m,1]) + inline TMatrix RightNullVector(int flags = 0) const; + inline TMatrix LeftNullVector(int flags = 0) const; + + #ifdef _USE_BOOST + // serialize + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & boost::serialization::base_object(*this); + } + #endif +}; +template const TMatrix TMatrix::ZERO(TMatrix::zeros()); +template const TMatrix TMatrix::IDENTITY(TMatrix::eye()); +template const TMatrix TMatrix::INF(TMatrix::all(std::numeric_limits::infinity())); +/*----------------------------------------------------------------*/ + + +// generic matrix struct +template +class TDMatrix : public cv::Mat_ +{ +public: + typedef TYPE Type; + typedef cv::Mat_ Base; + typedef cv::Size Size; + #ifdef _USE_EIGEN + typedef Eigen::Matrix EMat; + typedef Eigen::Map CEMatMap; + typedef Eigen::Map EMatMap; + #endif + + using Base::rows; + using Base::cols; + using Base::data; + using Base::step; + using Base::dims; + +public: + inline TDMatrix() {} + inline TDMatrix(const Base& rhs) : Base(rhs) {} + inline TDMatrix(const cv::Mat& rhs) : Base(rhs) {} + inline TDMatrix(const cv::MatExpr& rhs) : Base(rhs) {} + inline TDMatrix(int _rows, int _cols) : Base(_rows, _cols) {} + inline TDMatrix(int _rows, int _cols, TYPE* _data, size_t _step=Base::AUTO_STEP) : Base(_rows, _cols, _data, _step) {} + inline TDMatrix(const Size& sz) : Base(sz) {} + inline TDMatrix(const Size& sz, const TYPE& v) : Base(sz, v) {} + inline TDMatrix(const Size& sz, TYPE* _data, size_t _step=Base::AUTO_STEP) : Base(sz.height, sz.width, _data, _step) {} + #ifdef _USE_EIGEN + inline TDMatrix(const EMat& rhs) { operator EMatMap () = rhs; } + #endif + + inline TDMatrix& operator = (const Base& rhs) { Base::operator=(rhs); return *this; } + inline TDMatrix& operator = (const cv::MatExpr& rhs) { Base::operator=(rhs); return *this; } + #ifdef _USE_EIGEN + inline TDMatrix& operator = (const EMat& rhs) { operator EMatMap () = rhs; return *this; } + #endif + + /// Construct the 2D matrix with the desired size and init its elements + inline void construct(int _rows, int _cols) { + Base::create(_rows, _cols); + for (int i=0; i(i,j)) TYPE; + } + inline void construct(const Size& sz) { construct(sz.height, sz.width); } + inline void destroy() { + for (int i=0; i(i,j)->~TYPE(); + } + + /// Set all elements to the given value + inline void memset(uint8_t v) { ASSERT(dims == 2 && cv::Mat::isContinuous()); ::memset(data, v, row_stride()*rows); } + inline void fill(const TYPE& v) { ASSERT(dims == 2); for (int i=0; i(i,j) = v; } + + /// What is the row stride of the matrix? + inline size_t row_stride() const { ASSERT(dims == 2); return step[0]; } + /// What is the elem stride of the matrix? + inline size_t elem_stride() const { ASSERT(dims == 2 && step[1] == sizeof(TYPE)); return step[1]; } + /// Compute the area of the 2D matrix + inline int area() const { ASSERT(dims == 2); return cols*rows; } + + /// Is this coordinate inside the 2D matrix? + template + static inline typename std::enable_if::value,bool>::type isInside(const cv::Point_& pt, const cv::Size& size) { + return pt.x>=T(0) && pt.y>=T(0) && pt.x + static inline typename std::enable_if::value,bool>::type isInside(const cv::Point_& pt, const cv::Size& size) { + return pt.x>=T(0) && pt.y>=T(0) && pt.x<=T(size.width) && pt.y<=T(size.height); + } + template + inline bool isInside(const cv::Point_& pt) const { + return isInside(pt, Base::size()); + } + + /// Is this coordinate inside the 2D matrix, and not too close to the edges? + /// @param border: the size of the border + inline bool isInsideWithBorder(const Size& pt, int border) const { + return pt.width>=border && pt.height>=border && pt.width + inline typename std::enable_if::value,bool>::type isInsideWithBorder(const cv::Point_& pt, int border) const { + return pt.x>=border && pt.y>=border && pt.x + inline typename std::enable_if::value,bool>::type isInsideWithBorder(const cv::Point_& pt, int border) const { + return pt.x>=T(border) && pt.y>=T(border) && pt.x<=T(Base::size().width-(border+1)) && pt.y<=T(Base::size().height-(border+1)); + } + template + inline typename std::enable_if::value,bool>::type isInsideWithBorder(const cv::Point_& pt) const { + return pt.x>=border && pt.y>=border && pt.x + inline typename std::enable_if::value,bool>::type isInsideWithBorder(const cv::Point_& pt) const { + return pt.x>=T(border) && pt.y>=T(border) && pt.x<=T(Base::size().width-(border+1)) && pt.y<=T(Base::size().height-(border+1)); + } + + template + static inline void clip(TPoint2& ptMin, TPoint2& ptMax, const cv::Size& size) { + if (ptMin.x < T(border)) + ptMin.x = T(border); + if (ptMin.y < T(border)) + ptMin.y = T(border); + if (ptMax.x >= T(size.width-border)) + ptMax.x = T(size.width-(border+1)); + if (ptMax.y >= T(size.height-border)) + ptMax.y = T(size.height-(border+1)); + } + template + inline void clip(TPoint2& ptMin, TPoint2& ptMax) const { + clip(ptMin, ptMax, Base::size()); + } + + /// Remove the given element from the vector + inline void remove(int idx) { + // replace the removed element by the last one and decrease the size + ASSERT(rows == 1 || cols == 1); + const int last = area()-1; + Base::operator()(idx) = Base::operator()(last); + Base::resize((size_t)last); + } + + /** @author koeser + @warning very slow, generic implementation (order "n!"), better to use + matrix decomposition (see BIAS/MathAlgo/Lapack.hh) */ + inline TYPE getDetSquare() const; + + /** @brief computes the adjoint matrix + @author Christian Beder */ + inline TDMatrix getAdjoint() const; + + /** @brief compute square system matrix dest = A^T * A + @param dest holds result of Transpose * this + + If you want to solve A * x = b, where A has more rows than columns, + a common technique is to solve x = (A^T * A)^-1 * A^T * b. + This function provides a fast way to compute A^T*A from A. + @author grest/koeser */ + inline void getSystemMatrix(TDMatrix& dest) const; + + /** @brief componentwise: this = 0.5(this + this^T) yields symmetric matrix + only allowed for square shaped matrices + @author koeser 01/2007 */ + inline void makeSymmetric(); + + /** Return the L1 norm: |a| + |b| + |c| + ... + @author Ingo Thomsen + @date 04/11/2002 + @status untested **/ + inline TYPE getNormL1() const; + + /** Return the L2 norm: a^2 + b^2 + c^2 + ... + @author woelk 07/2004 */ + inline double getNormL2() const; + + /** Kronecker-product with matrix, result in dest */ + void Kronecker(const TDMatrix& B, TDMatrix& dest) const; + + /** @brief swaps two rows + @author woelk 05/2008 www.vision-n.de */ + void SwapRows(int i, int r); + + /** @brief use the Gauss Jordan Algorithm to transform the matrix to + reduced row echelon form. + @author woelk 05/2008 www.vision-n.de */ + void GaussJordan(); + + //! more convenient forms of row and element access operators + const TYPE& operator [](size_t i) const { return cv::Mat::at((int)i); } + TYPE& operator [](size_t i) { return cv::Mat::at((int)i); } + + /// Access an element from the matrix. Bounds checking is only performed in debug mode. + inline const TYPE& operator [] (const Size& pos) const { + ASSERT(isInside(pos) && elem_stride() == sizeof(TYPE)); + return ((const TYPE*)data)[pos.height*row_stride() + pos.width]; + } + inline TYPE& operator [] (const Size& pos) { + ASSERT(isInside(pos) && elem_stride() == sizeof(TYPE)); + return ((TYPE*)data)[pos.height*row_stride() + pos.width]; + } + + /// pointer to the beginning of the matrix data + inline const TYPE* getData() const { ASSERT(cv::Mat::empty() || cv::Mat::isContinuous()); return (const TYPE*)data; } + inline TYPE* getData() { ASSERT(cv::Mat::empty() || cv::Mat::isContinuous()); return (TYPE*)data; } + + #ifdef _USE_EIGEN + // Access point as Eigen equivalent + inline operator EMat () const { return CEMatMap(getData(), rows, cols); } + // Access point as Eigen::Map equivalent + inline operator const CEMatMap () const { return CEMatMap(getData(), rows, cols); } + inline operator EMatMap () { return EMatMap(getData(), rows, cols); } + #endif + + #ifdef _USE_BOOST + // serialize + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & boost::serialization::base_object(*this); + } + #endif +}; +/*----------------------------------------------------------------*/ +typedef TDMatrix DMatrix; +typedef TDMatrix DMatrix8S; +typedef TDMatrix DMatrix8U; +typedef TDMatrix DMatrix32S; +typedef TDMatrix DMatrix32U; +typedef TDMatrix DMatrix32F; +typedef TDMatrix DMatrix64F; +typedef CLISTDEF2(DMatrix) DMatrixArr; +typedef CLISTDEF2(cv::Mat) MatArr; +/*----------------------------------------------------------------*/ + + +// generic vector struct +template +class TDVector : public TDMatrix +{ +public: + typedef TYPE Type; + typedef TDMatrix Base; + typedef typename Base::Base BaseBase; + typedef cv::Size Size; + + using Base::rows; + using Base::cols; + using Base::data; + +public: + inline TDVector() {} + inline TDVector(const Base& rhs) : Base(rhs) {} + inline TDVector(const cv::Mat& rhs) : Base(rhs) {} + inline TDVector(const cv::MatExpr& rhs) : Base(rhs) {} + inline TDVector(int _rows) : Base(_rows, 1) {} + inline TDVector(int _rows, const TYPE& v) : Base(Size(_rows,1), v) {} + inline TDVector(int _rows, TYPE* _data, size_t _step=Base::AUTO_STEP) : Base(_rows, 1, _data, _step) {} + + inline TDVector& operator = (const Base& rhs) { BaseBase::operator=(rhs); return *this; } + inline TDVector& operator = (const BaseBase& rhs) { BaseBase::operator=(rhs); return *this; } + inline TDVector& operator = (const cv::MatExpr& rhs) { BaseBase::operator=(rhs); return *this; } + + //! equivalent to Mat::create(_rows, 1, DataType<_Tp>::type) + inline void create(int _rows) { BaseBase::create(_rows, 1); } + + /** outer product, constructs a matrix. + Often written as v * v^T for col vectors + @author Daniel Grest, Oct 2002 + @status tested */ + TDMatrix getOuterProduct(const TDVector& v) const; + + /** kronecker product + @author woelk 08/2004 */ + void getKroneckerProduct(const TDVector& arg, TDVector& dst) const; +}; +/*----------------------------------------------------------------*/ +typedef TDVector DVector; +typedef TDVector DVector8S; +typedef TDVector DVector8U; +typedef TDVector DVector32S; +typedef TDVector DVector32U; +typedef TDVector DVector32F; +typedef TDVector DVector64F; +typedef CLISTDEF2(DVector) DVectorArr; +/*----------------------------------------------------------------*/ + + +// generic color type +// Here the color order means the order the data is stored on this machine. +// For example BGR means that the blue byte is stored first and the red byte last, +// which correspond to the RGB format on a little-endian machine. +#define _COLORMODE_BGR 1 // little-endian +#define _COLORMODE_RGB 2 // big-endian +#ifndef _COLORMODE +#define _COLORMODE _COLORMODE_BGR +#endif + +template class ColorType +{ +public: + typedef TYPE value_type; + typedef value_type alt_type; + static const value_type ONE; + static const alt_type ALTONE; +}; +template<> class ColorType +{ +public: + typedef uint8_t value_type; + typedef float alt_type; + static const value_type ONE; + static const alt_type ALTONE; +}; +template<> class ColorType +{ +public: + typedef uint32_t value_type; + typedef float alt_type; + static const value_type ONE; + static const alt_type ALTONE; +}; +template<> class ColorType +{ +public: + typedef float value_type; + typedef uint8_t alt_type; + static const value_type ONE; + static const alt_type ALTONE; +}; +template<> class ColorType +{ +public: + typedef double value_type; + typedef uint8_t alt_type; + static const value_type ONE; + static const alt_type ALTONE; +}; +/*----------------------------------------------------------------*/ + +template +struct TPixel { + union { + struct { + #if _COLORMODE == _COLORMODE_BGR + TYPE b; + TYPE g; + TYPE r; + #endif + #if _COLORMODE == _COLORMODE_RGB + TYPE r; + TYPE g; + TYPE b; + #endif + }; + struct { + TYPE c0; + TYPE c1; + TYPE c2; + }; + TYPE c[3]; + }; + typedef typename ColorType::alt_type ALT; + typedef TYPE Type; + typedef TPoint3 Pnt; + static const TPixel BLACK; + static const TPixel WHITE; + static const TPixel GRAY; + static const TPixel RED; + static const TPixel GREEN; + static const TPixel BLUE; + static const TPixel YELLOW; + static const TPixel MAGENTA; + static const TPixel CYAN; + // init + inline TPixel() {} + template inline TPixel(const TPixel& p) + #if _COLORMODE == _COLORMODE_BGR + : b(TYPE(p.b)), g(TYPE(p.g)), r(TYPE(p.r)) {} + #endif + #if _COLORMODE == _COLORMODE_RGB + : r(TYPE(p.r)), g(TYPE(p.g)), b(TYPE(p.b)) {} + #endif + inline TPixel(TYPE _r, TYPE _g, TYPE _b) + #if _COLORMODE == _COLORMODE_BGR + : b(_b), g(_g), r(_r) {} + #endif + #if _COLORMODE == _COLORMODE_RGB + : r(_r), g(_g), b(_b) {} + #endif + inline TPixel(const Pnt& col) : c0(col.x), c1(col.y), c2(col.z) {} + explicit inline TPixel(uint32_t col) + #if _COLORMODE == _COLORMODE_BGR + : b(TYPE(col&0xFF)), g(TYPE((col>>8)&0xFF)), r(TYPE((col>>16)&0xFF)) {} + #endif + #if _COLORMODE == _COLORMODE_RGB + : r(TYPE((col>>16)&0xFF)), g(TYPE((col>>8)&0xFF)), b(TYPE(col&0xFF)) {} + #endif + // set/get from default type + inline TPixel& set(TYPE _r, TYPE _g, TYPE _b) { r = _r; g = _g; b = _b; return *this; } + inline TPixel& set(const TYPE* clr) { c[0] = clr[0]; c[1] = clr[1]; c[2] = clr[2]; return *this; } + inline void get(TYPE& _r, TYPE& _g, TYPE& _b) const { _r = r; _g = g; _b = b; } + inline void get(TYPE* clr) const { clr[0] = c[0]; clr[1] = c[1]; clr[2] = c[2]; } + // set/get from alternative type + inline TPixel& set(ALT _r, ALT _g, ALT _b) { r = TYPE(_r); g = TYPE(_g); b = TYPE(_b); return *this; } + inline TPixel& set(const ALT* clr) { c[0] = TYPE(clr[0]); c[1] = TYPE(clr[1]); c[2] = TYPE(clr[2]); return *this; } + inline void get(ALT& _r, ALT& _g, ALT& _b) const { _r = ALT(r); _g = ALT(g); _b = ALT(b); } + inline void get(ALT* clr) const { clr[0] = ALT(c[0]); clr[1] = ALT(c[1]); clr[2] = ALT(c[2]); } + template inline TPixel::value || !std::is_same::value,T>::type> cast() const { return TPixel(T(r), T(g), T(b)); } + template inline TPixel::value && std::is_same::value,T>::type> cast() const { + return TPixel( + (uint8_t)CLAMP(ROUND2INT(r), 0, 255), + (uint8_t)CLAMP(ROUND2INT(g), 0, 255), + (uint8_t)CLAMP(ROUND2INT(b), 0, 255) + ); + } + // set/get as vector + inline const TYPE& operator[](size_t i) const { ASSERT(i<3); return c[i]; } + inline TYPE& operator[](size_t i) { ASSERT(i<3); return c[i]; } + // access as point equivalent + template inline operator TPoint3() const { return TPoint3(T(c[0]), T(c[1]), T(c[2])); } + // access as vector equivalent + inline operator const Pnt& () const { return *((const Pnt*)this); } + inline operator Pnt& () { return *((Pnt*)this); } + // access as cv::Scalar equivalent + inline operator cv::Scalar () const { return cv::Scalar(c[0], c[1], c[2], TYPE(0)); } + // compare + inline bool operator==(const TPixel& col) const { return (memcmp(c, col.c, sizeof(TPixel)) == 0); } + inline bool operator!=(const TPixel& col) const { return (memcmp(c, col.c, sizeof(TPixel)) != 0); } + // operators + inline TPixel operator*(const TPixel& v) const { return TPixel(r*v.r, g*v.g, b*v.b); } + template inline TPixel operator*(T v) const { return TPixel((TYPE)(v*r), (TYPE)(v*g), (TYPE)(v*b)); } + template inline TPixel& operator*=(T v) { return (*this = operator*(v)); } + inline TPixel operator/(const TPixel& v) const { return TPixel(r/v.r, g/v.g, b/v.b); } + template inline TPixel operator/(T v) const { return operator*(T(1)/v); } + template inline TPixel& operator/=(T v) { return (*this = operator/(v)); } + inline TPixel operator+(const TPixel& v) const { return TPixel(r+v.r, g+v.g, b+v.b); } + template inline TPixel operator+(T v) const { return TPixel((TYPE)(r+v), (TYPE)(g+v), (TYPE)(b+v)); } + template inline TPixel& operator+=(T v) { return (*this = operator+(v)); } + inline TPixel operator-(const TPixel& v) const { return TPixel(r-v.r, g-v.g, b-v.b); } + template inline TPixel operator-(T v) const { return TPixel((TYPE)(r-v), (TYPE)(g-v), (TYPE)(b-v)); } + template inline TPixel& operator-=(T v) { return (*this = operator-(v)); } + inline uint32_t toDWORD() const { return RGBA((uint8_t)r, (uint8_t)g, (uint8_t)b, (uint8_t)0); } + // tools + template + static TPixel colorRamp(VT v, VT vmin, VT vmax); + static TPixel gray2color(ALT v); + #ifdef _USE_BOOST + // serialize + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & c; + } + #endif +}; +template <> inline TPixel& TPixel::set(uint8_t _r, uint8_t _g, uint8_t _b) { r = float(_r)/255; g = float(_g)/255; b = float(_b)/255; return *this; } +template <> inline void TPixel::get(uint8_t& _r, uint8_t& _g, uint8_t& _b) const { _r = (uint8_t)CLAMP(ROUND2INT(r*255), 0, 255); _g = (uint8_t)CLAMP(ROUND2INT(g*255), 0, 255); _b = (uint8_t)CLAMP(ROUND2INT(b*255), 0, 255); } +template <> inline TPixel& TPixel::set(float _r, float _g, float _b) { r = (uint8_t)CLAMP(ROUND2INT(_r*255), 0, 255); g = (uint8_t)CLAMP(ROUND2INT(_g*255), 0, 255); b = (uint8_t)CLAMP(ROUND2INT(_b*255), 0, 255); return *this; } +template <> inline void TPixel::get(float& _r, float& _g, float& _b) const { _r = float(r)/255; _g = float(g)/255; _b = float(b)/255; } +/*----------------------------------------------------------------*/ +typedef TPixel Pixel8U; +typedef TPixel Pixel32F; +typedef TPixel Pixel64F; +/*----------------------------------------------------------------*/ + +template +struct TColor { + union { + struct { + #if _COLORMODE == _COLORMODE_BGR + TYPE b; + TYPE g; + TYPE r; + TYPE a; + #endif + #if _COLORMODE == _COLORMODE_RGB + TYPE a; + TYPE r; + TYPE g; + TYPE b; + #endif + }; + struct { + TYPE c0; + TYPE c1; + TYPE c2; + TYPE c3; + }; + TYPE c[4]; + }; + typedef typename ColorType::alt_type ALT; + typedef TYPE Type; + typedef TPixel Pxl; + typedef TPoint3 Pnt; + static const TColor BLACK; + static const TColor WHITE; + static const TColor GRAY; + static const TColor RED; + static const TColor GREEN; + static const TColor BLUE; + static const TColor YELLOW; + static const TColor MAGENTA; + static const TColor CYAN; + // init + inline TColor() {} + template inline TColor(const TColor& p) + : r(TYPE(p.r)), g(TYPE(p.g)), b(TYPE(p.b)), a(TYPE(p.a)) {} + inline TColor(TYPE _r, TYPE _g, TYPE _b, TYPE _a=ColorType::ONE) + : r(_r), g(_g), b(_b), a(_a) {} + inline TColor(const Pxl& col, TYPE _a=ColorType::ONE) + : r(col.r), g(col.g), b(col.b), a(_a) {} + #if _COLORMODE == _COLORMODE_BGR + inline TColor(const Pnt& col, TYPE _a=ColorType::ONE) + : b(col.x), g(col.y), r(col.z), a(_a) {} + #endif + #if _COLORMODE == _COLORMODE_RGB + inline TColor(const Pnt& col, TYPE _a=ColorType::ONE) + : r(col.x), g(col.y), b(col.z), a(_a) {} + #endif + explicit inline TColor(uint32_t col) + : r(TYPE((col>>16)&0xFF)), g(TYPE((col>>8)&0xFF)), b(TYPE(col&0xFF)), a(TYPE((col>>24)&0xFF)) {} + // set/get from default type + inline TColor& set(TYPE _r, TYPE _g, TYPE _b, TYPE _a=ColorType::ONE) { r = _r; g = _g; b = _b; a = _a; return *this; } + inline TColor& set(const TYPE* clr) { c[0] = clr[0]; c[1] = clr[1]; c[2] = clr[2]; c[3] = clr[3]; return *this; } + inline void get(TYPE& _r, TYPE& _g, TYPE& _b, TYPE& _a) const { _r = r; _g = g; _b = b; _a = a; } + inline void get(TYPE* clr) const { clr[0] = c[0]; clr[1] = c[1]; clr[2] = c[2]; clr[3] = c[3]; } + // set/get from alternative type + inline TColor& set(ALT _r, ALT _g, ALT _b, ALT _a=ColorType::ALTONE) { r = TYPE(_r); g = TYPE(_g); b = TYPE(_b); a = TYPE(_a); return *this; } + inline TColor& set(const ALT* clr) { c[0] = TYPE(clr[0]); c[1] = TYPE(clr[1]); c[2] = TYPE(clr[2]); c[3] = TYPE(clr[3]); return *this; } + inline void get(ALT& _r, ALT& _g, ALT& _b, ALT& _a) const { _r = ALT(r); _g = ALT(g); _b = ALT(b); _a = ALT(a); } + inline void get(ALT* clr) const { clr[0] = ALT(c[0]); clr[1] = ALT(c[1]); clr[2] = ALT(c[2]); clr[3] = ALT(c[3]); } + template inline TColor::value || !std::is_same::value,T>::type> cast() const { return TColor(T(r), T(g), T(b), T(a)); } + template inline TColor::value && std::is_same::value,T>::type> cast() const { + return TColor( + (uint8_t)CLAMP(ROUND2INT(r), 0, 255), + (uint8_t)CLAMP(ROUND2INT(g), 0, 255), + (uint8_t)CLAMP(ROUND2INT(b), 0, 255), + (uint8_t)CLAMP(ROUND2INT(a), 0, 255) + ); + } + // set/get as vector + inline const TYPE& operator[](size_t i) const { ASSERT(i<4); return c[i]; } + inline TYPE& operator[](size_t i) { ASSERT(i<4); return c[i]; } + // access as pixel equivalent + inline operator const Pxl& () const { return *((const Pxl*)this); } + inline operator Pxl& () { return *((Pxl*)this); } + // access as point equivalent + inline operator const Pnt& () const { return *((const Pnt*)this); } + inline operator Pnt& () { return *((Pnt*)this); } + // access as cv::Scalar equivalent + inline operator cv::Scalar () const { return cv::Scalar(c[0], c[1], c[2], c[3]); } + // compare + inline bool operator==(const TColor& col) const { return (memcmp(c, col.c, sizeof(TColor)) == 0); } + inline bool operator!=(const TColor& col) const { return (memcmp(c, col.c, sizeof(TColor)) != 0); } + // operators + inline TColor operator*(const TColor& v) const { return TColor(r*v.r, g*v.g, b*v.b, a*v.a); } + template inline TColor operator*(T v) const { return TColor((TYPE)(v*r), (TYPE)(v*g), (TYPE)(v*b), (TYPE)(v*a)); } + template inline TColor& operator*=(T v) { return (*this = operator*(v)); } + inline TColor operator/(const TColor& v) const { return TColor(r/v.r, g/v.g, b/v.b, a/v.a); } + template inline TColor operator/(T v) const { return operator*(T(1)/v); } + template inline TColor& operator/=(T v) { return (*this = operator/(v)); } + inline TColor operator+(const TColor& v) const { return TColor(r+v.r, g+v.g, b+v.b, a+v.a); } + template inline TColor operator+(T v) const { return TColor((TYPE)(r+v), (TYPE)(g+v), (TYPE)(b+v), (TYPE)(a+v)); } + template inline TColor& operator+=(T v) { return (*this = operator+(v)); } + inline TColor operator-(const TColor& v) const { return TColor(r-v.r, g-v.g, b-v.b, a-v.a); } + template inline TColor operator-(T v) const { return TColor((TYPE)(r-v), (TYPE)(g-v), (TYPE)(b-v), (TYPE)(a-v)); } + template inline TColor& operator-=(T v) { return (*this = operator-(v)); } + inline uint32_t toDWORD() const { return RGBA((uint8_t)r, (uint8_t)g, (uint8_t)b, (uint8_t)a); } + #ifdef _USE_BOOST + // serialize + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & c; + } + #endif +}; +template <> inline TColor& TColor::set(uint8_t _r, uint8_t _g, uint8_t _b, uint8_t _a) { r = float(_r)/255; g = float(_g)/255; b = float(_b)/255; a = float(_a)/255; return *this; } +template <> inline void TColor::get(uint8_t& _r, uint8_t& _g, uint8_t& _b, uint8_t& _a) const { _r = uint8_t(r*255); _g = uint8_t(g*255); _b = uint8_t(b*255); _a = uint8_t(a*255); } +template <> inline TColor& TColor::set(float _r, float _g, float _b, float _a) { r = uint8_t(_r*255); g = uint8_t(_g*255); b = uint8_t(_b*255); a = uint8_t(_a*255); return *this; } +template <> inline void TColor::get(float& _r, float& _g, float& _b, float& _a) const { _r = float(r)/255; _g = float(g)/255; _b = float(b)/255; _a = float(a)/255; } +template <> inline bool TColor::operator==(const TColor& col) const { return (*((const uint32_t*)c) == *((const uint32_t*)col.c)); } +template <> inline bool TColor::operator!=(const TColor& col) const { return (*((const uint32_t*)c) != *((const uint32_t*)col.c)); } +template <> inline uint32_t TColor::toDWORD() const { return *((const uint32_t*)c); } +/*----------------------------------------------------------------*/ +typedef TColor Color8U; +typedef TColor Color32F; +typedef TColor Color64F; +/*----------------------------------------------------------------*/ + + +// structure containing the image pixels +typedef Point2i ImageRef; +template +class TImage : public TDMatrix +{ +public: + typedef TYPE Type; + typedef TDMatrix Base; + typedef cv::Size Size; + typedef typename Base::Base BaseBase; + + using Base::rows; + using Base::cols; + using Base::data; + +public: + inline TImage() {} + inline TImage(const Base& rhs) : Base(rhs) {} + inline TImage(const cv::Mat& rhs) : Base(rhs) {} + inline TImage(const cv::MatExpr& rhs) : Base(rhs) {} + inline TImage(int _rows, int _cols) : Base(_rows, _cols) {} + inline TImage(const Size& sz) : Base(sz) {} + inline TImage(const Size& sz, const TYPE& v) : Base(sz, v) {} + inline TImage(const Size& sz, TYPE* _data, size_t _step=Base::AUTO_STEP) : Base(sz.height, sz.width, _data, _step) {} + #ifdef _SUPPORT_CPP11 + inline TImage(cv::Mat&& rhs) : Base(std::forward(rhs)) {} + + inline TImage& operator = (cv::Mat&& rhs) { BaseBase::operator=(std::forward(rhs)); return *this; } + #endif + inline TImage& operator = (const Base& rhs) { BaseBase::operator=(rhs); return *this; } + inline TImage& operator = (const BaseBase& rhs) { BaseBase::operator=(rhs); return *this; } + inline TImage& operator = (const cv::MatExpr& rhs) { BaseBase::operator=(rhs); return *this; } + + /// What is the image size? + inline int width() const { return cols; } + inline int height() const { return rows; } + + /// What is the pixel stride of the image? + inline size_t pixel_stride() const { return Base::elem_stride(); } + + inline const TYPE& getPixel(int y, int x) const; + + template + TYPE sample(const TPoint2& pt) const; + template + TYPE sampleSafe(const TPoint2& pt) const; + + template + bool sample(TV& v, const TPoint2& pt, const Functor& functor) const; + template + bool sampleSafe(TV& v, const TPoint2& pt, const Functor& functor) const; + template + TYPE sample(const TPoint2& pt, const Functor& functor, const TYPE& dv) const; + template + TYPE sampleSafe(const TPoint2& pt, const Functor& functor, const TYPE& dv) const; + + template + INTERTYPE sample(const SAMPLER& sampler, const TPoint2& pt) const; + + template + void toGray(TImage& out, int code, bool bNormalize=false, bool bSRGB=false) const; + + static cv::Size computeResize(const cv::Size& size, REAL scale); + static cv::Size computeResize(const cv::Size& size, REAL scale, unsigned resizes); + unsigned computeMaxResolution(unsigned& level, unsigned minImageSize=320, unsigned maxImageSize=INT_MAX) const; + static unsigned computeMaxResolution(unsigned width, unsigned height, unsigned& level, unsigned minImageSize=320, unsigned maxImageSize=INT_MAX); + + template + static void RasterizeTriangle(const TPoint2& v1, const TPoint2& v2, const TPoint2& v3, PARSER& parser); + template + static void RasterizeTriangleBary(const TPoint2& v1, const TPoint2& v2, const TPoint2& v3, PARSER& parser); + template + static void RasterizeTriangleDepth(TPoint3 p1, TPoint3 p2, TPoint3 p3, PARSER& parser); + + template + static void DrawLine(const TPoint2& p1, const TPoint2& p2, PARSER& parser); + + typedef void (STCALL *FncDrawPointAntialias) (const ImageRef&, const ImageRef&, float, float, void*); + static bool DrawLineAntialias(Point2f x1, Point2f x2, FncDrawPointAntialias fncDrawPoint, void* pData=NULL); + static bool DrawLineAntialias(const ImageRef& x1, const ImageRef& x2, FncDrawPointAntialias fncDrawPoint, void* pData=NULL); + + template + void DilateMean(TImage& out, const TYPE& invalid) const; + + bool Load(const String&); + bool Save(const String&) const; + + #ifndef _RELEASE + void Show(const String& winname, int delay=0, bool bDestroy=true) const; + #endif + + #ifdef _USE_BOOST + // serialize + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & boost::serialization::base_object(*this); + } + #endif +}; +/*----------------------------------------------------------------*/ +typedef TImage Image8U; +typedef TImage Image16U; +typedef TImage Image16F; +typedef TImage Image32F; +typedef TImage Image64F; +typedef TImage Image8U3; +typedef TImage Image8U4; +typedef TImage Image32F3; +typedef TImage Image32F4; +/*----------------------------------------------------------------*/ + + +template +class TBitMatrix +{ +public: + typedef TYPE Type; + typedef cv::Size Size; + struct Index { + size_t idx; + Type flag; + inline Index() {} + inline Index(size_t _idx, Type _flag) : idx(_idx), flag(_flag) {} + }; + + enum { numBitsPerCell = sizeof(Type)*8 }; + enum { numBitsShift = LOG2I() }; + +public: + inline TBitMatrix() : data(NULL) {} + inline TBitMatrix(int _rows, int _cols=1) : rows(_rows), cols(_cols), data(rows && cols ? new Type[computeLength(rows, cols)] : NULL) {} + inline TBitMatrix(int _rows, int _cols, uint8_t v) : rows(_rows), cols(_cols), data(rows && cols ? new Type[computeLength(rows, cols)] : NULL) { if (!empty()) memset(v); } + inline TBitMatrix(const Size& sz) : rows(sz.height), cols(sz.width), data(rows && cols ? new Type[computeLength(sz)] : NULL) {} + inline TBitMatrix(const Size& sz, uint8_t v) : rows(sz.height), cols(sz.width), data(rows && cols ? new Type[computeLength(sz)] : NULL) { if (!empty()) memset(v); } + inline ~TBitMatrix() { delete[] data; } + + inline void create(int _rows, int _cols=1) { + if (!empty() && rows == _rows && cols == _cols) + return; + rows = _rows; cols = _cols; + if (rows <=0 || cols <= 0) { + release(); + return; + } + delete[] data; + data = new Type[length()]; + } + inline void create(const Size& sz) { create(sz.height, sz.width); } + inline void release() { delete[] data; data = NULL; } + inline void memset(uint8_t v) { ASSERT(!empty()); ::memset(data, v, sizeof(Type)*length()); } + inline void swap(TBitMatrix& m) { + union {int i; Type* d;} tmp; + tmp.i = rows; rows = m.rows; m.rows = tmp.i; + tmp.i = cols; cols = m.cols; m.cols = tmp.i; + tmp.d = data; data = m.data; m.data = tmp.d; + } + + inline void And(const TBitMatrix& m) { + ASSERT(rows == m.rows && cols == m.cols); + int i = length(); + while (i-- > 0) + data[i] &= m.data[i]; + } + inline void Or(const TBitMatrix& m) { + ASSERT(rows == m.rows && cols == m.cols); + int i = length(); + while (i-- > 0) + data[i] |= m.data[i]; + } + inline void XOr(const TBitMatrix& m) { + ASSERT(rows == m.rows && cols == m.cols); + int i = length(); + while (i-- > 0) + data[i] ^= m.data[i]; + } + + inline bool empty() const { return data == NULL; } + inline Size size() const { return Size(cols, rows); } + inline int area() const { return cols*rows; } + inline int length() const { return computeLength(rows, cols); } + + inline bool operator() (int i) const { return isSet(i); } + inline bool operator() (int i, int j) const { return isSet(i,j); } + inline bool operator() (const ImageRef& ir) const { return isSet(ir); } + + inline bool isSet(int i) const { ASSERT(!empty() && i=0 && pt.height>=0 && pt.width + inline bool isInside(const cv::Point_& pt) const { + return int(pt.x)>=0 && int(pt.y)>=0 && int(pt.x)=border && pt.height>=border && pt.width + inline bool isInsideWithBorder(const cv::Point_& pt, int border) const { + return int(pt.x)>=border && int(pt.y)>=border && int(pt.x) + inline bool isInsideWithBorder(const cv::Point_& pt) const { + return int(pt.x)>=border && int(pt.y)>=border && int(pt.x)>numBitsShift; } + static inline int computeLength(int _rows, int _cols) { return computeLength(_rows*_cols); } + static inline int computeLength(const Size& sz) { return computeLength(sz.area()); } + + static inline Index computeIndex(int i) { return Index(i>>numBitsShift, Type(1)<<(i&(numBitsPerCell-1))); } + static inline Index computeIndex(int i, int j, int stride) { return computeIndex(i*stride+j); } + static inline Index computeIndex(const ImageRef& ir, int stride) { return computeIndex(ir.y*stride+ir.x); } + + //! the number of rows and columns + int rows, cols; + //! pointer to the data + Type* data; + +#ifdef _USE_BOOST +protected: + // implement BOOST serialization + friend class boost::serialization::access; + template + void save(Archive& ar, const unsigned int /*version*/) const { + if (empty()) { + const int sz(0); + ar & sz; + return; + } + ar & cols; + ar & rows; + ar & boost::serialization::make_array(data, length()); + } + template + void load(Archive& ar, const unsigned int /*version*/) { + release(); + ar & cols; + if (cols == 0) + return; + ar & rows; + create(rows, cols); + ar & boost::serialization::make_array(data, length()); + } + BOOST_SERIALIZATION_SPLIT_MEMBER() +#endif +}; +/*----------------------------------------------------------------*/ +typedef TBitMatrix BitMatrix; +/*----------------------------------------------------------------*/ + + +// weighted accumulator class that operates on arbitrary types +template +struct TAccumulator { + typedef TYPE Type; + typedef ACCUMTYPE AccumType; + typedef WEIGHTTYPE WeightType; + + AccumType value; + WeightType weight; + + inline TAccumulator() : value(INITTO(static_cast(NULL), 0)), weight(0) {} + inline TAccumulator(const Type& v, const WeightType& w) : value(v), weight(w) {} + inline bool IsEmpty() const { return weight <= 0; } + // adds the given weighted value to the internal value + inline void Add(const Type& v, const WeightType& w) { + value += v*w; + weight += w; + } + inline TAccumulator& operator +=(const TAccumulator& accum) { + value += accum.value; + weight += accum.weight; + return *this; + } + inline TAccumulator operator +(const TAccumulator& accum) const { + return TAccumulator( + value + accum.value, + weight + accum.weight + ); + } + // subtracts the given weighted value to the internal value + inline void Sub(const Type& v, const WeightType& w) { + value -= v*w; + weight -= w; + } + inline TAccumulator& operator -=(const TAccumulator& accum) { + value -= accum.value; + weight -= accum.weight; + return *this; + } + inline TAccumulator operator -(const TAccumulator& accum) const { + return TAccumulator( + value - accum.value, + weight - accum.weight + ); + } + // returns the normalized version of the internal value + inline AccumType NormalizedFull() const { + return value / weight; + } + inline Type Normalized() const { + return Type(NormalizedFull()); + } + #ifdef _USE_BOOST + // implement BOOST serialization + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & value; + ar & weight; + } + #endif +}; +/*----------------------------------------------------------------*/ + + +// structure used for sorting some indices by their score (decreasing by default) +template +struct TIndexScore { + IndexType idx; + ScoreType score; + inline TIndexScore() {} + inline TIndexScore(IndexType _idx, ScoreType _score) : idx(_idx), score(_score) {} + // compare by index (increasing) + inline bool operator<(IndexType i) const { return (idx < i); } + inline bool operator==(IndexType i) const { return (idx == i); } + // compare by score (decreasing) + inline bool operator<(const TIndexScore& r) const { return (score > r.score); } + inline bool operator==(const TIndexScore& r) const { return (score == r.score); } + static bool STCALL CompareByIndex(const TIndexScore& l, const TIndexScore& r) { return (l.idx < r.idx); } + static bool STCALL CompareByScore(const TIndexScore& l, const TIndexScore& r) { return (r.score < l.score); } + #ifdef _USE_BOOST + // implement BOOST serialization + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & idx; + ar & score; + } + #endif +}; +/*----------------------------------------------------------------*/ +typedef TIndexScore IndexScore; +typedef CLISTDEF0(IndexScore) IndexScoreArr; +/*----------------------------------------------------------------*/ + + +// structure describing a pair of indices as one long index +struct PairIdx { + typedef uint64_t PairIndex; + typedef uint32_t Index; + union { + PairIndex idx; + Index indices[2]; + struct { + #if __BYTE_ORDER == __LITTLE_ENDIAN + Index j; + Index i; + #else + Index i; + Index j; + #endif + }; + }; + inline PairIdx() {} + inline PairIdx(PairIndex _idx) : idx(_idx) {} + inline PairIdx(Index _i, Index _j) + #if __BYTE_ORDER == __LITTLE_ENDIAN + : j(_j), i(_i) {} + #else + : i(_i), j(_j) {} + #endif + // get index + inline operator PairIndex () const { return idx; } + inline Index operator[](unsigned n) const { + ASSERT(n < 2); + #if __BYTE_ORDER == __LITTLE_ENDIAN + return indices[(n+1)%2]; + #else + return indices[n]; + #endif + } + inline Index& operator[](unsigned n) { + ASSERT(n < 2); + #if __BYTE_ORDER == __LITTLE_ENDIAN + return indices[(n+1)%2]; + #else + return indices[n]; + #endif + } + // compare by index (increasing) + inline bool operator<(const PairIdx& r) const { return (idx < r.idx); } + inline bool operator==(const PairIdx& r) const { return (idx == r.idx); } +}; +/*----------------------------------------------------------------*/ +typedef CLISTDEF0(PairIdx) PairIdxArr; +inline PairIdx MakePairIdx(uint32_t idxImageA, uint32_t idxImageB) { + return (idxImageA inline String cvMat2String(const TMatrix& mat, LPCSTR format="% 10.4f ") { return cvMat2String(cv::Mat(mat), format); } +template inline String cvMat2String(const TPoint3& pt, LPCSTR format="% 10.4f ") { return cvMat2String(cv::Mat(pt), format); } +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + + +#ifdef _USE_EIGEN + +// Implement SO3 and SO2 lie groups +// as in TooN library: https://github.com/edrosten/TooN +// Copyright (C) 2005,2009 Tom Drummond (twd20@cam.ac.uk) + +namespace Eigen { + +/// Class to represent a three-dimensional rotation matrix. Three-dimensional rotation +/// matrices are members of the Special Orthogonal Lie group SO3. This group can be parameterized +/// three numbers (a vector in the space of the Lie Algebra). In this class, the three parameters are the +/// finite rotation vector, i.e. a three-dimensional vector whose direction is the axis of rotation +/// and whose length is the angle of rotation in radians. Exponentiating this vector gives the matrix, +/// and the logarithm of the matrix gives this vector. +template +class SO3 +{ +public: + template + friend std::istream& operator>>(std::istream& is, SO3

& rhs); + + typedef Matrix Mat3; + typedef Matrix Vec3; + + /// Default constructor. Initializes the matrix to the identity (no rotation) + inline SO3() : mat(Mat3::Identity()) {} + + /// Construct from a rotation matrix. + inline SO3(const Mat3& rhs) : mat(rhs) {} + + /// Construct from the axis of rotation (and angle given by the magnitude). + inline SO3(const Vec3& v) : mat(exp(v)) {} + + /// creates an SO3 as a rotation that takes Vector a into the direction of Vector b + /// with the rotation axis along a ^ b. If |a ^ b| == 0, it creates the identity rotation. + /// An assertion will fail if Vector a and Vector b are in exactly opposite directions. + /// @param a source Vector + /// @param b target Vector + SO3(const Vec3& a, const Vec3& b) { + ASSERT(a.size() == 3); + ASSERT(b.size() == 3); + Vec3 n(a.cross(b)); + const Precision nrmSq(n.squaredNorm()); + if (nrmSq == Precision(0)) { + // check that the vectors are in the same direction if cross product is 0; if not, + // this means that the rotation is 180 degrees, which leads to an ambiguity in the rotation axis + ASSERT(a.dot(b) >= Precision(0)); + mat = Mat3::Identity(); + return; + } + n *= Precision(1)/sqrt(nrmSq); + Mat3 R1; + R1.col(0) = a.normalized(); + R1.col(1) = n; + R1.col(2) = R1.col(0).cross(n); + mat.col(0) = b.normalized(); + mat.col(1) = n; + mat.col(2) = mat.col(0).cross(n); + mat = mat * R1.transpose(); + } + + /// Assignment operator from a general matrix. This also calls coerce() + /// to make sure that the matrix is a valid rotation matrix. + inline SO3& operator=(const Mat3& rhs) { + mat = rhs; + coerce(); + return *this; + } + + /// Modifies the matrix to make sure it is a valid rotation matrix. + void coerce() { + mat.row(0).normalize(); + const Precision d01(mat.row(0).dot(mat.row(1))); + mat.row(1) -= mat.row(0) * d01; + mat.row(1).normalize(); + const Precision d02(mat.row(0).dot(mat.row(2))); + mat.row(2) -= mat.row(0) * d02; + const Precision d12(mat.row(1).dot(mat.row(2))); + mat.row(2) -= mat.row(1) * d12; + mat.row(2).normalize(); + // check for positive determinant <=> right handed coordinate system of row vectors + ASSERT(mat.row(0).cross(mat.row(1)).dot(mat.row(2)) > 0); + } + + /// Exponentiate a vector in the Lie algebra to generate a new SO3. + /// See the Detailed Description for details of this vector. + inline Mat3 exp(const Vec3& vect) const; + + /// Take the logarithm of the matrix, generating the corresponding vector in the Lie Algebra. + /// See the Detailed Description for details of this vector. + inline Vec3 ln() const; + + /// Right-multiply by another rotation matrix + template + inline SO3& operator *=(const SO3

& rhs) { + *this = *this * rhs; + return *this; + } + + /// Right-multiply by another rotation matrix + inline SO3 operator *(const SO3& rhs) const { return SO3(*this, rhs); } + + /// Returns the SO3 as a Matrix<3> + inline const Mat3& get_matrix() const { return mat; } + + /// Returns the i-th generator. The generators of a Lie group are the basis + /// for the space of the Lie algebra. For %SO3, the generators are three + /// \f$3\times3\f$ matrices representing the three possible (linearized) + /// rotations. + inline static Mat3 generator(int i) { + Mat3 result(Mat3::Zero()); + result((i+1)%3,(i+2)%3) = Precision(-1); + result((i+2)%3,(i+1)%3) = Precision( 1); + return result; + } + + /// Returns the i-th generator times pos + inline static Vec3 generator_field(int i, const Vec3& pos) { + Vec3 result; + result(i) = Precision(0); + result((i+1)%3) = -pos((i+2)%3); + result((i+2)%3) = pos((i+1)%3); + return result; + } + + template + inline SO3(const SO3& a, const SO3& b) : mat(a.get_matrix()*b.get_matrix()) {} + +protected: + Mat3 mat; + + #ifdef _USE_BOOST + // implement BOOST serialization + friend class boost::serialization::access; + template + void save(Archive& ar, const unsigned int /*version*/) const + { + Vec3 comp(ln()); + ar & comp; + } + template + void load(Archive& ar, const unsigned int /*version*/) + { + Vec3 comp; + ar & comp; + mat = exp(comp); + } + BOOST_SERIALIZATION_SPLIT_MEMBER() + #endif +}; +/*----------------------------------------------------------------*/ + + +/// Class to represent a two-dimensional rotation matrix. Two-dimensional rotation +/// matrices are members of the Special Orthogonal Lie group SO2. This group can be parameterized +/// with one number (the rotation angle). +template +class SO2 +{ +public: + template + friend std::istream& operator>>(std::istream&, SO2

&); + + typedef Matrix Mat2; + + /// Default constructor. Initializes the matrix to the identity (no rotation) + inline SO2() : mat(Mat2::Identity()) {} + + /// Construct from a rotation matrix. + inline SO2(const Mat2& rhs) : mat(rhs) {} + + /// Construct from an angle. + inline SO2(const Precision l) : mat(exp(l)) {} + + /// Assignment operator from a general matrix. This also calls coerce() + /// to make sure that the matrix is a valid rotation matrix. + inline SO2& operator=(const Mat2& rhs) { + mat = rhs; + coerce(); + return *this; + } + + /// Modifies the matrix to make sure it is a valid rotation matrix. + inline void coerce() { + mat.row(0).normalize(); + mat.row(1) = (mat.row(1) - mat.row(0) * (mat.row(0).dot(mat.row(1)))).normalized(); + } + + /// Exponentiate an angle in the Lie algebra to generate a new SO2. + inline Mat2 exp(const Precision& d) const; + + /// extracts the rotation angle from the SO2 + inline Precision ln() const; + + /// Self right-multiply by another rotation matrix + inline SO2& operator *=(const SO2& rhs) { + mat = mat*rhs.get_matrix(); + return *this; + } + + /// Right-multiply by another rotation matrix + inline SO2 operator *(const SO2& rhs) const { return SO2(*this, rhs); } + + /// Returns the SO2 as a Matrix<2> + inline const Mat2& get_matrix() const { return mat; } + + /// returns generator matrix + inline static Mat2 generator() { + Mat2 result; + result(0,0) = Precision(0); result(0,1) = Precision(-1); + result(1,0) = Precision(1); result(1,1) = Precision(0); + return result; + } + +protected: + Mat2 mat; + + #ifdef _USE_BOOST + // implement BOOST serialization + friend class boost::serialization::access; + template + void save(Archive& ar, const unsigned int /*version*/) const + { + Precision comp(ln()); + ar & comp; + } + template + void load(Archive& ar, const unsigned int /*version*/) + { + Precision comp; + ar & comp; + mat = exp(comp); + } + BOOST_SERIALIZATION_SPLIT_MEMBER() + #endif +}; +/*----------------------------------------------------------------*/ + +} // namespace Eigen + +#endif // _USE_EIGEN + +#include "../Math/LMFit/lmmin.h" +#include "Types.inl" +#include "Util.inl" +#include "Rotation.h" +#include "Sphere.h" +#include "AABB.h" +#include "OBB.h" +#include "Plane.h" +#include "Ray.h" +#include "Line.h" +#include "Octree.h" +#include "UtilCUDA.h" + +#endif // __SEACAVE_TYPES_H__ diff --git a/libs/Common/Types.inl b/libs/Common/Types.inl new file mode 100644 index 0000000..b2d6ed2 --- /dev/null +++ b/libs/Common/Types.inl @@ -0,0 +1,4039 @@ +//////////////////////////////////////////////////////////////////// +// Types.inl +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace std { + +//namespace tr1 { +// Specializations for unordered containers +template <> struct hash +{ + typedef SEACAVE::ImageRef argument_type; + typedef size_t result_type; + result_type operator()(const argument_type& v) const { + return std::hash()((const uint64_t&)v); + } +}; +//} // namespace tr1 + +} // namespace std + + +#if CV_MAJOR_VERSION > 3 +template +INT_TYPE cvRANSACUpdateNumIters(REAL_TYPE p, REAL_TYPE ep, INT_TYPE modelPoints, INT_TYPE maxIters) +{ + ASSERT(p>=0 && p<=1); + ASSERT(ep>=0 && ep<=1); + // avoid inf's & nan's + REAL_TYPE num = MAXF(REAL_TYPE(1)-p, SEACAVE::EPSILONTOLERANCE()); + REAL_TYPE denom = REAL_TYPE(1)-POWI(REAL_TYPE(1)-ep, modelPoints); + if (denom < SEACAVE::EPSILONTOLERANCE()) + return 0; + num = SEACAVE::LOGN(num); + denom = SEACAVE::LOGN(denom); + return (denom >= 0 || -num >= (-denom)*maxIters ? maxIters : ROUND2INT(num/denom)); +} +#endif + +namespace cv { + +#if CV_MAJOR_VERSION > 2 +template<> class DataType +{ +public: + typedef unsigned value_type; + typedef value_type work_type; + typedef value_type channel_type; + typedef value_type vec_type; + enum { generic_type = 0, + depth = CV_32S, + channels = 1, + fmt = (int)'i', + type = CV_MAKETYPE(depth, channels) + }; +}; +#else +#if CV_MINOR_VERSION < 4 +template static inline +_AccTp normL2Sqr(const _Tp* a, int n) +{ + _AccTp s = 0; + int i=0; + #if CV_ENABLE_UNROLLED + for (; i <= n - 4; i += 4) { + _AccTp v0 = a[i], v1 = a[i+1], v2 = a[i+2], v3 = a[i+3]; + s += v0*v0 + v1*v1 + v2*v2 + v3*v3; + } + #endif + for (; i < n; i++) { + _AccTp v = a[i]; + s += v*v; + } + return s; +} +#endif + +#if CV_MINOR_VERSION < 4 || CV_SUBMINOR_VERSION < 11 +// Convenience creation functions. In the far future, there may be variadic templates here. +template +Ptr makePtr() +{ + return Ptr(new T()); +} +template +Ptr makePtr(const A1& a1) +{ + return Ptr(new T(a1)); +} +template +Ptr makePtr(const A1& a1, const A2& a2) +{ + return Ptr(new T(a1, a2)); +} +template +Ptr makePtr(const A1& a1, const A2& a2, const A3& a3) +{ + return Ptr(new T(a1, a2, a3)); +} +template +Ptr makePtr(const A1& a1, const A2& a2, const A3& a3, const A4& a4) +{ + return Ptr(new T(a1, a2, a3, a4)); +} +template +Ptr makePtr(const A1& a1, const A2& a2, const A3& a3, const A4& a4, const A5& a5) +{ + return Ptr(new T(a1, a2, a3, a4, a5)); +} +template +Ptr makePtr(const A1& a1, const A2& a2, const A3& a3, const A4& a4, const A5& a5, const A6& a6) +{ + return Ptr(new T(a1, a2, a3, a4, a5, a6)); +} +template +Ptr makePtr(const A1& a1, const A2& a2, const A3& a3, const A4& a4, const A5& a5, const A6& a6, const A7& a7) +{ + return Ptr(new T(a1, a2, a3, a4, a5, a6, a7)); +} +template +Ptr makePtr(const A1& a1, const A2& a2, const A3& a3, const A4& a4, const A5& a5, const A6& a6, const A7& a7, const A8& a8) +{ + return Ptr(new T(a1, a2, a3, a4, a5, a6, a7, a8)); +} +template +Ptr makePtr(const A1& a1, const A2& a2, const A3& a3, const A4& a4, const A5& a5, const A6& a6, const A7& a7, const A8& a8, const A9& a9) +{ + return Ptr(new T(a1, a2, a3, a4, a5, a6, a7, a8, a9)); +} +template +Ptr makePtr(const A1& a1, const A2& a2, const A3& a3, const A4& a4, const A5& a5, const A6& a6, const A7& a7, const A8& a8, const A9& a9, const A10& a10) +{ + return Ptr(new T(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)); +} +#endif + +// property implementation macros +#define CV_IMPL_PROPERTY_RO(type, name, member) \ + inline type get##name() const { return member; } + +#define CV_HELP_IMPL_PROPERTY(r_type, w_type, name, member) \ + CV_IMPL_PROPERTY_RO(r_type, name, member) \ + inline void set##name(w_type val) { member = val; } + +#define CV_HELP_WRAP_PROPERTY(r_type, w_type, name, internal_name, internal_obj) \ + r_type get##name() const { return internal_obj.get##internal_name(); } \ + void set##name(w_type val) { internal_obj.set##internal_name(val); } + +#define CV_IMPL_PROPERTY(type, name, member) CV_HELP_IMPL_PROPERTY(type, type, name, member) +#define CV_IMPL_PROPERTY_S(type, name, member) CV_HELP_IMPL_PROPERTY(type, const type &, name, member) + +#define CV_WRAP_PROPERTY(type, name, internal_name, internal_obj) CV_HELP_WRAP_PROPERTY(type, type, name, internal_name, internal_obj) +#define CV_WRAP_PROPERTY_S(type, name, internal_name, internal_obj) CV_HELP_WRAP_PROPERTY(type, const type &, name, internal_name, internal_obj) + +#define CV_WRAP_SAME_PROPERTY(type, name, internal_obj) CV_WRAP_PROPERTY(type, name, name, internal_obj) +#define CV_WRAP_SAME_PROPERTY_S(type, name, internal_obj) CV_WRAP_PROPERTY_S(type, name, name, internal_obj) +#endif + +//! copy every second source image element to the destination image +inline void downsample2x(InputArray _src, OutputArray _dst) +{ + Mat src(_src.getMat()); + #if 1 + _dst.create((src.rows+1)/2, (src.cols+1)/2, src.type()); + #else + if (_dst.empty()) { + // create a new matrix + _dst.create(src.rows/2, src.cols/2, src.type()); + } else { + // overwrite elements in the existing matrix + ASSERT(src.rows > 0 && (unsigned)(src.rows-_dst.size().height*2) <= 1); + ASSERT(src.cols > 0 && (unsigned)(src.cols-_dst.size().width*2) <= 1); + ASSERT(src.type() == _dst.type()); + } + #endif + Mat dst(_dst.getMat()); + ASSERT(src.elemSize() == dst.elemSize()); + switch (src.elemSize()) { + case 1: + for (int i=0; i(i,j) = src.at(2*i,2*j); + break; + case 2: + for (int i=0; i(i,j) = src.at(2*i,2*j); + break; + case 4: + for (int i=0; i(i,j) = src.at(2*i,2*j); + break; + case 8: + for (int i=0; i(i,j) = src.at(2*i,2*j); + break; + default: + for (int i=0; i 0 && src.rows*2-1 <= _dst.size().height); + ASSERT(src.cols > 0 && src.cols*2-1 <= _dst.size().width); + ASSERT(src.type() == _dst.type()); + } + Mat dst(_dst.getMat()); + ASSERT(src.elemSize() == dst.elemSize()); + switch (src.elemSize()) { + case 1: + for (int i=0; i(2*i,2*j) = src.at(i,j); + break; + case 2: + for (int i=0; i(2*i,2*j) = src.at(i,j); + break; + case 4: + for (int i=0; i(2*i,2*j) = src.at(i,j); + break; + case 8: + for (int i=0; i(2*i,2*j) = src.at(i,j); + break; + default: + for (int i=0; i const typename ColorType::value_type ColorType::ONE(1); +template const typename ColorType::alt_type ColorType::ALTONE(1); + +template const TPixel TPixel::BLACK (0, 0, 0); +template const TPixel TPixel::WHITE (ColorType::ONE, ColorType::ONE, ColorType::ONE); +template const TPixel TPixel::GRAY (0.8f*ColorType::ONE, 0.8f*ColorType::ONE, 0.8f*ColorType::ONE); +template const TPixel TPixel::RED (ColorType::ONE, 0, 0); +template const TPixel TPixel::GREEN (0, ColorType::ONE, 0); +template const TPixel TPixel::BLUE (0, 0, ColorType::ONE); +template const TPixel TPixel::YELLOW (ColorType::ONE, ColorType::ONE, 0); +template const TPixel TPixel::MAGENTA (ColorType::ONE, 0, ColorType::ONE); +template const TPixel TPixel::CYAN (0, ColorType::ONE, ColorType::ONE); + +template const TColor TColor::BLACK (0, 0, 0, ColorType::ONE); +template const TColor TColor::WHITE (ColorType::ONE, ColorType::ONE, ColorType::ONE, ColorType::ONE); +template const TColor TColor::GRAY (0.8f*ColorType::ONE, 0.8f*ColorType::ONE, 0.8f*ColorType::ONE, ColorType::ONE); +template const TColor TColor::RED (ColorType::ONE, 0, 0, ColorType::ONE); +template const TColor TColor::GREEN (0, ColorType::ONE, 0, ColorType::ONE); +template const TColor TColor::BLUE (0, 0, ColorType::ONE, ColorType::ONE); +template const TColor TColor::YELLOW (ColorType::ONE, ColorType::ONE, 0, ColorType::ONE); +template const TColor TColor::MAGENTA (ColorType::ONE, 0, ColorType::ONE, ColorType::ONE); +template const TColor TColor::CYAN (0, ColorType::ONE, ColorType::ONE, ColorType::ONE); + +#ifdef _SUPPORT_CPP11 +template +inline typename std::enable_if::value, size_t>::type +SizeOfArray(const A&) { + return std::extent::value; +} +#else +template +inline size_t +SizeOfArray(const T(&)[N]) { + return N; +} +#endif + + +// C L A S S ////////////////////////////////////////////////////// + +// square +template +FORCEINLINE SEACAVE::TPoint2 SQUARE(const SEACAVE::TPoint2& v) +{ + return SEACAVE::TPoint2(SQUARE(v.x), SQUARE(v.y)); +} +template +FORCEINLINE SEACAVE::TPoint3 SQUARE(const SEACAVE::TPoint3& v) +{ + return SEACAVE::TPoint3(SQUARE(v.x), SQUARE(v.y), SQUARE(v.z)); +} +template +FORCEINLINE SEACAVE::TMatrix SQUARE(const SEACAVE::TMatrix& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i +FORCEINLINE SEACAVE::TPoint2 SQUARE(const cv::Point_& v) +{ + return SEACAVE::TPoint2(SQUARE(v.x), SQUARE(v.y)); +} +template +FORCEINLINE SEACAVE::TPoint3 SQUARE(const cv::Point3_& v) +{ + return SEACAVE::TPoint3(SQUARE(v.x), SQUARE(v.y), SQUARE(v.z)); +} +template +FORCEINLINE SEACAVE::TMatrix SQUARE(const cv::Matx& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i +FORCEINLINE SEACAVE::TPixel SQUARE(const SEACAVE::TPixel& v) +{ + return SEACAVE::TPixel(SQUARE(v.r), SQUARE(v.g), SQUARE(v.b)); +} +template +FORCEINLINE SEACAVE::TColor SQUARE(const SEACAVE::TColor& v) +{ + return SEACAVE::TColor(SQUARE(v.r), SQUARE(v.g), SQUARE(v.b), SQUARE(v.a)); +} + +// sqrt +template +FORCEINLINE SEACAVE::TPoint2 SQRT(const SEACAVE::TPoint2& v) +{ + return SEACAVE::TPoint2(SQRT(v.x), SQRT(v.y)); +} +template +FORCEINLINE SEACAVE::TPoint3 SQRT(const SEACAVE::TPoint3& v) +{ + return SEACAVE::TPoint3(SQRT(v.x), SQRT(v.y), SQRT(v.z)); +} +template +FORCEINLINE SEACAVE::TMatrix SQRT(const SEACAVE::TMatrix& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i +FORCEINLINE SEACAVE::TPoint2 SQRT(const cv::Point_& v) +{ + return SEACAVE::TPoint2(SQRT(v.x), SQRT(v.y)); +} +template +FORCEINLINE SEACAVE::TPoint3 SQRT(const cv::Point3_& v) +{ + return SEACAVE::TPoint3(SQRT(v.x), SQRT(v.y), SQRT(v.z)); +} +template +FORCEINLINE SEACAVE::TMatrix SQRT(const cv::Matx& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i +FORCEINLINE SEACAVE::TPixel SQRT(const SEACAVE::TPixel& v) +{ + return SEACAVE::TPixel(SQRT(v.r), SQRT(v.g), SQRT(v.b)); +} +template +FORCEINLINE SEACAVE::TColor SQRT(const SEACAVE::TColor& v) +{ + return SEACAVE::TColor(SQRT(v.r), SQRT(v.g), SQRT(v.b), SQRT(v.a)); +} + +// exp +template +FORCEINLINE SEACAVE::TPoint2 EXP(const SEACAVE::TPoint2& v) +{ + return SEACAVE::TPoint2(EXP(v.x), EXP(v.y)); +} +template +FORCEINLINE SEACAVE::TPoint3 EXP(const SEACAVE::TPoint3& v) +{ + return SEACAVE::TPoint3(EXP(v.x), EXP(v.y), EXP(v.z)); +} +template +FORCEINLINE SEACAVE::TMatrix EXP(const SEACAVE::TMatrix& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i +FORCEINLINE SEACAVE::TPoint2 EXP(const cv::Point_& v) +{ + return SEACAVE::TPoint2(EXP(v.x), EXP(v.y)); +} +template +FORCEINLINE SEACAVE::TPoint3 EXP(const cv::Point3_& v) +{ + return SEACAVE::TPoint3(EXP(v.x), EXP(v.y), EXP(v.z)); +} +template +FORCEINLINE SEACAVE::TMatrix EXP(const cv::Matx& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i +FORCEINLINE SEACAVE::TPixel EXP(const SEACAVE::TPixel& v) +{ + return SEACAVE::TPixel(EXP(v.r), EXP(v.g), EXP(v.b)); +} +template +FORCEINLINE SEACAVE::TColor EXP(const SEACAVE::TColor& v) +{ + return SEACAVE::TColor(EXP(v.r), EXP(v.g), EXP(v.b), EXP(v.a)); +} + +// loge +template +FORCEINLINE SEACAVE::TPoint2 LOGN(const SEACAVE::TPoint2& v) +{ + return SEACAVE::TPoint2(LOGN(v.x), LOGN(v.y)); +} +template +FORCEINLINE SEACAVE::TPoint3 LOGN(const SEACAVE::TPoint3& v) +{ + return SEACAVE::TPoint3(LOGN(v.x), LOGN(v.y), LOGN(v.z)); +} +template +FORCEINLINE SEACAVE::TMatrix LOGN(const SEACAVE::TMatrix& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i +FORCEINLINE SEACAVE::TPoint2 LOGN(const cv::Point_& v) +{ + return SEACAVE::TPoint2(LOGN(v.x), LOGN(v.y)); +} +template +FORCEINLINE SEACAVE::TPoint3 LOGN(const cv::Point3_& v) +{ + return SEACAVE::TPoint3(LOGN(v.x), LOGN(v.y), LOGN(v.z)); +} +template +FORCEINLINE SEACAVE::TMatrix LOGN(const cv::Matx& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i +FORCEINLINE SEACAVE::TPixel LOGN(const SEACAVE::TPixel& v) +{ + return SEACAVE::TPixel(LOGN(v.r), LOGN(v.g), LOGN(v.b)); +} +template +FORCEINLINE SEACAVE::TColor LOGN(const SEACAVE::TColor& v) +{ + return SEACAVE::TColor(LOGN(v.r), LOGN(v.g), LOGN(v.b), LOGN(v.a)); +} + +// abs +template +FORCEINLINE SEACAVE::TPoint2 ABS(const SEACAVE::TPoint2& v) +{ + return SEACAVE::TPoint2(ABS(v.x), ABS(v.y)); +} +template +FORCEINLINE SEACAVE::TPoint3 ABS(const SEACAVE::TPoint3& v) +{ + return SEACAVE::TPoint3(ABS(v.x), ABS(v.y), ABS(v.z)); +} +template +FORCEINLINE SEACAVE::TMatrix ABS(const SEACAVE::TMatrix& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i +FORCEINLINE SEACAVE::TPoint2 ABS(const cv::Point_& v) +{ + return SEACAVE::TPoint2(ABS(v.x), ABS(v.y)); +} +template +FORCEINLINE SEACAVE::TPoint3 ABS(const cv::Point3_& v) +{ + return SEACAVE::TPoint3(ABS(v.x), ABS(v.y), ABS(v.z)); +} +template +FORCEINLINE SEACAVE::TMatrix ABS(const cv::Matx& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i +FORCEINLINE bool ISZERO(const cv::Point_& v) +{ + return (ISZERO(v.x) && ISZERO(v.y)); +} +template +FORCEINLINE bool ISZERO(const cv::Point3_& v) +{ + return (ISZERO(v.x) && ISZERO(v.y) && ISZERO(v.z)); +} +template +FORCEINLINE bool ISZERO(const cv::Matx& v) +{ + for (int i=0; i +FORCEINLINE bool ISEQUAL(const cv::Point_& v1, const cv::Point_& v2) +{ + return (ISEQUAL(v1.x,v2.x) && ISEQUAL(v1.y,v2.y)); +} +template +FORCEINLINE bool ISEQUAL(const cv::Point3_& v1, const cv::Point3_& v2) +{ + return (ISEQUAL(v1.x,v2.x) && ISEQUAL(v1.y,v2.y) && ISEQUAL(v1.z,v2.z)); +} +template +FORCEINLINE bool ISEQUAL(const cv::Matx& v1, const cv::Matx& v2) +{ + for (int i=0; i +FORCEINLINE SEACAVE::TPoint2 Floor2Int(const cv::Point_& v) +{ + return SEACAVE::TPoint2(FLOOR2INT(v.x), FLOOR2INT(v.y)); +} +template +FORCEINLINE SEACAVE::TPoint2 Ceil2Int(const cv::Point_& v) +{ + return SEACAVE::TPoint2(CEIL2INT(v.x), CEIL2INT(v.y)); +} +template +FORCEINLINE SEACAVE::TPoint2 Round2Int(const cv::Point_& v) +{ + return SEACAVE::TPoint2(ROUND2INT(v.x), ROUND2INT(v.y)); +} + +template +FORCEINLINE SEACAVE::TPoint3 Floor2Int(const cv::Point3_& v) +{ + return SEACAVE::TPoint3(FLOOR2INT(v.x), FLOOR2INT(v.y), FLOOR2INT(v.z)); +} +template +FORCEINLINE SEACAVE::TPoint3 Ceil2Int(const cv::Point3_& v) +{ + return SEACAVE::TPoint3(CEIL2INT(v.x), CEIL2INT(v.y), CEIL2INT(v.z)); +} +template +FORCEINLINE SEACAVE::TPoint3 Round2Int(const cv::Point3_& v) +{ + return SEACAVE::TPoint3(ROUND2INT(v.x), ROUND2INT(v.y), ROUND2INT(v.z)); +} + +template +FORCEINLINE SEACAVE::TMatrix Floor2Int(const cv::Matx& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i(v.val[i]); + return nv; +} +template +FORCEINLINE SEACAVE::TMatrix Ceil2Int(const cv::Matx& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i(v.val[i]); + return nv; +} +template +FORCEINLINE SEACAVE::TMatrix Round2Int(const cv::Matx& v) +{ + SEACAVE::TMatrix nv; + for (int i=0; i(v.val[i]); + return nv; +} + +// ISFINITE +template +FORCEINLINE bool ISFINITE(const SEACAVE::TPoint2& v) +{ + return (ISFINITE(v.x) && ISFINITE(v.y)); +} +template +FORCEINLINE bool ISFINITE(const SEACAVE::TPoint3& v) +{ + return (ISFINITE(v.x) && ISFINITE(v.y) && ISFINITE(v.z)); +} +template +FORCEINLINE bool ISFINITE(const SEACAVE::TDMatrix& v) +{ + ASSERT(v.isContinuous()); + return ISFINITE(v.val, v.area()); +} +template +FORCEINLINE bool ISFINITE(const cv::Point_& v) +{ + return (ISFINITE(v.x) && ISFINITE(v.y)); +} +template +FORCEINLINE bool ISFINITE(const cv::Point3_& v) +{ + return (ISFINITE(v.x) && ISFINITE(v.y) && ISFINITE(v.z)); +} +template +FORCEINLINE bool ISFINITE(const cv::Matx& v) +{ + ASSERT(v.isContinuous()); + return ISFINITE(v.val, m*n); +} +template +FORCEINLINE bool ISFINITE(const Eigen::Matrix& m) +{ + return ISFINITE(m.data(), m.size()); +} + + +// initializing both scalar and matrix variables +template +FORCEINLINE Scalar INITTO(const Scalar*, Value v) +{ + return static_cast(v); +} +template +FORCEINLINE TPoint2 INITTO(const TPoint2*, Value v) +{ + return TPoint2(static_cast(v)); +} +template +FORCEINLINE TPoint3 INITTO(const TPoint3*, Value v) +{ + return TPoint3(static_cast(v)); +} +template +FORCEINLINE Eigen::Matrix INITTO(const Eigen::Matrix*, Value v) +{ + return Eigen::Matrix::Constant(static_cast(v)); +} +/*----------------------------------------------------------------*/ + +template +inline IDX MinIndex(const TYPE* data, IDX size) { + ASSERT(size>0); + IDX idx = 0; + for (IDX i=1; i data[i]) + idx = i; + return idx; +} +template +inline IDX MaxIndex(const TYPE* data, IDX size) { + ASSERT(size>0); + IDX idx = 0; + for (IDX i=1; i +inline ACCTYPE dot(const TYPE* a, const TYPE* b, size_t n) { + ACCTYPE s(0); + const TYPE* const last = a+n; + #if 1 // unrolled + const TYPE* const lastgroup = last-3; + while (a < lastgroup) { + s += ACCTYPE(a[0])*ACCTYPE(b[0]) + ACCTYPE(a[1])*ACCTYPE(b[1]) + ACCTYPE(a[2])*ACCTYPE(b[2]) + ACCTYPE(a[3])*ACCTYPE(b[3]); + a += 4; b += 4; + } + #endif + while (a < last) + s += ACCTYPE(*a++) * ACCTYPE(*b++); + return s; +} +template +inline ACCTYPE dot(const TYPE* a, const TYPE* b) { + ACCTYPE s(0); + const TYPE* const last = a+N; + #if 1 // unrolled + const TYPE* const lastgroup = last-3; + while (a < lastgroup) { + s += ACCTYPE(a[0])*ACCTYPE(b[0]) + ACCTYPE(a[1])*ACCTYPE(b[1]) + ACCTYPE(a[2])*ACCTYPE(b[2]) + ACCTYPE(a[3])*ACCTYPE(b[3]); + a += 4; b += 4; + } + #endif + while (a < last) + s += ACCTYPE(*a++) * ACCTYPE(*b++); + return s; +} +#ifdef _USE_SSE +template <> +inline float dot(const float* l, const float* r, size_t size) { + return sse_f_t::dot(l, r, size); +} +template <> +inline double dot(const double* l, const double* r, size_t size) { + return sse_d_t::dot(l, r, size); +} +#endif + +template +inline TYPE dot(const TMatrix& l, const TMatrix& r) { + return ((const cv::Vec&)l).dot((const cv::Vec&)r); +} +template +inline TYPE dot(const cv::Matx& l, const cv::Matx& r) { + return ((const cv::Vec&)l).dot((const cv::Vec&)r); +} + +inline float dot(float v1, float v2) { + return v1 * v2; +} +inline double dot(double v1, double v2) { + return v1 * v2; +} +template +inline TYPE dot(const cv::Point_* v1, const cv::Point_* v2, size_t size) { + return dot((const TYPE*)v1, (const TYPE*)v2, size*2); +} +template +inline TYPE dot(const cv::Point3_* v1, const cv::Point3_* v2, size_t size) { + return dot((const TYPE*)v1, (const TYPE*)v2, size*3); +} +template +inline TYPE dot(const cv::Vec* v1, const cv::Vec* v2, size_t size) { + return dot((const TYPE*)v1, (const TYPE*)v2, size*m); +} + +template +inline TMatrix cross(const TMatrix& l, const TMatrix& r) { + return ((const typename TMatrix::Vec&)l).cross((const typename TMatrix::Vec&)r); +} +template +inline TMatrix cross(const cv::Matx& l, const cv::Matx& r) { + return ((const typename TMatrix::Vec&)l).cross((const typename TMatrix::Vec&)r); +} + +template +inline typename RealType::type normSq(const cv::Point_& v) { + typedef typename RealType::type real; + return SQUARE((real)v.x)+SQUARE((real)v.y); +} +template +inline typename RealType::type normSq(const cv::Point3_& v) { + typedef typename RealType::type real; + return SQUARE((real)v.x)+SQUARE((real)v.y)+SQUARE((real)v.z); +} +template +inline typename RealType::type normSq(const TMatrix& v) { + typedef typename RealType::type real; + return cv::normL2Sqr(v.val, m*n); +} +template +inline typename RealType::type normSq(const cv::Matx& v) { + typedef typename RealType::type real; + return cv::normL2Sqr(v.val, m*n); +} +template +inline typename RealType::type normSq(const TDMatrix& v) { + typedef typename RealType::type real; + return cv::normL2Sqr(v.cv::Mat::template ptr(), v.area()); +} +template +inline typename RealType::type normSq(const cv::Mat_& v) { + typedef typename RealType::type real; + return cv::normL2Sqr(v.cv::Mat::template ptr(), v.cols*v.rows); +} + +inline REAL normSq(int v) { + return SQUARE((REAL)v); +} +inline REAL normSq(unsigned v) { + return SQUARE((REAL)v); +} +inline float normSq(float v) { + return SQUARE(v); +} +inline double normSq(double v) { + return SQUARE(v); +} +template +inline typename RealType::type normSq(const cv::Point_* v, size_t size) { + typedef typename RealType::type real; + return cv::normL2Sqr((const TYPE*)v, size*2); +} +template +inline typename RealType::type normSq(const cv::Point3_* v, size_t size) { + typedef typename RealType::type real; + return cv::normL2Sqr((const TYPE*)v, size*3); +} +template +inline typename RealType::type normSq(const cv::Vec* v, size_t size) { + typedef typename RealType::type real; + return cv::normL2Sqr((const TYPE*)v, size*m); +} + +template +inline ACCTYPE normSq(const TYPE* a, size_t n) { + ACCTYPE s(0); + const TYPE* const last = a+n; + #if 1 // unrolled + const TYPE* const lastgroup = last-3; + while (a < lastgroup) { + s += SQUARE(ACCTYPE(a[0])) + SQUARE(ACCTYPE(a[1])) + SQUARE(ACCTYPE(a[2])) + SQUARE(ACCTYPE(a[3])); + a += 4; + } + #endif + while (a < last) + s += SQUARE(ACCTYPE(*a++)); + return s; +} +template +inline ACCTYPE normSq(const TYPE* a) { + ACCTYPE s(0); + const TYPE* const last = a+N; + #if 1 // unrolled + const TYPE* const lastgroup = last-3; + while (a < lastgroup) { + s += SQUARE(ACCTYPE(a[0])) + SQUARE(ACCTYPE(a[1])) + SQUARE(ACCTYPE(a[2])) + SQUARE(ACCTYPE(a[3])); + a += 4; + } + #endif + while (a < last) + s += SQUARE(ACCTYPE(*a++)); + return s; +} + +template +inline ACCTYPE normSq(const TYPE* a, const TYPE* b, size_t n) { + ACCTYPE s(0); + const TYPE* const last = a+n; + #if 1 // unrolled + const TYPE* const lastgroup = last-3; + while (a < lastgroup) { + const INTTYPE v0(a[0] - b[0]), v1(a[1] - b[1]), v2(a[2] - b[2]), v3(a[3] - b[3]); + s += ACCTYPE(v0*v0 + v1*v1 + v2*v2 + v3*v3); + a += 4; b += 4; + } + #endif + while (a < last) { + const INTTYPE v(*a++ - *b++); + s += ACCTYPE(v*v); + } + return s; +} +template +inline ACCTYPE normSq(const TYPE* a, const TYPE* b) { + ACCTYPE s(0); + const TYPE* const last = a+N; + #if 1 // unrolled + const TYPE* const lastgroup = last-3; + while (a < lastgroup) { + const INTTYPE v0(a[0] - b[0]), v1(a[1] - b[1]), v2(a[2] - b[2]), v3(a[3] - b[3]); + s += ACCTYPE(v0*v0 + v1*v1 + v2*v2 + v3*v3); + a += 4; b += 4; + } + #endif + while (a < last) { + const INTTYPE v(*a++ - *b++); + s += ACCTYPE(v*v); + } + return s; +} + +template +inline ACCTYPE normSqDelta(TYPE* a, size_t n, const ACCTYPE avg) { + ACCTYPE s(0); + const TYPE* const last = a+n; + #if 1 // unrolled + const TYPE* const lastgroup = last-3; + while (a < lastgroup) { + const ACCTYPE v0(a[0]-=avg), v1(a[1]-=avg), v2(a[2]-=avg), v3(a[3]-=avg); + s += v0*v0 + v1*v1 + v2*v2 + v3*v3; + a += 4; + } + #endif + while (a < last) { + const ACCTYPE v(*a++ -= avg); + s += v*v; + } + return s; +} +template +inline ACCTYPE normSqDelta(TYPE* a, const ACCTYPE avg) { + ACCTYPE s(0); + const TYPE* const last = a+N; + #if 1 // unrolled + const TYPE* const lastgroup = last-3; + while (a < lastgroup) { + const ACCTYPE v0(a[0]-=avg), v1(a[1]-=avg), v2(a[2]-=avg), v3(a[3]-=avg); + s += v0*v0 + v1*v1 + v2*v2 + v3*v3; + a += 4; + } + #endif + while (a < last) { + const ACCTYPE v(*a++ -= avg); + s += v*v; + } + return s; +} + +template +inline ACCTYPE normSqZeroMean(TYPE* ptr, size_t n) { + TYPE* a = ptr; + ACCTYPE avg(0); + const TYPE* const last = a+n; + #if 1 // unrolled + const TYPE* const lastgroup = last-3; + while (a < lastgroup) { + avg += ACCTYPE(a[0]) + ACCTYPE(a[1]) + ACCTYPE(a[2]) + ACCTYPE(a[3]); + a += 4; + } + #endif + while (a < last) + avg += ACCTYPE(*a++); + avg /= (ACCTYPE)n; + + a = ptr; + ACCTYPE s(0); + #if 1 // unrolled + while (a < lastgroup) { + const ACCTYPE v0(a[0]-=avg), v1(a[1]-=avg), v2(a[2]-=avg), v3(a[3]-=avg); + s += v0*v0 + v1*v1 + v2*v2 + v3*v3; + a += 4; + } + #endif + while (a < last) { + const ACCTYPE v(*a++ -= avg); + s += v*v; + } + return s; +} +template +inline ACCTYPE normSqZeroMean(TYPE* ptr) { + TYPE* a = ptr; + ACCTYPE avg(0); + const TYPE* const last = a+N; + #if 1 // unrolled + const TYPE* const lastgroup = last-3; + while (a < lastgroup) { + avg += ACCTYPE(a[0]) + ACCTYPE(a[1]) + ACCTYPE(a[2]) + ACCTYPE(a[3]); + a += 4; + } + #endif + while (a < last) + avg += ACCTYPE(*a++); + avg /= (ACCTYPE)N; + + a = ptr; + ACCTYPE s(0); + #if 1 // unrolled + while (a < lastgroup) { + const ACCTYPE v0(a[0]-=avg), v1(a[1]-=avg), v2(a[2]-=avg), v3(a[3]-=avg); + s += v0*v0 + v1*v1 + v2*v2 + v3*v3; + a += 4; + } + #endif + while (a < last) { + const ACCTYPE v(*a++ -= avg); + s += v*v; + } + return s; +} + +template +inline typename RealType::type norm(const TPoint2& v) { + return SQRT(normSq(v)); +} +template +inline typename RealType::type norm(const TPoint3& v) { + return SQRT(normSq(v)); +} +template +inline typename RealType::type norm(const TMatrix& v) { + typedef typename RealType::type real; + return SQRT(cv::normL2Sqr(v.val, m*n)); +} +template +inline typename RealType::type norm(const TDMatrix& v) { + typedef typename RealType::type real; + return SQRT(cv::normL2Sqr(v.cv::Mat::template ptr(), v.area())); +} + +template +inline TPoint2 normalized(const TPoint2& v) { + return cv::normalize((const typename TPoint2::cvVec&)v); +} +template +inline TPoint2 normalized(const cv::Point_& v) { + return cv::normalize((const typename TPoint2::cvVec&)v); +} +template +inline TPoint3 normalized(const TPoint3& v) { + return cv::normalize((const typename TPoint3::cvVec&)v); +} +template +inline TPoint3 normalized(const cv::Point3_& v) { + return cv::normalize((const typename TPoint3::cvVec&)v); +} +template +inline TMatrix normalized(const TMatrix& v) { + return cv::normalize((const typename TMatrix::Vec&)v); +} +template +inline TMatrix normalized(const cv::Matx& v) { + return cv::normalize((const typename TMatrix::Vec&)v); +} + +template +inline void normalize(TPoint2& v) { + (typename TPoint2::cvVec&)v = cv::normalize((const typename TPoint2::cvVec&)v); +} +template +inline void normalize(cv::Point_& v) { + (typename TPoint2::cvVec&)v = cv::normalize((const typename TPoint2::cvVec&)v); +} +template +inline void normalize(TPoint3& v) { + (typename TPoint3::cvVec&)v = cv::normalize((const typename TPoint3::cvVec&)v); +} +template +inline void normalize(cv::Point3_& v) { + (typename TPoint3::cvVec&)v = cv::normalize((const typename TPoint3::cvVec&)v); +} +template +inline void normalize(TMatrix& v) { + (typename TMatrix::Vec&)v = cv::normalize((const typename TMatrix::Vec&)v); +} +template +inline void normalize(cv::Matx& v) { + (typename TMatrix::Vec&)v = cv::normalize((const typename TMatrix::Vec&)v); +} + +template +inline TYPE area(const TPoint2& v1, const TPoint2& v2) { + return ABS(v1.x * v2.y - v1.y * v2.x); +} +template +inline TYPE area(const cv::Point_& v1, const cv::Point_& v2) { + return ABS(v1.x * v2.y - v1.y * v2.x); +} +template +inline TYPE area(const TPoint3& v1, const TPoint3& v2) { + return norm(cross(v1,v2)); +} +template +inline TYPE area(const cv::Point3_& v1, const cv::Point3_& v2) { + return norm(cross(v1,v2)); +} +/*----------------------------------------------------------------*/ + + +/** @brief comparison function for floating point values + See http://www.boost.org/libs/test/doc/components/test_tools/floating_point_comparison.html */ +template // left == right +inline bool equal(const _Tp& left, const _Tp& right, const _Tp& = std::numeric_limits<_Tp>::epsilon()) +{ return (left==right); } +template<> +inline bool equal(const double& left, const double& right, const double& eps) { + // the code below does not work when one value is zero + if (left==0. || right==0.) + return (ABS(left-right) <= eps); + // compare two non-zero values + return (ABS(left-right) <= eps*MAXF(ABS(right),ABS(left))); +} +template<> +inline bool equal(const float& left, const float& right, const float& eps) { + // the code below does not work when one value is zero + if (left==0. || right==0.) + return (ABS(left-right) <= eps); + // compare two non-zero values + return (ABS(left-right) <= eps*MAXF(ABS(right),ABS(left))); +} + +/** @brief comparison function for floating point values */ +template // left < right +inline bool less(const _Tp& left, const _Tp& right, const _Tp& eps = std::numeric_limits<_Tp>::epsilon()) +{ if (left==0. || right==0.) return (right-left) > eps; + return ((right-left) > eps * MINF(right,left)); } + +/** @brief comparison function for floating point values */ +template // left <= right +inline bool lessEqual(const _Tp& left, const _Tp& right, const _Tp& eps = std::numeric_limits<_Tp>::epsilon()) +{ return (less(left, right, eps) || equal(left, right, eps)); } + +/** @brief comparison function for floating point values */ +template // left > right +inline bool greater(const _Tp& left, const _Tp& right, const _Tp& eps = std::numeric_limits<_Tp>::epsilon()) +{ if (left==0. || right==0.) return (left-right) > eps; + return ((left-right) > eps * MINF(right,left)); } + +/** @brief comparison function for floating point values */ +template // left >= right +inline bool greaterEqual(const _Tp& left, const _Tp& right, const _Tp& eps = std::numeric_limits<_Tp>::epsilon()) +{ return (greater(left,right) || equal(left, right, eps)); } +/*----------------------------------------------------------------*/ + + +/** @brief determine the number of decimals to store, w.g. to file + @returns the number of decimals between + lower bound guaranteed precision (digits10) + and the upper bound max. precision (nr. of binary digits) */ +#ifdef WIN32 +# pragma warning(push, 2) +#endif // WIN32 +template +inline unsigned decimalsToStore() { + if (std::numeric_limits<_Tp>::is_integer){ + // integer types can display exactly one decimal more than their guaranteed precision digits + return std::numeric_limits<_Tp>::digits10+1; + } else { + //return std::numeric_limits<_Tp>::digits10; // lower bound + return std::numeric_limits<_Tp>::digits; // upper bound + } +} +#ifdef WIN32 +# pragma warning(pop) +#endif // WIN32 +/*----------------------------------------------------------------*/ + + +// operators + +// TPoint2 operators +#if CV_MAJOR_VERSION > 2 +template +inline TPoint2 operator/(const TPoint2& pt, TYPEM m) { + return TPoint2(pt.x/m, pt.y/m); +} +template +inline TPoint2 operator/(const TPoint2& pt, TYPEM m) { + const float invm(INVERT(float(m))); + return TPoint2(invm*pt.x, invm*pt.y); +} +template +inline TPoint2 operator/(const TPoint2& pt, TYPEM m) { + const double invm(INVERT(double(m))); + return TPoint2(invm*pt.x, invm*pt.y); +} + +template +inline TPoint2& operator/=(TPoint2& pt, TYPEM m) { + pt.x /= m; pt.y /= m; + return pt; +} +template +inline TPoint2& operator/=(TPoint2& pt, TYPEM m) { + const float invm(INVERT(float(m))); + pt.x *= invm; pt.y *= invm; + return pt; +} +template +inline TPoint2& operator/=(TPoint2& pt, TYPEM m) { + const double invm(INVERT(double(m))); + pt.x *= invm; pt.y *= invm; + return pt; +} +#else +template +inline TPoint2 operator/(const cv::Point_& pt, TYPEM m) { + return TPoint2(pt.x/m, pt.y/m); +} +template +inline TPoint2 operator/(const cv::Point_& pt, TYPEM m) { + const float invm(INVERT(float(m))); + return TPoint2(invm*pt.x, invm*pt.y); +} +template +inline TPoint2 operator/(const cv::Point_& pt, TYPEM m) { + const double invm(INVERT(double(m))); + return TPoint2(invm*pt.x, invm*pt.y); +} + +template +inline TPoint2& operator/=(cv::Point_& pt, TYPEM m) { + pt.x /= m; pt.y /= m; + return (TPoint2&)pt; +} +template +inline TPoint2& operator/=(cv::Point_& pt, TYPEM m) { + const float invm(INVERT(float(m))); + pt.x *= invm; pt.y *= invm; + return (TPoint2&)pt; +} +template +inline TPoint2& operator/=(cv::Point_& pt, TYPEM m) { + const double invm(INVERT(double(m))); + pt.x *= invm; pt.y *= invm; + return (TPoint2&)pt; +} +#endif + +template +inline TPoint2 operator/(const cv::Point_& pt0, const cv::Point_& pt1) { + return TPoint2(pt0.x/pt1.x, pt0.y/pt1.y); +} +template +inline TPoint2& operator/=(cv::Point_& pt0, const cv::Point_& pt1) { + pt0.x/=pt1.x; pt0.y/=pt1.y; + return (TPoint2&)pt0; +} + +template +inline TPoint2 operator*(const cv::Point_& pt0, const cv::Point_& pt1) { + return TPoint2(pt0.x*pt1.x, pt0.y*pt1.y); +} +template +inline TPoint2& operator*=(cv::Point_& pt0, const cv::Point_& pt1) { + pt0.x*=pt1.x; pt0.y*=pt1.y; + return (TPoint2&)pt0; +} + +template +inline TPoint2 cvtPoint2(const TPoint2& p) { + return TPoint2(TTO(p.x), TTO(p.y)); +} + +// TPoint3 operators +#if CV_MAJOR_VERSION > 2 +template +inline TPoint3 operator/(const TPoint3& pt, TYPEM m) { + return TPoint3(pt.x/m, pt.y/m, pt.z/m); +} +template +inline TPoint3 operator/(const TPoint3& pt, TYPEM m) { + const float invm(INVERT(float(m))); + return TPoint3(invm*pt.x, invm*pt.y, invm*pt.z); +} +template +inline TPoint3 operator/(const TPoint3& pt, TYPEM m) { + const double invm(INVERT(double(m))); + return TPoint3(invm*pt.x, invm*pt.y, invm*pt.z); +} + +template +inline TPoint3& operator/=(TPoint3& pt, TYPEM m) { + pt.x /= m; pt.y /= m; pt.z /= m; + return pt; +} +template +inline TPoint3& operator/=(TPoint3& pt, TYPEM m) { + const float invm(INVERT(float(m))); + pt.x *= invm; pt.y *= invm; pt.z *= invm; + return pt; +} +template +inline TPoint3& operator/=(TPoint3& pt, TYPEM m) { + const double invm(INVERT(double(m))); + pt.x *= invm; pt.y *= invm; pt.z *= invm; + return pt; +} +#else +template +inline TPoint3 operator/(const cv::Point3_& pt, TYPEM m) { + return TPoint3(pt.x/m, pt.y/m, pt.z/m); +} +template +inline TPoint3 operator/(const cv::Point3_& pt, TYPEM m) { + const float invm(INVERT(float(m))); + return TPoint3(invm*pt.x, invm*pt.y, invm*pt.z); +} +template +inline TPoint3 operator/(const cv::Point3_& pt, TYPEM m) { + const double invm(INVERT(double(m))); + return TPoint3(invm*pt.x, invm*pt.y, invm*pt.z); +} + +template +inline TPoint3& operator/=(cv::Point3_& pt, TYPEM m) { + pt.x /= m; pt.y /= m; pt.z /= m; + return (TPoint3&)pt; +} +template +inline TPoint3& operator/=(cv::Point3_& pt, TYPEM m) { + const float invm(INVERT(float(m))); + pt.x *= invm; pt.y *= invm; pt.z *= invm; + return (TPoint3&)pt; +} +template +inline TPoint3& operator/=(cv::Point3_& pt, TYPEM m) { + const double invm(INVERT(double(m))); + pt.x *= invm; pt.y *= invm; pt.z *= invm; + return (TPoint3&)pt; +} +#endif + +template +inline TPoint3 operator/(const cv::Point3_& pt0, const cv::Point3_& pt1) { + return TPoint3(pt0.x/pt1.x, pt0.y/pt1.y, pt0.z/pt1.z); +} +template +inline TPoint3& operator/=(cv::Point3_& pt0, const cv::Point3_& pt1) { + pt0.x/=pt1.x; pt0.y/=pt1.y; pt0.z/=pt1.z; + return (TPoint3&)pt0; +} + +template +inline TPoint3 operator*(const cv::Point3_& pt0, const cv::Point3_& pt1) { + return TPoint3(pt0.x*pt1.x, pt0.y*pt1.y, pt0.z*pt1.z); +} +template +inline TPoint3& operator*=(cv::Point3_& pt0, const cv::Point3_& pt1) { + pt0.x*=pt1.x; pt0.y*=pt1.y; pt0.z*=pt1.z; + return (TPoint3&)pt0; +} + +template +inline TPoint3 cvtPoint3(const TPoint3& p) { + return TPoint3(TTO(p.x), TTO(p.y), TTO(p.z)); +} + +// TPixel operators +template +inline TPixel operator/(const TPixel& pt, TYPEM m) { + const TYPEM invm(INVERT(m)); + return TPixel(invm*pt.r, invm*pt.g, invm*pt.b); +} +template +inline TPixel& operator/=(TPixel& pt, TYPEM m) { + const TYPEM invm(INVERT(m)); + pt.r *= invm; pt.g *= invm; pt.b *= invm; + return pt; +} +template +inline TPixel operator/(const TPixel& pt0, const TPixel& pt1) { + return TPixel(pt0.r/pt1.r, pt0.g/pt1.g, pt0.b/pt1.b); +} +template +inline TPixel& operator/=(TPixel& pt0, const TPixel& pt1) { + pt0.r/=pt1.r; pt0.g/=pt1.g; pt0.b/=pt1.b; + return pt0; +} +template +inline TPixel operator*(const TPixel& pt0, const TPixel& pt1) { + return TPixel(pt0.r*pt1.r, pt0.g*pt1.g, pt0.b*pt1.b); +} +template +inline TPixel& operator*=(TPixel& pt0, const TPixel& pt1) { + pt0.r*=pt1.r; pt0.g*=pt1.g; pt0.b*=pt1.b; + return pt0; +} + +// TColor operators +template +inline TColor operator/(const TColor& pt, TYPEM m) { + const TYPEM invm(INVERT(m)); + return TColor(invm*pt.r, invm*pt.g, invm*pt.b, invm*pt.a); +} +template +inline TColor& operator/=(TColor& pt, TYPEM m) { + const TYPEM invm(INVERT(m)); + pt.r *= invm; pt.g *= invm; pt.b *= invm; pt.a *= invm; + return pt; +} +template +inline TColor operator/(const TColor& pt0, const TColor& pt1) { + return TColor(pt0.r/pt1.r, pt0.g/pt1.g, pt0.b/pt1.b, pt0.a/pt1.a); +} +template +inline TColor& operator/=(TColor& pt0, const TColor& pt1) { + pt0.r/=pt1.r; pt0.g/=pt1.g; pt0.b/=pt1.b; pt0.a/=pt1.a; + return pt0; +} +template +inline TColor operator*(const TColor& pt0, const TColor& pt1) { + return TColor(pt0.r*pt1.r, pt0.g*pt1.g, pt0.b*pt1.b, pt0.a*pt1.a); +} +template +inline TColor& operator*=(TColor& pt0, const TColor& pt1) { + pt0.r*=pt1.r; pt0.g*=pt1.g; pt0.b*=pt1.b; pt0.a*=pt1.a; + return pt0; +} + +// TMatrix operators +template +inline TMatrix operator + (const TMatrix& m1, const TMatrix& m2) { + return TMatrix(m1, m2, cv::Matx_AddOp()); +} +template +inline TMatrix operator + (const TMatrix& m1, const cv::Matx& m2) { + return cv::Matx(m1, m2, cv::Matx_AddOp()); +} +template +inline TMatrix operator + (const cv::Matx& m1, const TMatrix& m2) { + return TMatrix(m1, m2, cv::Matx_AddOp()); +} + +template +inline TMatrix operator - (const TMatrix& m1, const TMatrix& m2) { + return TMatrix(m1, m2, cv::Matx_SubOp()); +} +template +inline TMatrix operator - (const TMatrix& m1, const cv::Matx& m2) { + return TMatrix(m1, m2, cv::Matx_SubOp()); +} +template +inline TMatrix operator - (const cv::Matx& m1, const TMatrix& m2) { + return TMatrix(m1, m2, cv::Matx_SubOp()); +} +template +inline TMatrix operator - (const TMatrix& M) { + return TMatrix(M, TYPE(-1), cv::Matx_ScaleOp()); +} + +template +inline TMatrix operator * (const TMatrix& m1, const TMatrix& m2) { + return TMatrix(m1, m2, cv::Matx_MatMulOp()); +} + +template +inline TMatrix operator / (const TMatrix& mat, TYPE2 v) { + typedef typename std::conditional::value,TYPE2,REAL>::type real_t; + return TMatrix(mat, real_t(1)/v, cv::Matx_ScaleOp()); +} +template +inline TMatrix& operator /= (TMatrix& mat, TYPE2 v) { + return mat = mat/v; +} + +// TImage operators +template +inline TImage cvtImage(const TImage& image) { + TImage img(image.size()); + for (int r=0; r +FORCEINLINE SEACAVE::TPoint2 operator/(TYPEM n, const cv::Point_& d) { + return SEACAVE::TPoint2(n/d.x, n/d.y); +} +template +FORCEINLINE SEACAVE::TPoint3 operator/(TYPEM n, const cv::Point3_& d) { + return SEACAVE::TPoint3(n/d.x, n/d.y, n/d.z); +} +template +FORCEINLINE SEACAVE::TPixel operator/(TYPEM n, const SEACAVE::TPixel& d) { + return SEACAVE::TPixel(n/d.r, n/d.g, n/d.b); +} +template +FORCEINLINE SEACAVE::TColor operator/(TYPEM n, const SEACAVE::TColor& d) { + return SEACAVE::TColor(n/d.r, n/d.g, n/d.b, n/d.a); +} +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + + +// Informative template class for OpenCV "scalars" +#define DEFINE_GENERIC_CVDATATYPE(tp,ctp) namespace cv { \ +template<> class DataType< tp > { \ +public: \ + typedef tp value_type; \ + typedef value_type work_type; \ + typedef ctp channel_type; \ + typedef value_type vec_type; \ + enum { \ + generic_type = 0, \ + depth = DataDepth::value, \ + channels = sizeof(value_type)/sizeof(channel_type), \ + fmt = DataDepth::fmt, \ + type = CV_MAKETYPE(depth, channels) \ + }; \ +}; } +#define DEFINE_CVDATATYPE(tp) DEFINE_GENERIC_CVDATATYPE(tp,tp::Type) + +#define DEFINE_GENERIC_CVDATADEPTH(tp,ctp) namespace cv { \ +template<> class DataDepth< tp > { \ +public: \ + enum { \ + value = DataDepth< ctp >::value, \ + fmt = DataDepth< ctp >::fmt \ + }; \ +}; } +#define DEFINE_CVDATADEPTH(tp) DEFINE_GENERIC_CVDATADEPTH(tp,tp::Type) + +// define specialized cv:DataType<> +DEFINE_CVDATADEPTH(SEACAVE::hfloat) +DEFINE_CVDATADEPTH(SEACAVE::cuint32_t) + +// define specialized cv:DataType<> +DEFINE_CVDATATYPE(SEACAVE::hfloat) +DEFINE_CVDATATYPE(SEACAVE::cuint32_t) +DEFINE_GENERIC_CVDATATYPE(uint64_t,double) +/*----------------------------------------------------------------*/ +DEFINE_CVDATATYPE(SEACAVE::Point2i) +DEFINE_CVDATATYPE(SEACAVE::Point2hf) +DEFINE_CVDATATYPE(SEACAVE::Point2f) +DEFINE_CVDATATYPE(SEACAVE::Point2d) +/*----------------------------------------------------------------*/ +DEFINE_CVDATATYPE(SEACAVE::Point3i) +DEFINE_CVDATATYPE(SEACAVE::Point3hf) +DEFINE_CVDATATYPE(SEACAVE::Point3f) +DEFINE_CVDATATYPE(SEACAVE::Point3d) +/*----------------------------------------------------------------*/ +DEFINE_CVDATATYPE(SEACAVE::Pixel8U) +DEFINE_CVDATATYPE(SEACAVE::Pixel32F) +DEFINE_CVDATATYPE(SEACAVE::Pixel64F) +/*----------------------------------------------------------------*/ +DEFINE_CVDATATYPE(SEACAVE::Color8U) +DEFINE_CVDATATYPE(SEACAVE::Color32F) +DEFINE_CVDATATYPE(SEACAVE::Color64F) +/*----------------------------------------------------------------*/ +DEFINE_GENERIC_CVDATATYPE(SEACAVE::DMatrix8S, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::DMatrix8U, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::DMatrix32S, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::DMatrix32U, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::DMatrix32F, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::DMatrix64F, uint8_t) +/*----------------------------------------------------------------*/ +DEFINE_GENERIC_CVDATATYPE(SEACAVE::DVector8S, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::DVector8U, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::DVector32S, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::DVector32U, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::DVector32F, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::DVector64F, uint8_t) +/*----------------------------------------------------------------*/ +DEFINE_GENERIC_CVDATATYPE(SEACAVE::Image8U, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::Image16F, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::Image32F, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::Image64F, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::Image8U3, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::Image8U4, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::Image32F3, uint8_t) +DEFINE_GENERIC_CVDATATYPE(SEACAVE::Image32F4, uint8_t) +/*----------------------------------------------------------------*/ + + +namespace SEACAVE { + +namespace CONVERT { + +// convert sRGB to/from linear value +// (see http://en.wikipedia.org/wiki/SRGB) +template +constexpr T sRGB2RGB(T x) { + return x <= T(0.04045) ? x * (T(1)/T(12.92)) : POW((x + T(0.055)) * (T(1)/T(1.055)), T(2.4)); +} +template +constexpr T RGB2sRGB(T x) { + return x <= T(0.0031308) ? T(12.92) * x : T(1.055) * POW(x, T(1)/T(2.4)) - T(0.055); +} +static const CAutoPtrArr g_ptrsRGB82RGBf([]() { + float* const buffer = new float[256]; + for (int i=0; i<256; ++i) + buffer[i] = sRGB2RGB(float(i)/255.f); + return buffer; +}()); + +// color conversion helper structures +template +struct NormRGB_t { + const TO v; + inline NormRGB_t(TI _v) : v(TO(_v)*(TO(1)/TO(255))) {} + inline operator TO () const { return v; } +}; +template +struct RGBUnNorm_t { + const TO v; + inline RGBUnNorm_t(TI _v) : v(TO(_v)*TO(255)) {} + inline operator TO () const { return v; } +}; +template +struct RoundF2U_t { + const TO v; + inline RoundF2U_t(TI _v) : v(ROUND2INT(_v)) {} + inline operator TO () const { return v; } +}; +template +struct sRGB2RGB_t { + const TO v; + inline sRGB2RGB_t(TI _v) : v(sRGB2RGB(TO(_v))) {} + inline operator TO () const { return v; } +}; +template +struct NormsRGB2RGB_t { + const TO v; + inline NormsRGB2RGB_t(TI _v) : v(sRGB2RGB(TO(_v)*(TO(1)/TO(255)))) {} + inline operator TO () const { return v; } +}; +template <> +struct NormsRGB2RGB_t { + const float v; + inline NormsRGB2RGB_t(uint8_t _v) : v(g_ptrsRGB82RGBf[_v]) {} + inline operator float () const { return v; } +}; +template +struct NormsRGB2RGBUnNorm_t { + const TO v; + inline NormsRGB2RGBUnNorm_t(TI _v) : v(sRGB2RGB(TO(_v)*(TO(1)/TO(255)))*TO(255)) {} + inline operator TO () const { return v; } +}; +template <> +struct NormsRGB2RGBUnNorm_t { + const float v; + inline NormsRGB2RGBUnNorm_t(uint8_t _v) : v(g_ptrsRGB82RGBf[_v]*255.f) {} + inline operator float () const { return v; } +}; + +} // namespace CONVERT +/*----------------------------------------------------------------*/ + + +// C L A S S ////////////////////////////////////////////////////// + +template +struct OppositeType { + typedef FLT Type; +}; +template <> +struct OppositeType { + typedef double Type; +}; +template <> +struct OppositeType { + typedef float Type; +}; +// Point2 +template +inline cv::Point_ Cast(const cv::Point_& pt) { + return pt; +} +template +inline TPoint2 Cast(const TPoint2& pt) { + return pt; +} +// Point3 +template +inline cv::Point3_ Cast(const cv::Point3_& pt) { + return pt; +} +template +inline TPoint3 Cast(const TPoint3& pt) { + return pt; +} +// Pixel +template +inline TPixel Cast(const TPixel& pt) { + return pt; +} +// Color +template +inline TColor Cast(const TColor& pt) { + return pt; +} +// Matrix +template +inline TMatrix Cast(const TMatrix& v) { + return v; +} +/*----------------------------------------------------------------*/ + + +// C L A S S ////////////////////////////////////////////////////// + +template +TPoint3 TPoint3::RotateAngleAxis(const TPoint3& pt, const TPoint3& angle_axis) +{ + TPoint3 result; + const TYPE theta2(normSq(angle_axis)); + if (theta2 > TYPE(1e-12)) { + // Away from zero, use the Rodriguez formula + // + // result = pt costheta + + // (w x pt) * sintheta + + // w (w . pt) (1 - costheta) + // + // We want to be careful to only evaluate the square root if the + // norm of the angle_axis vector is greater than zero. Otherwise + // we get a division by zero. + // + const TYPE theta(SQRT(theta2)); + const TPoint3 w(angle_axis*(TYPE(1)/theta)); + const TYPE costheta(COS(theta)); + const TYPE sintheta(SIN(theta)); + const TPoint3 w_cross_pt(w.cross(pt)); + const TYPE w_dot_pt(w.dot(pt)); + result = pt*costheta + w_cross_pt*sintheta + w*(TYPE(1.0)-costheta)*w_dot_pt; + } else { + // Near zero, the first order Taylor approximation of the rotation + // matrix R corresponding to a vector w and angle w is + // + // R = I + hat(w) * sin(theta) + // + // But sintheta ~ theta and theta * w = angle_axis, which gives us + // + // R = I + hat(w) + // + // and actually performing multiplication with the point pt, gives us + // R * pt = pt + w x pt. + // + // Switching to the Taylor expansion at zero helps avoid all sorts + // of numerical nastiness. + const TPoint3 w_cross_pt(angle_axis.cross(pt)); + result = pt + w_cross_pt; + } + return result; +} +/*----------------------------------------------------------------*/ + + +// C L A S S ////////////////////////////////////////////////////// + +template +inline TMatrix::TMatrix(TYPE v0) +{ + STATIC_ASSERT(channels >= 1); + val[0] = v0; + for (int i = 1; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(TYPE v0, TYPE v1) +{ + STATIC_ASSERT(channels >= 2); + val[0] = v0; val[1] = v1; + for (int i = 2; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(TYPE v0, TYPE v1, TYPE v2) +{ + STATIC_ASSERT(channels >= 3); + val[0] = v0; val[1] = v1; val[2] = v2; + for (int i = 3; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3) +{ + STATIC_ASSERT(channels >= 4); + val[0] = v0; val[1] = v1; val[2] = v2; val[3] = v3; + for (int i = 4; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4) +{ + STATIC_ASSERT(channels >= 5); + val[0] = v0; val[1] = v1; val[2] = v2; val[3] = v3; val[4] = v4; + for (int i = 5; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5) +{ + STATIC_ASSERT(channels >= 6); + val[0] = v0; val[1] = v1; val[2] = v2; val[3] = v3; + val[4] = v4; val[5] = v5; + for (int i = 6; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5, TYPE v6) +{ + STATIC_ASSERT(channels >= 7); + val[0] = v0; val[1] = v1; val[2] = v2; val[3] = v3; + val[4] = v4; val[5] = v5; val[6] = v6; + for (int i = 7; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5, TYPE v6, TYPE v7) +{ + STATIC_ASSERT(channels >= 8); + val[0] = v0; val[1] = v1; val[2] = v2; val[3] = v3; + val[4] = v4; val[5] = v5; val[6] = v6; val[7] = v7; + for (int i = 8; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5, TYPE v6, TYPE v7, TYPE v8) +{ + STATIC_ASSERT(channels >= 9); + val[0] = v0; val[1] = v1; val[2] = v2; val[3] = v3; + val[4] = v4; val[5] = v5; val[6] = v6; val[7] = v7; + val[8] = v8; + for (int i = 9; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5, TYPE v6, TYPE v7, TYPE v8, TYPE v9) +{ + STATIC_ASSERT(channels >= 10); + val[0] = v0; val[1] = v1; val[2] = v2; val[3] = v3; + val[4] = v4; val[5] = v5; val[6] = v6; val[7] = v7; + val[8] = v8; val[9] = v9; + for (int i = 10; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5, TYPE v6, TYPE v7, TYPE v8, TYPE v9, TYPE v10, TYPE v11) +{ + STATIC_ASSERT(channels >= 12); + val[0] = v0; val[1] = v1; val[2] = v2; val[3] = v3; + val[4] = v4; val[5] = v5; val[6] = v6; val[7] = v7; + val[8] = v8; val[9] = v9; val[10] = v10; val[11] = v11; + for (int i = 12; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5, TYPE v6, TYPE v7, TYPE v8, TYPE v9, TYPE v10, TYPE v11, TYPE v12, TYPE v13) +{ + STATIC_ASSERT(channels == 14); + val[0] = v0; val[1] = v1; val[2] = v2; val[3] = v3; + val[4] = v4; val[5] = v5; val[6] = v6; val[7] = v7; + val[8] = v8; val[9] = v9; val[10] = v10; val[11] = v11; + val[12] = v12; val[13] = v13; + for (int i = 14; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(TYPE v0, TYPE v1, TYPE v2, TYPE v3, TYPE v4, TYPE v5, TYPE v6, TYPE v7, TYPE v8, TYPE v9, TYPE v10, TYPE v11, TYPE v12, TYPE v13, TYPE v14, TYPE v15) +{ + STATIC_ASSERT(channels >= 16); + val[0] = v0; val[1] = v1; val[2] = v2; val[3] = v3; + val[4] = v4; val[5] = v5; val[6] = v6; val[7] = v7; + val[8] = v8; val[9] = v9; val[10] = v10; val[11] = v11; + val[12] = v12; val[13] = v13; val[14] = v14; val[15] = v15; + for (int i = 16; i < channels; i++) + val[i] = TYPE(0); +} +template +inline TMatrix::TMatrix(const TYPE* values) +{ + for (int i = 0; i < channels; i++) + val[i] = values[i]; +} + + +template +inline bool TMatrix::IsEqual(const Base& rhs) const +{ + for (int i=0; i +inline bool TMatrix::IsEqual(const Base& rhs, TYPE eps) const +{ + for (int i=0; i +inline TMatrix TMatrix::RightNullSpace(int flags /*= 0*/) const +{ + STATIC_ASSERT(n > m); + const cv::SVD svd(*this, flags|cv::SVD::FULL_UV); + // the orthonormal basis of the null space is formed by the columns + // of svd.vt such that the corresponding singular values are 0 (n - m or svd.vt.cols - svd.w.rows) + ASSERT(svd.vt.rows == n && svd.vt.rows == svd.vt.cols && svd.w.rows == m); + // return result (last rows transposed) + return svd.vt.rowRange(m,n).t(); +} +// calculate right null-vector of the matrix ([n,1]) +template +inline TMatrix TMatrix::RightNullVector(int flags /*= 0*/) const +{ + const cv::SVD svd(*this, (m >= n ? flags : flags|cv::SVD::FULL_UV)); + // the singular values could be checked for numerical stability + // return result (last row transposed) + ASSERT(svd.vt.rows == n); + return *svd.vt.ptr< const TMatrix >(n-1); +} +// calculate left null-vector of the matrix ([m,1]) +template +inline TMatrix TMatrix::LeftNullVector(int flags /*= 0*/) const +{ + return TMatrix(Base::t()).RightNullVector(flags); +} +/*----------------------------------------------------------------*/ + + +// C L A S S ////////////////////////////////////////////////////// + +template +inline TYPE TDMatrix::getDetSquare() const +{ + ASSERT(cols == rows); + const int dim = rows; + const int subdim = dim-1; + // end of recursion in 1x1: + if (subdim==0) + return Base::operator()(0,0); + // end of recursion in 2x2: + if (subdim==1) + return (Base::operator()(0,0)*Base::operator()(1,1)- + Base::operator()(1,0)*Base::operator()(0,1)); + TYPE d = 0; + TDMatrix SubMatrix(subdim, subdim); + for (int sub=0; sub +inline TDMatrix TDMatrix::getAdjoint() const +{ + ASSERT(rows==cols); + TDMatrix Out(cols,rows); + TDMatrix SubMatrix(rows-1,cols-1); + for (int i=0; i +inline void TDMatrix::getSystemMatrix(TDMatrix& dest) const +{ + // resize result to square matrix + dest = TDMatrix::zeros(cols, cols); + /// Hessian is symmetric! so first diagonal then lower left + // diagonal + for (int col = 0; col +inline void TDMatrix::makeSymmetric() +{ + ASSERT(cols == rows); + const int num = cols; + for (int r=0; r +inline TYPE TDMatrix::getNormL1() const +{ + TYPE result = 0; + for (const TYPE* dataP = getData()+Base::total()-1; dataP >= getData(); dataP--) + result += ABS(*dataP); + return result; +} + + +template +inline double TDMatrix::getNormL2() const +{ + double result = 0; + for (const TYPE* dataP = getData()+Base::total()-1; dataP >= getData(); dataP--) + result += (double)((*dataP) * (*dataP)); + return SQRT(result); +} + +/** Kronecker-product with matrix B, result in dest */ +template +void TDMatrix::Kronecker(const TDMatrix& B, TDMatrix& dest) const +{ + const int A_rows = rows; + const int A_cols = cols; + const int B_rows = B.rows; + const int B_cols = B.cols; + + dest.newsize(A_rows*B_rows,A_cols*B_cols); + for (int i=0; i +void TDMatrix::SwapRows(int i, int r) +{ + TYPE* const rowi = Base::operator[](i); + TYPE* const rowr = Base::operator[](r); + for (int c=0; c +void TDMatrix::GaussJordan() +{ + //const bool verbose = false; + const int numr = rows, numc = cols; + const int max = MINF(numr, numc); + ///<< offset to right from diagonal, the working column is given by r+offs + int offs = 0; + for (int r=0; r=0; --r) { + // search for leading element + int c; + for (c=r; c=0; --row) { + const TYPE wval=Base::operator()(row,c); + for (int col=0; col +TDMatrix TDVector::getOuterProduct(const TDVector& v) const { + TDMatrix mat(rows,v.rows); + TYPE rVal; + for (int row=0; row +void TDVector::getKroneckerProduct(const TDVector& arg, TDVector& dst) const +{ + const int s1=rows, s2=arg.rows; + int l=0; + dst.newsize(s1*s2); + for (int i=0; i +template +TPixel TPixel::colorRamp(VT v, VT vmin, VT vmax) +{ + if (v < vmin) + v = vmin; + if (v > vmax) + v = vmax; + const TYPE dv((TYPE)(vmax - vmin)); + TPixel c(1,1,1); // white + if (v < vmin + (VT)(TYPE(0.25) * dv)) { + c.r = TYPE(0); + c.g = TYPE(4) * (v - vmin) / dv; + } else if (v < vmin + (VT)(TYPE(0.5) * dv)) { + c.r = TYPE(0); + c.b = TYPE(1) + TYPE(4) * (vmin + TYPE(0.25) * dv - v) / dv; + } else if (v < vmin + (VT)(TYPE(0.75) * dv)) { + c.r = TYPE(4) * (v - vmin - TYPE(0.5) * dv) / dv; + c.b = TYPE(0); + } else { + c.g = TYPE(1) + TYPE(4) * (vmin + TYPE(0.75) * dv - v) / dv; + c.b = TYPE(0); + } + return c; +} + +// Gray values are expected in the range [0, 1] and converted to RGB values. +template +TPixel TPixel::gray2color(ALT gray) +{ + ASSERT(ALT(0) <= gray && gray <= ALT(1)); + // Jet colormap inspired by Matlab. + auto const Interpolate = [](ALT val, ALT y0, ALT x0, ALT y1, ALT x1) -> ALT { + return (val - x0) * (y1 - y0) / (x1 - x0) + y0; + }; + auto const Base = [&Interpolate](ALT val) -> ALT { + if (val <= ALT(0.125)) { + return ALT(0); + } else if (val <= ALT(0.375)) { + return Interpolate(ALT(2) * val - ALT(1), ALT(0), ALT(-0.75), ALT(1), ALT(-0.25)); + } else if (val <= ALT(0.625)) { + return ALT(1); + } else if (val <= ALT(0.87)) { + return Interpolate(ALT(2) * val - ALT(1), ALT(1), ALT(0.25), ALT(0), ALT(0.75)); + } else { + return ALT(0); + } + }; + return TPixel().set( + Base(gray + ALT(0.25)), + Base(gray), + Base(gray - ALT(0.25)) + ); +} +/*----------------------------------------------------------------*/ + + +// C L A S S ////////////////////////////////////////////////////// + +// Find a pixel inside the image +template +inline const TYPE& TImage::getPixel(int y, int x) const +{ + if (x < 0) + x = 0; + else if (x >= cols) + x = cols-1; + if (y < 0) + y = 0; + else if (y >= rows) + y = rows-1; + return BaseBase::operator()(y,x); +} +/*----------------------------------------------------------------*/ + + +// sample by bilinear interpolation +template +template +TYPE TImage::sample(const TPoint2& pt) const +{ + const int lx((int)pt.x); + const int ly((int)pt.y); + const T x(pt.x-lx), x1(T(1)-x); + const T y(pt.y-ly), y1(T(1)-y); + return (BaseBase::operator()( ly, lx)*x1 + BaseBase::operator()( ly, lx+1)*x)*y1 + + (BaseBase::operator()(ly+1,lx)*x1 + BaseBase::operator()(ly+1,lx+1)*x)*y; +} +template +template +TYPE TImage::sampleSafe(const TPoint2& pt) const +{ + const int lx((int)pt.x); + const int ly((int)pt.y); + const T x(pt.x-lx), x1(T(1)-x); + const T y(pt.y-ly), y1(T(1)-y); + return (getPixel( ly, lx)*x1 + getPixel( ly, lx+1)*x)*y1 + + (getPixel(ly+1,lx)*x1 + getPixel(ly+1,lx+1)*x)*y; +} +/*----------------------------------------------------------------*/ + + +// sample by bilinear interpolation, using only pixels that meet the user condition +template +template +bool TImage::sample(TV& v, const TPoint2& pt, const Functor& functor) const +{ + const int lx((int)pt.x); + const int ly((int)pt.y); + const T x(pt.x-lx), x1(T(1)-x); + const T y(pt.y-ly), y1(T(1)-y); + const TYPE& x0y0(BaseBase::operator()(ly , lx )); const bool b00(functor(x0y0)); + const TYPE& x1y0(BaseBase::operator()(ly , lx+1)); const bool b10(functor(x1y0)); + const TYPE& x0y1(BaseBase::operator()(ly+1, lx )); const bool b01(functor(x0y1)); + const TYPE& x1y1(BaseBase::operator()(ly+1, lx+1)); const bool b11(functor(x1y1)); + if (!b00 && !b10 && !b01 && !b11) + return false; + v = TV(y1*(x1*Cast(b00 ? x0y0 : (b10 ? x1y0 : (b01 ? x0y1 : x1y1))) + x*Cast(b10 ? x1y0 : (b00 ? x0y0 : (b11 ? x1y1 : x0y1)))) + + y *(x1*Cast(b01 ? x0y1 : (b11 ? x1y1 : (b00 ? x0y0 : x1y0))) + x*Cast(b11 ? x1y1 : (b01 ? x0y1 : (b10 ? x1y0 : x0y0))))); + return true; +} +template +template +bool TImage::sampleSafe(TV& v, const TPoint2& pt, const Functor& functor) const +{ + const int lx((int)pt.x); + const int ly((int)pt.y); + const T x(pt.x-lx), x1(T(1)-x); + const T y(pt.y-ly), y1(T(1)-y); + const TYPE& x0y0(getPixel(ly , lx )); const bool b00(functor(x0y0)); + const TYPE& x1y0(getPixel(ly , lx+1)); const bool b10(functor(x1y0)); + const TYPE& x0y1(getPixel(ly+1, lx )); const bool b01(functor(x0y1)); + const TYPE& x1y1(getPixel(ly+1, lx+1)); const bool b11(functor(x1y1)); + if (!b00 && !b10 && !b01 && !b11) + return false; + v = TV(y1*(x1*Cast(b00 ? x0y0 : (b10 ? x1y0 : (b01 ? x0y1 : x1y1))) + x*Cast(b10 ? x1y0 : (b00 ? x0y0 : (b11 ? x1y1 : x0y1)))) + + y *(x1*Cast(b01 ? x0y1 : (b11 ? x1y1 : (b00 ? x0y0 : x1y0))) + x*Cast(b11 ? x1y1 : (b01 ? x0y1 : (b10 ? x1y0 : x0y0))))); + return true; +} +// same as above, but using default value if the condition is not met +template +template +TYPE TImage::sample(const TPoint2& pt, const Functor& functor, const TYPE& dv) const +{ + const int lx((int)pt.x); + const int ly((int)pt.y); + const T x(pt.x-lx), x1(T(1)-x); + const T y(pt.y-ly), y1(T(1)-y); + const TYPE& x0y0(BaseBase::operator()(ly , lx )); const bool b00(functor(x0y0)); + const TYPE& x1y0(BaseBase::operator()(ly , lx+1)); const bool b10(functor(x1y0)); + const TYPE& x0y1(BaseBase::operator()(ly+1, lx )); const bool b01(functor(x0y1)); + const TYPE& x1y1(BaseBase::operator()(ly+1, lx+1)); const bool b11(functor(x1y1)); + return TYPE(y1*(x1*Cast(b00 ? x0y0 : dv) + x*Cast(b10 ? x1y0 : dv)) + + y *(x1*Cast(b01 ? x0y1 : dv) + x*Cast(b11 ? x1y1 : dv))); +} +template +template +TYPE TImage::sampleSafe(const TPoint2& pt, const Functor& functor, const TYPE& dv) const +{ + const int lx((int)pt.x); + const int ly((int)pt.y); + const T x(pt.x-lx), x1(T(1)-x); + const T y(pt.y-ly), y1(T(1)-y); + const TYPE& x0y0(getPixel(ly , lx )); const bool b00(functor(x0y0)); + const TYPE& x1y0(getPixel(ly , lx+1)); const bool b10(functor(x1y0)); + const TYPE& x0y1(getPixel(ly+1, lx )); const bool b01(functor(x0y1)); + const TYPE& x1y1(getPixel(ly+1, lx+1)); const bool b11(functor(x1y1)); + return TYPE(y1*(x1*Cast(b00 ? x0y0 : dv) + x*Cast(b10 ? x1y0 : dv)) + + y *(x1*Cast(b01 ? x0y1 : dv) + x*Cast(b11 ? x1y1 : dv))); +} + +// same as above, sample image at a specified position, but using the given sampler +#include "Sampler.inl" +template +template +INTERTYPE TImage::sample(const SAMPLER& sampler, const TPoint2& pt) const +{ + return Sampler::Sample< TImage, SAMPLER, TPoint2, INTERTYPE >(*this, sampler, pt); +} + +// convert color image to gray +template +template +void TImage::toGray(TImage& out, int code, bool bNormalize, bool bSRGB) const +{ + #if 1 + typedef typename RealType::type Real; + ASSERT(code==cv::COLOR_RGB2GRAY || code==cv::COLOR_RGBA2GRAY || code==cv::COLOR_BGR2GRAY || code==cv::COLOR_BGRA2GRAY); + static const Real coeffsRGB[] = {Real(0.299), Real(0.587), Real(0.114)}; + static const Real coeffsBGR[] = {Real(0.114), Real(0.587), Real(0.299)}; + const Real* coeffs; + switch (code) { + case cv::COLOR_BGR2GRAY: + case cv::COLOR_BGRA2GRAY: + coeffs = coeffsBGR; + break; + case cv::COLOR_RGB2GRAY: + case cv::COLOR_RGBA2GRAY: + coeffs = coeffsRGB; + break; + default: + ASSERT("Unsupported image format" == NULL); + } + const Real &cb(coeffs[0]), &cg(coeffs[1]), &cr(coeffs[2]); + if (out.rows!=rows || out.cols!=cols) + out.create(rows, cols); + ASSERT(cv::Mat::isContinuous()); + ASSERT(out.cv::Mat::isContinuous()); + const int scn(this->cv::Mat::channels()); + T* dst = out.cv::Mat::template ptr(); + T* const dstEnd = dst + out.area(); + typedef typename cv::DataType::channel_type ST; + if (bSRGB) { + if (bNormalize) { + typedef typename CONVERT::NormsRGB2RGB_t ColConv; + for (const ST* src=cv::Mat::template ptr(); dst!=dstEnd; src+=scn) + *dst++ = T(cb*ColConv(src[0]) + cg*ColConv(src[1]) + cr*ColConv(src[2])); + } else { + typedef typename CONVERT::NormsRGB2RGBUnNorm_t ColConv; + for (const ST* src=cv::Mat::template ptr(); dst!=dstEnd; src+=scn) + *dst++ = T(cb*ColConv(src[0]) + cg*ColConv(src[1]) + cr*ColConv(src[2])); + } + } else { + if (bNormalize) { + typedef typename CONVERT::NormRGB_t ColConv; + for (const ST* src=cv::Mat::template ptr(); dst!=dstEnd; src+=scn) + *dst++ = T(cb*ColConv(src[0]) + cg*ColConv(src[1]) + cr*ColConv(src[2])); + } else { + for (const ST* src=cv::Mat::template ptr(); dst!=dstEnd; src+=scn) + *dst++ = T(cb*src[0] + cg*src[1] + cr*src[2]); + } + } + #else + cv::Mat cimg; + convertTo(cimg, cv::DataType::type); + cv::cvtColor(cimg, out, code); + if (bNormalize) + out *= T(1)/T(255); + #endif +} +/*----------------------------------------------------------------*/ + + +// compute scaled size such that the biggest dimension is scaled as desired +// and the smaller one maintains the aspect ratio as best as it can +template +cv::Size TImage::computeResize(const cv::Size& size, REAL scale) +{ + return cv::Size( + cv::saturate_cast((REAL)size.width*scale), + cv::saturate_cast((REAL)size.height*scale)); +} +// compute the final scaled size by performing successive resizes +// with the given scale value +template +cv::Size TImage::computeResize(const cv::Size& size, REAL scale, unsigned resizes) +{ + cv::Size scaledSize(size); + while (resizes-- > 0) + scaledSize = computeResize(scaledSize, scale); + return scaledSize; +} + +// compute image scale for a given max and min resolution +template +unsigned TImage::computeMaxResolution(unsigned width, unsigned height, unsigned& level, unsigned minImageSize, unsigned maxImageSize) +{ + // consider the native resolution the max(width,height) + const unsigned imageSize = MAXF(width, height); + // if the max level it's used, return original image size + if (level == 0) + return MINF(imageSize, maxImageSize); + // compute the resolution corresponding to the desired level + unsigned size = (imageSize >> level); + // if the image is too small + if (size < minImageSize) { + // start from the max level + level = 0; + while ((imageSize>>(level+1)) >= minImageSize) + ++level; + size = (imageSize>>level); + } + return MINF(size, maxImageSize); +} +template +unsigned TImage::computeMaxResolution(unsigned& level, unsigned minImageSize, unsigned maxImageSize) const +{ + return computeMaxResolution((unsigned)width(), (unsigned)height(), level, minImageSize, maxImageSize); +} +/*----------------------------------------------------------------*/ + + +// Raster the given triangle and output the position of each pixel of the triangle; +// based on "Advanced Rasterization" by Nick (Nicolas Capens) +// http://devmaster.net/forums/topic/1145-advanced-rasterization +template +template +void TImage::RasterizeTriangle(const TPoint2& v1, const TPoint2& v2, const TPoint2& v3, PARSER& parser) +{ + // 28.4 fixed-point coordinates + const int_t Y1 = ROUND2INT(T(16) * v1.y); + const int_t Y2 = ROUND2INT(T(16) * v2.y); + const int_t Y3 = ROUND2INT(T(16) * v3.y); + + const int_t X1 = ROUND2INT(T(16) * v1.x); + const int_t X2 = ROUND2INT(T(16) * v2.x); + const int_t X3 = ROUND2INT(T(16) * v3.x); + + // Deltas + const int_t DX12 = X1 - X2; + const int_t DX23 = X2 - X3; + const int_t DX31 = X3 - X1; + + const int_t DY12 = Y1 - Y2; + const int_t DY23 = Y2 - Y3; + const int_t DY31 = Y3 - Y1; + + // Fixed-point deltas + const int_t FDX12 = DX12 << 4; + const int_t FDX23 = DX23 << 4; + const int_t FDX31 = DX31 << 4; + + const int_t FDY12 = DY12 << 4; + const int_t FDY23 = DY23 << 4; + const int_t FDY31 = DY31 << 4; + + // Bounding rectangle + int minx = (int)((MINF3(X1, X2, X3) + 0xF) >> 4); + int maxx = (int)((MAXF3(X1, X2, X3) + 0xF) >> 4); + int miny = (int)((MINF3(Y1, Y2, Y3) + 0xF) >> 4); + int maxy = (int)((MAXF3(Y1, Y2, Y3) + 0xF) >> 4); + + // Block size, standard 8x8 (must be power of two) + const int q = 8; + + // Start in corner of 8x8 block + minx &= ~(q - 1); + miny &= ~(q - 1); + + // Half-edge constants + int_t C1 = DY12 * X1 - DX12 * Y1; + int_t C2 = DY23 * X2 - DX23 * Y2; + int_t C3 = DY31 * X3 - DX31 * Y3; + + // Correct for fill convention + if (DY12 < 0 || (DY12 == 0 && DX12 > 0)) C1++; + if (DY23 < 0 || (DY23 == 0 && DX23 > 0)) C2++; + if (DY31 < 0 || (DY31 == 0 && DX31 > 0)) C3++; + + // Loop through blocks + int pixy = miny; + for (int y = miny; y < maxy; y += q) + { + for (int x = minx; x < maxx; x += q) + { + // Corners of block + const int_t x0 = int_t(x) << 4; + const int_t x1 = int_t(x + q - 1) << 4; + const int_t y0 = int_t(y) << 4; + const int_t y1 = int_t(y + q - 1) << 4; + + // Evaluate half-space functions + const bool a00 = C1 + DX12 * y0 - DY12 * x0 > 0; + const bool a10 = C1 + DX12 * y0 - DY12 * x1 > 0; + const bool a01 = C1 + DX12 * y1 - DY12 * x0 > 0; + const bool a11 = C1 + DX12 * y1 - DY12 * x1 > 0; + const int a = (a00 << 0) | (a10 << 1) | (a01 << 2) | (a11 << 3); + + const bool b00 = C2 + DX23 * y0 - DY23 * x0 > 0; + const bool b10 = C2 + DX23 * y0 - DY23 * x1 > 0; + const bool b01 = C2 + DX23 * y1 - DY23 * x0 > 0; + const bool b11 = C2 + DX23 * y1 - DY23 * x1 > 0; + const int b = (b00 << 0) | (b10 << 1) | (b01 << 2) | (b11 << 3); + + const bool c00 = C3 + DX31 * y0 - DY31 * x0 > 0; + const bool c10 = C3 + DX31 * y0 - DY31 * x1 > 0; + const bool c01 = C3 + DX31 * y1 - DY31 * x0 > 0; + const bool c11 = C3 + DX31 * y1 - DY31 * x1 > 0; + const int c = (c00 << 0) | (c10 << 1) | (c01 << 2) | (c11 << 3); + + // Skip block when outside an edge + if (a == 0x0 || b == 0x0 || c == 0x0) continue; + + int nowpixy = pixy; + + // Accept whole block when totally covered + if (a == 0xF && b == 0xF && c == 0xF) + { + for (int iy = 0; iy < q; iy++) + { + for (int ix = x; ix < x + q; ix++) + parser(ImageRef(ix,nowpixy)); + + ++nowpixy; + } + } + else // Partially covered block + { + int_t CY1 = C1 + DX12 * y0 - DY12 * x0; + int_t CY2 = C2 + DX23 * y0 - DY23 * x0; + int_t CY3 = C3 + DX31 * y0 - DY31 * x0; + + for (int iy = y; iy < y + q; iy++) + { + int_t CX1 = CY1; + int_t CX2 = CY2; + int_t CX3 = CY3; + + for (int ix = x; ix < x + q; ix++) + { + if (CX1 > 0 && CX2 > 0 && CX3 > 0) + parser(ImageRef(ix,nowpixy)); + + CX1 -= FDY12; + CX2 -= FDY23; + CX3 -= FDY31; + } + + CY1 += FDX12; + CY2 += FDX23; + CY3 += FDX31; + + ++nowpixy; + } + } + } + + pixy += q; + } +} + +// same as above, but raster a triangle using barycentric coordinates: +// https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation +template +template +void TImage::RasterizeTriangleBary(const TPoint2& v1, const TPoint2& v2, const TPoint2& v3, PARSER& parser) +{ + // compute bounding-box fully containing the triangle + const TPoint2 boxMin(MINF3(v1.x, v2.x, v3.x), MINF3(v1.y, v2.y, v3.y)); + const TPoint2 boxMax(MAXF3(v1.x, v2.x, v3.x), MAXF3(v1.y, v2.y, v3.y)); + // check the bounding-box intersects the image + const cv::Size size(parser.Size()); + if (boxMax.x < T(0) || boxMin.x > T(size.width - 1) || + boxMax.y < T(0) || boxMin.y > T(size.height - 1)) + return; + // clip bounding-box to be fully contained by the image + ImageRef boxMinI(FLOOR2INT(boxMin)); + ImageRef boxMaxI(CEIL2INT(boxMax)); + Base::clip(boxMinI, boxMaxI, size); + // ignore back oriented triangles (negative area) + const T area(EdgeFunction(v1, v2, v3)); + if (CULL && area <= 0) + return; + // parse all pixels inside the bounding-box + const T invArea(T(1) / area); + for (int y = boxMinI.y; y <= boxMaxI.y; ++y) { + for (int x = boxMinI.x; x <= boxMaxI.x; ++x) { + const ImageRef pt(x, y); + const TPoint2 p(Cast(pt)); + // discard point if not in triangle; + // testing only for negative barycentric coordinates + // guarantees all will be in [0,1] at the end of all checks + const T b1(EdgeFunction(v2, v3, p) * invArea); + if (b1 < 0) + continue; + const T b2(EdgeFunction(v3, v1, p) * invArea); + if (b2 < 0) + continue; + const T b3(EdgeFunction(v1, v2, p) * invArea); + if (b3 < 0) + continue; + // output pixel + parser(pt, TPoint3(b1, b2, b3)); + } + } +} + +// drawing line between 2 points from left to right +// papb -> pcpd +// pa, pb, pc, pd must then be sorted before +template +inline void _ProcessScanLine(int y, const TPoint3& pa, const TPoint3& pb, const TPoint3& pc, const TPoint3& pd, PARSER& parser) { + // Thanks to current Y, we can compute the gradient to compute others values like + // the starting X (sx) and ending X (ex) to draw between + // if pa.y == pb.y or pc.y == pd.y, gradient is forced to 1 + const T gradient1 = CLAMP(pa.y != pb.y ? (T(y) - pa.y) / (pb.y - pa.y) : T(1), T(0), T(1)); + const T gradient2 = CLAMP(pc.y != pd.y ? (T(y) - pc.y) / (pd.y - pc.y) : T(1), T(0), T(1)); + + const int sx = FLOOR2INT(lerp(pa.x, pb.x, gradient1)); + const int ex = FLOOR2INT(lerp(pc.x, pd.x, gradient2)); + + // starting Z & ending Z + const T z1 = lerp(pa.z, pb.z, gradient1); + const T z2 = lerp(pc.z, pd.z, gradient2); + + // drawing a line from left (sx) to right (ex) + const T invd = T(1) / (T)(ex - sx); + for (int x = sx; x < ex; ++x) { + const T gradient = T(x - sx) * invd; + ASSERT(gradient >= T(0) && gradient <= T(1)); + const T z = lerp(z1, z2, gradient); + parser(ImageRef(x,y), z); + } +} +// Raster the given triangle and output the position and depth of each pixel of the triangle; +// based on "Learning how to write a 3D software engine � Rasterization & Z-Buffering" by Nick (David Rousset) +// http://blogs.msdn.com/b/davrous/archive/2013/06/21/tutorial-part-4-learning-how-to-write-a-3d-software-engine-in-c-ts-or-js-rasterization-amp-z-buffering.aspx +template +template +void TImage::RasterizeTriangleDepth(TPoint3 p1, TPoint3 p2, TPoint3 p3, PARSER& parser) +{ + // sorting the points in order to always have this order on screen p1, p2 & p3 + // with p1 always up (thus having the Y the lowest possible to be near the top screen) + // then p2 between p1 & p3 + if (p1.y > p2.y) + std::swap(p1, p2); + if (p2.y > p3.y) + std::swap(p2, p3); + if (p1.y > p2.y) + std::swap(p1, p2); + + // computing lines' directions (slopes) + // http://en.wikipedia.org/wiki/Slope + const T dP1P2 = (p2.y - p1.y > 0 ? (p2.x - p1.x) / (p2.y - p1.y) : T(0)); + const T dP1P3 = (p3.y - p1.y > 0 ? (p3.x - p1.x) / (p3.y - p1.y) : T(0)); + + // first case where triangles are like that: + // P1 + // - + // -- + // - - + // - - + // - - P2 + // - - + // - - + // - + // P3 + if (dP1P2 > dP1P3) { + for (int y = (int)p1.y; y <= (int)p3.y; ++y) { + if (y < p2.y) + _ProcessScanLine(y, p1, p3, p1, p2, parser); + else + _ProcessScanLine(y, p1, p3, p2, p3, parser); + } + } + // second case where triangles are like that: + // P1 + // - + // -- + // - - + // - - + // P2 - - + // - - + // - - + // - + // P3 + else { + for (int y = (int)p1.y; y <= (int)p3.y; ++y) { + if (y < p2.y) + _ProcessScanLine(y, p1, p2, p1, p3, parser); + else + _ProcessScanLine(y, p2, p3, p1, p3, parser); + } + } +} + + +template +template +void TImage::DrawLine(const TPoint2& p1, const TPoint2& p2, PARSER& parser) +{ + #if 0 + const TPoint2 d(ABS(p2 - p1)); + const int sx(p1.xd.y ? d.x : -d.y)/2), e2; + + const ImageRef x2(p2); + ImageRef p(p1); + while (true) { + parser(p); + if (p == x2) break; + e2 = err; + if (e2 >-d.x) { err -= d.y; p.x += sx; } + if (e2 < d.y) { err += d.x; p.y += sy; } + } + #else + // Bresenham's line algorithm + // https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm + T x1(p1.x), y1(p1.y), x2(p2.x), y2(p2.y); + const bool steep(ABS(y2 - y1) > ABS(x2 - x1)); + if (steep) { + std::swap(x1, y1); + std::swap(x2, y2); + } + if (x1 > x2) { + std::swap(x1, x2); + std::swap(y1, y2); + } + const T dx(x2 - x1); + const T dy(ABS(y2 - y1)); + + T error(dx / 2.f); + int y(ROUND2INT(y1)); + const int ystep((y1 < y2) ? 1 : -1); + const int maxX(ROUND2INT(x2)); + for (int x=ROUND2INT(x1); x +bool TImage::DrawLineAntialias(Point2f x1, Point2f x2, + FncDrawPointAntialias fncDrawPoint, void* pData) +{ + bool revert = false; + const Point2f dx = x2 - x1; + if (ABS(dx.x) > ABS(dx.y)) { + const ImageRef delta(0,1); + if (x2.x < x1.x) { + std::swap(x1, x2); + revert = true; + } + + const float gradient = dx.y / dx.x; + int xpxl1 = round_(x1.x); + float yend = x1.y + gradient*(float(xpxl1) - x1.x); + int ypxl1 = ipart_(yend); + float xgap = rfpart_(x1.x + 0.5); + float tmp = fpart_(yend)*xgap; + fncDrawPoint(ImageRef(xpxl1, ypxl1), delta, xgap-tmp, tmp, pData); + + float intery = yend + gradient; + int xpxl2 = round_(x2.x); + yend = x2.y + gradient*(float(xpxl2) - x2.x); + int ypxl2 = ipart_(yend); + for (int x=xpxl1+1; x +bool TImage::DrawLineAntialias(const ImageRef& x1, const ImageRef& x2, + FncDrawPointAntialias fncDrawPoint, void* pData) +{ + const ImageRef dx = x2 - x1; + if (ABS(dx.x) > ABS(dx.y)) { + const ImageRef delta(0,1); + const float gradient = (float)dx.y / dx.x; + float intery = float(x1.y) + gradient; + if (x2.x < x1.x) { + for (int x=x1.x-1; x>x2.x; --x) { + const float tmp = fpart_(intery); + fncDrawPoint(ImageRef(x, ipart_(intery)), delta, 1.f-tmp, tmp, pData); + intery += gradient; + } + return true; + } + else { + for (int x=x1.x+1; xx2.y; --y) { + const float tmp = fpart_(interx); + fncDrawPoint(ImageRef(ipart_(interx), y), delta, 1.f-tmp, tmp, pData); + interx += gradient; + } + return true; + } + else { + for (int y=x1.y+1; y +template +void TImage::DilateMean(TImage& dst, const TYPE& invalid) const +{ + TImage out; + Base::copyTo(out); + const int RowsEnd(rows-HalfSize); + const int ColsEnd(cols-HalfSize); + for (int r=HalfSize; r 0) + out(r,c) = (n > 1 ? vo/n : vo); + } + } + dst = out; +} +/*----------------------------------------------------------------*/ + + +template +bool TImage::Load(const String& fileName) +{ + if (Util::getFileExt(fileName).ToLower() == ".pfm") { + if (Base::depth() != CV_32F) + return false; + File fImage(fileName, File::READ, File::OPEN); + if (!fImage.isOpen()) + return false; + ASSERT(sizeof(float) == 4); + int i; + char buffer[128]; + // check header + for (i=0; i<4; ++i) { + if (fImage.read(buffer+i, 1) != 1) + return false; + if (buffer[i] == '\n') + break; + } + if (buffer[0] != 'P' || buffer[1] != 'f' || buffer[i] != '\n') + return false; + // read resolution + int w, h; + for (i=0; i<127; ++i) { + if (fImage.read(buffer+i, 1) != 1) + return false; + if (buffer[i] == '\n') + break; + } + buffer[i] = 0; + if (sscanf(buffer, "%d %d", &w, &h) != 2) + return false; + // read number of channels + double sc; + for (i=0; i<127; ++i) { + if (fImage.read(buffer+i, 1) != 1) + return false; + if (buffer[i] == '\n') + break; + } + buffer[i] = 0; + if (sscanf(buffer, "%lf", &sc) != 1) + return false; + const bool bLittleEndian(sc < 0); + #if __BYTE_ORDER == __LITTLE_ENDIAN + ASSERT(bLittleEndian); + #else + ASSERT(!bLittleEndian); + #endif + const int nChannels(bLittleEndian ? -((int)sc) : (int)sc); + if (nChannels != Base::channels()) + return false; + Base::create(h, w); + ASSERT(sizeof(float)*Base::channels() == Base::step.p[1]); + const size_t rowbytes((size_t)Base::size.p[1]*Base::step.p[1]); + for (int i=rows; i>0; ) + if (fImage.read(cv::Mat::template ptr(--i), rowbytes) != rowbytes) + return false; + return true; + } + cv::Mat img(cv::imread(fileName, cv::IMREAD_UNCHANGED)); + if (img.empty()) { + VERBOSE("error: loading image '%s'", fileName.c_str()); + return false; + } + if (img.channels() != Base::channels()) { + if (img.channels() == 3 && Base::channels() == 1) + cv::cvtColor(img, img, cv::COLOR_BGR2GRAY); + else if (img.channels() == 1 && Base::channels() == 3) + cv::cvtColor(img, img, cv::COLOR_GRAY2BGR); + else if (img.channels() == 4 && Base::channels() == 1) + cv::cvtColor(img, img, cv::COLOR_BGRA2GRAY); + else if (img.channels() == 1 && Base::channels() == 4) + cv::cvtColor(img, img, cv::COLOR_GRAY2BGRA); + else if (img.channels() == 4 && Base::channels() == 3) + cv::cvtColor(img, img, cv::COLOR_BGRA2BGR); + } + if (img.type() == Base::type()) + cv::swap(img, *this); + else + img.convertTo(*this, Base::type()); + return true; +} +/*----------------------------------------------------------------*/ + +template +bool TImage::Save(const String& fileName) const +{ + std::vector compression_params; + const String ext(Util::getFileExt(fileName).ToLower()); + if (ext == ".png") { + compression_params.push_back(cv::IMWRITE_PNG_COMPRESSION); + compression_params.push_back(6); + } else + if (ext == ".jpg") { + compression_params.push_back(cv::IMWRITE_JPEG_QUALITY); + compression_params.push_back(95); + } else + if (ext == ".pfm") { + if (Base::depth() != CV_32F) + return false; + Util::ensureFolder(fileName); + File fImage(fileName, File::WRITE, File::CREATE | File::TRUNCATE); + if (!fImage.isOpen()) + return false; + ASSERT(sizeof(float) == 4); + #if __BYTE_ORDER == __LITTLE_ENDIAN + static const double scale(-1.0); + #else + static const double scale(1.0); + #endif + fImage.print("Pf\n%d %d\n%lf\n", width(), height(), scale*Base::channels()); + ASSERT(sizeof(float)*Base::channels() == Base::step.p[1]); + const size_t rowbytes = (size_t)Base::size.p[1]*Base::step.p[1]; + for (int i=rows; i>0; ) + fImage.write(cv::Mat::template ptr(--i), rowbytes); + return true; + } + + try { + if (!cv::imwrite(fileName, *this, compression_params)) { + VERBOSE("error: saving image '%s'", fileName.c_str()); + return false; + } + } + catch (std::runtime_error& ex) { + VERBOSE("error: saving image '%s' (exception: %s)", fileName.c_str(), ex.what()); + return false; + } + return true; +} +/*----------------------------------------------------------------*/ + +#ifndef _RELEASE +template +void TImage::Show(const String& winname, int delay, bool bDestroy) const +{ + cv::imshow(winname, *this); + cv::waitKey(delay); + if (bDestroy) + cv::destroyWindow(winname); +} +/*----------------------------------------------------------------*/ +#endif + + +// C L A S S ////////////////////////////////////////////////////// + +// Create a derivative kernel, such that you can take the +// derivative of an image by convolving with the kernel horizontally and vertically +inline const TMatrix& CreateDerivativeKernel3x3() { + static const double kernel[3*3] = { + -1.0/8.0, 0.0, 1.0/8.0, + -2.0/8.0, 0.0, 2.0/8.0, + -1.0/8.0, 0.0, 1.0/8.0 + }; + return *((const TMatrix*)kernel); +} +inline const TMatrix& CreateDerivativeKernel3x3xx() { + static const double kernel[3*3] = { + 1.0/6.0, -2.0/6.0, 1.0/6.0, + 4.0/6.0, -8.0/6.0, 4.0/6.0, + 1.0/6.0, -2.0/6.0, 1.0/6.0 + }; + return *((const TMatrix*)kernel); +} +inline const TMatrix& CreateDerivativeKernel3x3xy() { + static const double kernel[3*3] = { + 1.0/4.0, 0.0, -1.0/4.0, + 0.0, 0.0, 0.0, + -1.0/4.0, 0.0, 1.0/4.0 + }; + return *((const TMatrix*)kernel); +} +// same as above, but using central differences like approach: Noise Robust Gradient Operators +// (see: http://www.holoborodko.com/pavel/image-processing/edge-detection) +inline const TMatrix& CreateDerivativeKernel3x5() { + static const double kernel[3*5] = { + -1.0/32.0, -2.0/32.0, 0.0, 2.0/32.0, 1.0/32.0, + -2.0/32.0, -4.0/32.0, 0.0, 4.0/32.0, 2.0/32.0, + -1.0/32.0, -2.0/32.0, 0.0, 2.0/32.0, 1.0/32.0 + }; + return *((const TMatrix*)kernel); +} +inline const TMatrix& CreateDerivativeKernel5x7() { + static const double kernel[5*7] = { + -1.0/512.0, -4.0/512.0, -5.0/512.0, 0.0, 5.0/512.0, 4.0/512.0, 1.0/512.0, + -4.0/512.0, -16.0/512.0, 20.0/512.0, 0.0, 20.0/512.0, 16.0/512.0, 4.0/512.0, + -6.0/512.0, -24.0/512.0, 30.0/512.0, 0.0, 30.0/512.0, 24.0/512.0, 6.0/512.0, + -4.0/512.0, -16.0/512.0, 20.0/512.0, 0.0, 20.0/512.0, 16.0/512.0, 4.0/512.0, + -1.0/512.0, -4.0/512.0, -5.0/512.0, 0.0, 5.0/512.0, 4.0/512.0, 1.0/512.0 + }; + return *((const TMatrix*)kernel); +} + +// Zero mean Gaussian. +inline double Gaussian(double x, double sigma) { + return 1/sqrt(2*M_PI*sigma*sigma) * exp(-(x*x/2.0/sigma/sigma)); +} +// 2D gaussian (zero mean) +// (see: (9) in http://mathworld.wolfram.com/GaussianFunction.html) +inline double Gaussian2D(double x, double y, double sigma) { + return 1.0/(2.0*M_PI*sigma*sigma) * exp( -(x*x+y*y)/(2.0*sigma*sigma)); +} +inline double GaussianDerivative(double x, double sigma) { + return -x / sigma / sigma * Gaussian(x, sigma); +} +// Solve the inverse of the Gaussian for positive x. +inline double GaussianInversePositive(double y, double sigma) { + return sqrt(-2.0 * sigma * sigma * log(y * sigma * sqrt(2.0*M_PI))); +} + +// Compute the Gaussian kernel width corresponding to the given sigma. +inline int ComputeGaussianKernelWidth(double sigma) { + ASSERT(sigma >= 0.0); + // 0.004 implies a 3 pixel kernel with 1 pixel sigma. + const double truncation_factor(0.004); + // Calculate the kernel size based on sigma such that it is odd. + const double precisehalfwidth(GaussianInversePositive(truncation_factor, sigma)); + int width = ROUND2INT(2*precisehalfwidth); + if (width % 2 == 0) + width++; + return width; +} +// Compute a Gaussian kernel, such that you can compute the blurred image +// by convolving with the kernel horizontally then vertically. +inline void ComputeGaussianKernel(double sigma, DVector64F& kernel) { + const int width(ComputeGaussianKernelWidth(sigma)); + // Calculate the gaussian kernel and its derivative. + kernel.create(width); + kernel.memset(0); + const int halfwidth(width / 2); + for (int i = -halfwidth; i <= halfwidth; ++i) + kernel(i + halfwidth) = Gaussian(i, sigma); + // Since images should not get brighter or darker, normalize. + cv::normalize(kernel, kernel, 1.0, 0.0, cv::NORM_L1); +} +// Compute a Gaussian kernel and derivative, such that you can take the +// derivative of an image by convolving with the kernel horizontally then the +// derivative vertically to get (eg) the y derivative. +inline void ComputeGaussianKernel(double sigma, DVector64F& kernel, DVector64F& derivative) { + const int width(ComputeGaussianKernelWidth(sigma)); + // Calculate the gaussian kernel and its derivative. + kernel.create(width); + derivative.create(width); + kernel.memset(0); + derivative.memset(0); + const int halfwidth(width / 2); + for (int i = -halfwidth; i <= halfwidth; ++i) { + kernel(i + halfwidth) = Gaussian(i, sigma); + derivative(i + halfwidth) = GaussianDerivative(i, sigma); + } + // Since images should not get brighter or darker, normalize. + cv::normalize(kernel, kernel, 1.0, 0.0, cv::NORM_L1); + // Normalize the derivative differently. See + // www.cs.duke.edu/courses/spring03/cps296.1/handouts/Image%20Processing.pdf + double factor(0); + for (int i = -halfwidth; i <= halfwidth; ++i) { + factor -= derivative(i+halfwidth)*i; + } + derivative /= factor; +} + +template +void FastConvolve(const DVector64F& kernel, int width, int height, + const Type* src, int src_stride, int src_line_stride, + Type* dst, int dst_stride) { + double coefficients[2 * size + 1]; + for (int k = 0; k < 2 * size + 1; ++k) { + coefficients[k] = kernel(2 * size - k); + } + // Fast path: if the kernel has a certain size, use the constant sized loops. + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + double sum(0); + for (int k = -size; k <= size; ++k) { + if (vertical) { + if (y + k >= 0 && y + k < height) { + sum += double(src[k * src_line_stride]) * coefficients[k + size]; + } + } else { + if (x + k >= 0 && x + k < width) { + sum += double(src[k * src_stride]) * coefficients[k + size]; + } + } + } + dst[0] = static_cast(sum); + src += src_stride; + dst += dst_stride; + } + } +} +template +void Convolve(const TImage& in, + const DVector64F& kernel, + TImage& out) { + ASSERT(kernel.rows % 2 == 1); + ASSERT(&in != &out); + + const int width = in.width(); + const int height = in.height(); + out.create(in.size()); + + const int src_line_stride = in.step1(0); + const int src_stride = in.step1(1); + const int dst_stride = out.step1(1); + const Type* src = in.cv::Mat::template ptr(); + Type* dst = out.cv::Mat::template ptr(); + + // Use a dispatch table to make most convolutions used in practice use the + // fast path. + const int half_width(kernel.rows / 2); + switch (half_width) { + #define static_convolution(size) case size: \ + FastConvolve(kernel, width, height, src, src_stride, \ + src_line_stride, dst, dst_stride); break; + static_convolution(1) + static_convolution(2) + static_convolution(3) + static_convolution(4) + static_convolution(5) + static_convolution(6) + static_convolution(7) + #undef static_convolution + default: + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + double sum = 0; + // Slow path: this loop cannot be unrolled. + for (int k = -half_width; k <= half_width; ++k) { + if (vertical) { + if (y + k >= 0 && y + k < height) { + sum += double(src[k * src_line_stride]) * kernel(2 * half_width - (k + half_width)); + } + } else { + if (x + k >= 0 && x + k < width) { + sum += double(src[k * src_stride]) * kernel(2 * half_width - (k + half_width)); + } + } + } + dst[0] = static_cast(sum); + src += src_stride; + dst += dst_stride; + } + } + } +} +template +inline void ConvolveHorizontal(const TImage& in, const DVector64F& kernel, TImage& out) { + Convolve(in, kernel, out); +} +template +inline void ConvolveVertical(const TImage& in, const DVector64F& kernel, TImage& out) { + Convolve(in, kernel, out); +} + +// Compute the gaussian blur of an image, and store the results in one channel. +template +void BlurredImage(const TImage& in, TImage& blurred, double sigma=0.32) { + ASSERT(in.channels() == 1); + DVector64F kernel; + ComputeGaussianKernel(sigma, kernel); + TImage tmp; + // Compute convolved image. + blurred.create(in.size()); + ConvolveVertical(in, kernel, tmp); + ConvolveHorizontal(tmp, kernel, blurred); +} +// Compute the gaussian blur of an image and the derivatives of the blurred +// image, and store the results in three channels. +template +void BlurredImageAndDerivatives(const TImage& in, TImage& blurred, TImage grad[2], double sigma=0.32) { + ASSERT(in.channels() == 1); + DVector64F kernel, derivative; + ComputeGaussianKernel(sigma, kernel, derivative); + TImage tmp, dir[2]; + // Compute convolved image. + blurred.create(in.size()); + ConvolveVertical(in, kernel, tmp); + ConvolveHorizontal(tmp, kernel, blurred); + // Compute first derivative in x. + ConvolveHorizontal(tmp, derivative, grad[0]); + // Compute first derivative in y. + ConvolveHorizontal(in, kernel, tmp); + ConvolveVertical(tmp, derivative, grad[1]); +} +template +void BlurredImageAndDerivatives(const TImage& in, TImage& blurred, TImage< TPoint2 >& grad, double sigma=0.32) { + TImage dir[2]; + BlurredImageAndDerivatives(in, blurred, grad, sigma); + cv::merge(dir, 2, grad); +} +/*----------------------------------------------------------------*/ + + +// C L A S S ////////////////////////////////////////////////////// + +// returns determinant, note that if it's zero no inversion done; +// use Mi == M to store result into M +template +TYPE InvertMatrix3x3(const TYPE* m, TYPE* mi) { + const TYPE d(m[0]*(m[4]*m[8]-m[5]*m[7])+m[1]*(m[5]*m[6]-m[3]*m[8])+m[2]*(m[3]*m[7]-m[4]*m[6])); + if (d == TYPE(0)) + return TYPE(0); + const TYPE invd = TYPE(1)/d; + + TYPE mc[9]; + const TYPE* mm = m; + if (mi == m) { + memcpy(mc, m, sizeof(TYPE)*9); + mm = mc; + } + + mi[0] = (mm[4]*mm[8]-mm[5]*mm[7])*invd; + mi[1] = (mm[2]*mm[7]-mm[1]*mm[8])*invd; + mi[2] = (mm[1]*mm[5]-mm[2]*mm[4])*invd; + mi[3] = (mm[5]*mm[6]-mm[3]*mm[8])*invd; + mi[4] = (mm[0]*mm[8]-mm[2]*mm[6])*invd; + mi[5] = (mm[2]*mm[3]-mm[0]*mm[5])*invd; + mi[6] = (mm[3]*mm[7]-mm[4]*mm[6])*invd; + mi[7] = (mm[1]*mm[6]-mm[0]*mm[7])*invd; + mi[8] = (mm[0]*mm[4]-mm[1]*mm[3])*invd; + + return d; +} +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + + +// C L A S S ////////////////////////////////////////////////////// + +#ifdef _USE_EIGEN + +///Compute a rotation exponential using the Rodrigues Formula. +///The rotation axis is given by \f$\vec{w}\f$, and the rotation angle must +///be computed using \f$ \theta = |\vec{w}|\f$. This is provided as a separate +///function primarily to allow fast and rough matrix exponentials using fast +///and rough approximations to \e A and \e B. +/// +///@param w Vector about which to rotate. +///@param A \f$\frac{\sin \theta}{\theta}\f$ +///@param B \f$\frac{1 - \cos \theta}{\theta^2}\f$ +///@param R Matrix to hold the return value. +///@relates SO3 +template +inline void eigen_SO3_exp(const typename Eigen::SO3::Vec3& w, typename Eigen::SO3::Mat3& R) { + static const Precision one_6th(1.0/6.0); + static const Precision one_20th(1.0/20.0); + //Use a Taylor series expansion near zero. This is required for + //accuracy, since sin t / t and (1-cos t)/t^2 are both 0/0. + Precision A, B; + const Precision theta_sq(w.squaredNorm()); + if (theta_sq < Precision(1e-8)) { + A = Precision(1) - one_6th * theta_sq; + B = Precision(0.5); + } else { + if (theta_sq < Precision(1e-6)) { + B = Precision(0.5) - Precision(0.25) * one_6th * theta_sq; + A = Precision(1) - theta_sq * one_6th*(Precision(1) - one_20th * theta_sq); + } else { + const Precision theta(sqrt(theta_sq)); + const Precision inv_theta(Precision(1)/theta); + A = sin(theta) * inv_theta; + B = (Precision(1) - cos(theta)) * (inv_theta * inv_theta); + } + } + { + const Precision wx2(w(0)*w(0)); + const Precision wy2(w(1)*w(1)); + const Precision wz2(w(2)*w(2)); + R(0,0) = Precision(1) - B*(wy2 + wz2); + R(1,1) = Precision(1) - B*(wx2 + wz2); + R(2,2) = Precision(1) - B*(wx2 + wy2); + } + { + const Precision a(A*w[2]); + const Precision b(B*(w[0]*w[1])); + R(0,1) = b - a; + R(1,0) = b + a; + } + { + const Precision a(A*w[1]); + const Precision b(B*(w[0]*w[2])); + R(0,2) = b + a; + R(2,0) = b - a; + } + { + const Precision a(A*w[0]); + const Precision b(B*(w[1]*w[2])); + R(1,2) = b - a; + R(2,1) = b + a; + } +} +template +inline typename Eigen::SO3::Mat3 Eigen::SO3::exp(const Vec3& w) const { + Mat3 result; + eigen_SO3_exp(w, result); + return result; +} + +/// Take the logarithm of the matrix, generating the corresponding vector in the Lie Algebra. +/// See the Detailed Description for details of this vector. +template +inline void eigen_SO3_ln(const typename Eigen::SO3::Mat3& R, typename Eigen::SO3::Vec3& w) { + const Precision cos_angle((R(0,0) + R(1,1) + R(2,2) - Precision(1)) * Precision(0.5)); + w(0) = (R(2,1)-R(1,2))*Precision(0.5); + w(1) = (R(0,2)-R(2,0))*Precision(0.5); + w(2) = (R(1,0)-R(0,1))*Precision(0.5); + + const Precision sin_angle_abs(sqrt(w.squaredNorm())); + if (cos_angle > Precision(M_SQRT1_2)) { // [0 - Pi/4] use asin + if (sin_angle_abs > Precision(0)) + w *= asin(sin_angle_abs) / sin_angle_abs; + } else if (cos_angle > Precision(-M_SQRT1_2)) { // [Pi/4 - 3Pi/4] use acos, but antisymmetric part + if (sin_angle_abs > Precision(0)) + w *= acos(cos_angle) / sin_angle_abs; + } else { // rest use symmetric part + // antisymmetric part vanishes, but still large rotation, need information from symmetric part + const Precision angle(Precision(M_PI) - asin(sin_angle_abs)); + const Precision d0(R(0,0) - cos_angle); + const Precision d1(R(1,1) - cos_angle); + const Precision d2(R(2,2) - cos_angle); + typename Eigen::SO3::Vec3 r2; + if (d0*d0 > d1*d1 && d0*d0 > d2*d2) { // first is largest, fill with first column + r2(0) = d0; + r2(1) = (R(1,0)+R(0,1))*Precision(0.5); + r2(2) = (R(0,2)+R(2,0))*Precision(0.5); + } else if (d1*d1 > d2*d2) { // second is largest, fill with second column + r2(0) = (R(1,0)+R(0,1))*Precision(0.5); + r2(1) = d1; + r2(2) = (R(2,1)+R(1,2))*Precision(0.5); + } else { // third is largest, fill with third column + r2(0) = (R(0,2)+R(2,0))*Precision(0.5); + r2(1) = (R(2,1)+R(1,2))*Precision(0.5); + r2(2) = d2; + } + // flip, if we point in the wrong direction! + if (r2.dot(w) < Precision(0)) + r2 *= Precision(-1); + w = r2 * (angle/r2.norm()); + } +} +template +inline typename Eigen::SO3::Vec3 Eigen::SO3::ln() const { + Vec3 result; + eigen_SO3_ln(mat, result); + return result; +} + +/// Write an SO3 to a stream +/// @relates SO3 +template +inline std::ostream& operator<<(std::ostream& os, const Eigen::SO3& rhs) { + return os << rhs.get_matrix(); +} +/// Read from SO3 to a stream +/// @relates SO3 +template +inline std::istream& operator>>(std::istream& is, Eigen::SO3& rhs) { + is >> rhs.mat; + rhs.coerce(); + return is; +} + +/// Right-multiply by a Vector +/// @relates SO3 +template +inline Eigen::Matrix operator*(const Eigen::SO3

& lhs, const Eigen::Matrix& rhs) { + return lhs.get_matrix() * rhs; +} +/// Left-multiply by a Vector +/// @relates SO3 +template +inline Eigen::Matrix operator*(const Eigen::Matrix& lhs, const Eigen::SO3

& rhs) { + return lhs * rhs.get_matrix(); +} +/// Right-multiply by a matrix +/// @relates SO3 +template +inline Eigen::Matrix operator*(const Eigen::SO3

& lhs, const Eigen::Matrix& rhs) { + return lhs.get_matrix() * rhs; +} +/// Left-multiply by a matrix +/// @relates SO3 +template +inline Eigen::Matrix operator*(const Eigen::Matrix& lhs, const Eigen::SO3

& rhs) { + return lhs * rhs.get_matrix(); +} +/*----------------------------------------------------------------*/ + + +/// Exponentiate an angle in the Lie algebra to generate a new SO2. +template +inline void eigen_SO2_exp(const Precision& d, typename Eigen::SO2::Mat2& R) { + R(0,0) = R(1,1) = cos(d); + R(1,0) = sin(d); + R(0,1) = -R(1,0); +} +template +inline typename Eigen::SO2::Mat2 Eigen::SO2::exp(const Precision& d) const { + Mat2 result; + eigen_SO2_exp(d, result); + return result; +} + +/// Extracts the rotation angle from the SO2 +template +inline void eigen_SO2_ln(const typename Eigen::SO2::Mat2& R, Precision& d) { + d = atan2(R(1,0), R(0,0)); +} +template +inline Precision Eigen::SO2::ln() const { + Precision d; + eigen_SO2_ln(mat, d); + return d; +} + +/// Write an SO2 to a stream +/// @relates SO2 +template +inline std::ostream& operator<<(std::ostream& os, const Eigen::SO2 & rhs) { + return os << rhs.get_matrix(); +} +/// Read from SO2 to a stream +/// @relates SO2 +template +inline std::istream& operator>>(std::istream& is, Eigen::SO2& rhs) { + is >> rhs.mat; + rhs.coerce(); + return is; +} + +/// Right-multiply by a Vector +/// @relates SO2 +template +inline Eigen::Matrix operator*(const Eigen::SO2

& lhs, const Eigen::Matrix& rhs) { + return lhs.get_matrix() * rhs; +} +/// Left-multiply by a Vector +/// @relates SO2 +template +inline Eigen::Matrix operator*(const Eigen::Matrix& lhs, const Eigen::SO2

& rhs) { + return lhs * rhs.get_matrix(); +} +/// Right-multiply by a Matrix +/// @relates SO2 +template +inline Eigen::Matrix operator*(const Eigen::SO2

& lhs, const Eigen::Matrix& rhs) { + return lhs.get_matrix() * rhs; +} +/// Left-multiply by a Matrix +/// @relates SO2 +template +inline Eigen::Matrix operator*(const Eigen::Matrix& lhs, const Eigen::SO2

& rhs) { + return lhs * rhs.get_matrix(); +} +/*----------------------------------------------------------------*/ + +namespace Eigen { + +template +std::istream& operator >> (std::istream& st, MatrixBase& m) { + for (int i = 0; i < m.rows(); ++i) + for (int j = 0; j < m.cols(); ++j) + st >> m(i, j); + return st; +} + +} // namespace Eigen +/*----------------------------------------------------------------*/ + +#endif // _USE_EIGEN + + +// C L A S S ////////////////////////////////////////////////////// + +#ifdef _USE_BOOST + +namespace boost { + namespace serialization { + + // Serialization support for cv::Mat + template + void save(Archive& ar, const cv::Mat& m, const unsigned int /*version*/) { + const int elem_type = m.type(); + const size_t elem_size = m.elemSize(); + + ar & m.cols; + ar & m.rows; + ar & elem_type; + ar & elem_size; + + const size_t data_size = elem_size * m.cols * m.rows; + if (m.isContinuous()) { + ar & boost::serialization::make_array(m.ptr(), data_size); + } else { + cv::Mat m_cont; + m.copyTo(m_cont); + ar & boost::serialization::make_array(m_cont.ptr(), data_size); + } + } + template + void load(Archive& ar, cv::Mat& m, const unsigned int /*version*/) { + int cols, rows, elem_type; + size_t elem_size; + + ar & cols; + ar & rows; + ar & elem_type; + ar & elem_size; + + m.create(rows, cols, elem_type); + + const size_t data_size = elem_size * m.cols * m.rows; + ar & boost::serialization::make_array(m.ptr(), data_size); + } + template + inline void serialize(Archive& ar, cv::Mat& m, const unsigned int version) { + split_free(ar, m, version); + } + + // Serialization support for cv::Mat_ + template + void save(Archive& ar, const cv::Mat_<_Tp>& m, const unsigned int /*version*/) { + ar & m.cols; + ar & m.rows; + + const size_t data_size = m.cols * m.rows; + if (m.isContinuous()) { + ar & boost::serialization::make_array((const _Tp*)m.ptr(), data_size); + } else { + cv::Mat_<_Tp> m_cont; + m.copyTo(m_cont); + ar & boost::serialization::make_array((const _Tp*)m_cont.ptr(), data_size); + } + } + template + void load(Archive& ar, cv::Mat_<_Tp>& m, const unsigned int /*version*/) { + int cols, rows; + ar & cols; + ar & rows; + + m.create(rows, cols); + + const size_t data_size = m.cols * m.rows; + for (size_t n=0; n + inline void serialize(Archive& ar, cv::Mat_<_Tp>& m, const unsigned int version) { + split_free(ar, m, version); + } + + // Serialization support for cv::Matx + template + void serialize(Archive& ar, cv::Matx<_Tp, m, n>& _m, const unsigned int /*version*/) { + ar & _m.val; + } + + // Serialization support for cv::Vec + template + void serialize(Archive& ar, cv::Vec<_Tp, cn>& v, const unsigned int /*version*/) { + ar & boost::serialization::base_object >(v); + } + + // Serialization support for cv::Point_ + template + void serialize(Archive& ar, cv::Point_<_Tp>& pt, const unsigned int /*version*/) { + ar & pt.x & pt.y; + } + + // Serialization support for cv::Point3_ + template + void serialize(Archive& ar, cv::Point3_<_Tp>& pt, const unsigned int /*version*/) { + ar & pt.x & pt.y & pt.z; + } + + // Serialization support for cv::Size_ + template + void serialize(Archive& ar, cv::Size_<_Tp>& sz, const unsigned int /*version*/) { + ar & sz.width & sz.height; + } + + // Serialization support for cv::Rect_ + template + void serialize(Archive& ar, cv::Rect_<_Tp>& rc, const unsigned int /*version*/) { + ar & rc.x & rc.y & rc.width & rc.height; + } + + // Serialization support for cv::KeyPoint + template + void serialize(Archive& ar, cv::KeyPoint& k, const unsigned int /*version*/) { + ar & k.pt; + ar & k.size; + ar & k.angle; + ar & k.response; + ar & k.octave; + ar & k.class_id; + } + + // Serialization support for cv::DMatch + template + void serialize(Archive& ar, cv::DMatch& m, const unsigned int /*version*/) { + ar & m.queryIdx; + ar & m.trainIdx; + ar & m.imgIdx; + ar & m.distance; + } + + #ifdef _USE_EIGEN + // Serialization support for Eigen::Matrix + // specialization handling fixed sized matrices + template + inline void save(Archive& ar, const Eigen::Matrix& M, const unsigned int /*version*/) { + ar << make_nvp("data", make_array(M.data(), _Rows*_Cols)); + } + template + inline void load(Archive& ar, Eigen::Matrix& M, const unsigned int /*version*/) { + ar >> make_nvp("data", make_array(M.data(), _Rows*_Cols)); + } + // The function that causes boost::serialization to look for separate + // save() and load() functions when serializing and Eigen matrix. + template + inline void serialize(Archive& ar, Eigen::Matrix& M, const unsigned int version) { + split_free(ar, M, version); + } + #endif // _USE_EIGEN + + } // namespace serialization +} // namespace boost +/*----------------------------------------------------------------*/ + +// include headers that implement a archive in simple text and binary format or XML format +#if defined(_MSC_VER) +#pragma warning (push) +#pragma warning (disable : 4275) // non dll-interface class +#pragma warning (disable : 4715) // not all control paths return a value +#endif +#include +#include +#include +#include +//#include +//#include +// include headers that implement compressed serialization support +#include +#include +#if BOOST_VERSION >= 106900 +#include +#endif +#if defined(_MSC_VER) +#pragma warning (pop) +#endif + +enum ARCHIVE_TYPE { + ARCHIVE_MVS = -1, + ARCHIVE_TEXT = 0, + ARCHIVE_BINARY, + ARCHIVE_BINARY_ZIP, + ARCHIVE_BINARY_ZSTD, + ARCHIVE_LAST, + #if BOOST_VERSION >= 106900 + ARCHIVE_DEFAULT = ARCHIVE_BINARY_ZSTD + #else + ARCHIVE_DEFAULT = ARCHIVE_BINARY_ZIP + #endif +}; + +// export the current state of the given reconstruction object +template +bool SerializeSave(const TYPE& obj, std::ofstream& fs, ARCHIVE_TYPE type, unsigned flags=boost::archive::no_header) +{ + // serialize out the current state + switch (type) { + case ARCHIVE_TEXT: { + boost::archive::text_oarchive ar(fs, flags); + ar << obj; + break; } + case ARCHIVE_BINARY: { + boost::archive::binary_oarchive ar(fs, flags); + ar << obj; + break; } + case ARCHIVE_BINARY_ZIP: { + namespace io = boost::iostreams; + io::filtering_streambuf ffs; + ffs.push(io::zlib_compressor(io::zlib::best_speed)); + ffs.push(fs); + boost::archive::binary_oarchive ar(ffs, flags); + ar << obj; + break; } + #if BOOST_VERSION >= 106900 + case ARCHIVE_BINARY_ZSTD: { + namespace io = boost::iostreams; + io::filtering_streambuf ffs; + ffs.push(io::zstd_compressor(io::zstd::best_speed)); + ffs.push(fs); + boost::archive::binary_oarchive ar(ffs, flags); + ar << obj; + break; } + #endif + default: + VERBOSE("error: Can not save the object, invalid archive type"); + return false; + } + return true; +} // SerializeSave +template +bool SerializeSave(const TYPE& obj, const SEACAVE::String& fileName, ARCHIVE_TYPE type, unsigned flags=boost::archive::no_header) +{ + // open the output stream + std::ofstream fs(fileName, std::ios::out | std::ios::binary); + if (!fs.is_open()) + return false; + // serialize out the current state + return SerializeSave(obj, fs, type, flags); +} // SerializeSave + +// import the state to the given reconstruction object +template +bool SerializeLoad(TYPE& obj, std::ifstream& fs, ARCHIVE_TYPE type, unsigned flags=boost::archive::no_header) +{ + try { + // serialize in the saved state + switch (type) { + case ARCHIVE_TEXT: { + boost::archive::text_iarchive ar(fs, flags); + ar >> obj; + break; } + case ARCHIVE_BINARY: { + boost::archive::binary_iarchive ar(fs, flags); + ar >> obj; + break; } + case ARCHIVE_BINARY_ZIP: { + namespace io = boost::iostreams; + io::filtering_streambuf ffs; + ffs.push(io::zlib_decompressor()); + ffs.push(fs); + boost::archive::binary_iarchive ar(ffs, flags); + ar >> obj; + break; } + #if BOOST_VERSION >= 106900 + case ARCHIVE_BINARY_ZSTD: { + namespace io = boost::iostreams; + io::filtering_streambuf ffs; + ffs.push(io::zstd_decompressor()); + ffs.push(fs); + boost::archive::binary_iarchive ar(ffs, flags); + ar >> obj; + break; } + #endif + default: + VERBOSE("error: Can not load the object, invalid archive type"); + return false; + } + } + catch (const std::exception& e) { + VERBOSE("error: invalid stream (%s)", e.what()); + return false; + } + return true; +} // SerializeLoad +template +bool SerializeLoad(TYPE& obj, const SEACAVE::String& fileName, ARCHIVE_TYPE type, unsigned flags=boost::archive::no_header) +{ + // open the input stream + std::ifstream fs(fileName, std::ios::in | std::ios::binary); + if (!fs.is_open()) + return false; + // serialize in the saved state + return SerializeLoad(obj, fs, type, flags); +} // SerializeLoad +/*----------------------------------------------------------------*/ + +#endif // _USE_BOOST + + +#ifdef _USE_BREAKPAD + +#include +#include +#include +#include + +class MiniDumper { +public: + static inline void Create(const SEACAVE::String& strAppNameANSI, SEACAVE::String strDumpPathANSI, MINIDUMP_TYPE dumpType=MiniDumpNormal, bool bOutOfProcess=true) { + static MiniDumper oMiniDumper(strAppNameANSI, strDumpPathANSI, dumpType, bOutOfProcess); + } + static inline void Create(const SEACAVE::String& strAppNameANSI, SEACAVE::String strDumpPathANSI, int verbosity, bool bOutOfProcess=true) { + MINIDUMP_TYPE dumpType; + switch (verbosity) { + case 1: + dumpType = MiniDumpWithDataSegs; + break; + case 2: + dumpType = MiniDumpWithFullMemory; + break; + default: + dumpType = MiniDumpNormal; + } + Create(strAppNameANSI, strDumpPathANSI, dumpType, bOutOfProcess); + } + MiniDumper(const SEACAVE::String& strAppNameANSI, SEACAVE::String strDumpPathANSI, MINIDUMP_TYPE dumpType=MiniDumpNormal, bool bOutOfProcess=true) + : + crash_server(NULL), + handler(NULL) + { + if (strDumpPathANSI.IsEmpty()) + strDumpPathANSI = SEACAVE::Util::getCurrentFolder(); + const std::wstring strDumpPath(strDumpPathANSI.begin(), strDumpPathANSI.end()); + const std::wstring strAppName(strAppNameANSI.begin(), strAppNameANSI.end()); + std::wstring strPipeName; + if (bOutOfProcess) { + // try to create an out-of-process minidumper + const SEACAVE::String strUUIDANSI(SEACAVE::Util::getUniqueName(0)); + const std::wstring strUUID(strUUIDANSI.begin(), strUUIDANSI.end()); + strPipeName = L"\\\\.\\pipe\\BreakpadCrashServices\\"+strAppName+L"-"+strUUID; + crash_server = new google_breakpad::CrashGenerationServer( + strPipeName, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + true, + &strDumpPath); + if (!crash_server->Start()) { + VERBOSE("error: unable to start minidump server"); + delete crash_server; + crash_server = NULL; + } + } + // create the minidumper + const int kCustomInfoCount = 2; + const google_breakpad::CustomInfoEntry kCustomInfoEntries[] = { + google_breakpad::CustomInfoEntry(L"prod", strAppName.c_str()), + google_breakpad::CustomInfoEntry(L"ver", L"1.0"), + }; + google_breakpad::CustomClientInfo custom_info = {kCustomInfoEntries, kCustomInfoCount}; + handler = new google_breakpad::ExceptionHandler( + strDumpPath, + NULL, + NULL, + NULL, + google_breakpad::ExceptionHandler::HANDLER_ALL, + dumpType, + (crash_server ? strPipeName.c_str() : NULL), + &custom_info); + } + ~MiniDumper() { + delete handler; + delete crash_server; + } + google_breakpad::CrashGenerationServer* crash_server; + google_breakpad::ExceptionHandler* handler; +}; + +#endif diff --git a/libs/Common/Util.cpp b/libs/Common/Util.cpp new file mode 100644 index 0000000..1ae22cb --- /dev/null +++ b/libs/Common/Util.cpp @@ -0,0 +1,805 @@ +//////////////////////////////////////////////////////////////////// +// Util.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" +#include "Util.h" +#ifdef _MSC_VER +#include +#ifndef _USE_WINSDKOS +#define _USE_WINSDKOS +#include +#endif +#else +#include +#ifdef __APPLE__ +#include +#else +#include +#endif +#include +#endif + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +typedef struct CPUINFO_TYP { + bool bSSE; // Streaming SIMD Extensions + bool bSSE2; // Streaming SIMD Extensions 2 + bool bSSE3; // Streaming SIMD Extensions 3 + bool bSSE41; // Streaming SIMD Extensions 4.1 + bool bSSE42; // Streaming SIMD Extensions 4.2 + bool bAVX; // Advanced Vector Extensions + bool bFMA; // Fused Multiply�Add + bool b3DNOW; // 3DNow! (vendor independent) + bool b3DNOWEX; // 3DNow! (AMD specific extensions) + bool bMMX; // MMX support + bool bMMXEX; // MMX (AMD specific extensions) + bool bEXT; // extended features available + char vendor[13]; // vendor name + char name[49]; // CPU name +} CPUINFO; + + +// F U N C T I O N S /////////////////////////////////////////////// + +Flags InitCPU(); +CPUINFO GetCPUInfo(); +bool OSSupportsSSE(); +bool OSSupportsAVX(); + + +// G L O B A L S /////////////////////////////////////////////////// + +const Flags Util::ms_CPUFNC(InitCPU()); + +/** + * Lookup table (precomputed CRC64 values for each 8 bit string) computation + * takes into account the fact that the reverse polynom has zeros in lower 8 bits: + * + * @code + * for (i = 0; i < 256; i++) + * { + * shiftRegister = i; + * for (j = 0; j < 8; j++) + * { + * if (shiftRegister & 1) + * shiftRegister = (shiftRegister >> 1) ^ Reverse_polynom; + * else + * shiftRegister >>= 1; + * } + * CRCTable[i] = shiftRegister; + * } + * @endcode + * + * Generic code would look as follows: + * + * @code + * for (i = 0; i < 256; i++) + * { + * shiftRegister = 0; + * bitString = i; + * for (j = 0; j < 8; j++) + * { + * if ((shiftRegister ^ (bitString >> j)) & 1) + * shiftRegister = (shiftRegister >> 1) ^ Reverse_polynom; + * else + * shiftRegister >>= 1; + * } + * CRCTable[i] = shiftRegister; + * } + * @endcode + * + * @remark Since the lookup table elements have 0 in the lower 32 bit word, + * the 32 bit assembler implementation of CRC64Process can be optimized, + * avoiding at least one 'xor' operation. + */ +static const uint64_t gs_au64CRC64[256] = +{ + 0x0000000000000000ULL, 0x01B0000000000000ULL, 0x0360000000000000ULL, 0x02D0000000000000ULL, + 0x06C0000000000000ULL, 0x0770000000000000ULL, 0x05A0000000000000ULL, 0x0410000000000000ULL, + 0x0D80000000000000ULL, 0x0C30000000000000ULL, 0x0EE0000000000000ULL, 0x0F50000000000000ULL, + 0x0B40000000000000ULL, 0x0AF0000000000000ULL, 0x0820000000000000ULL, 0x0990000000000000ULL, + 0x1B00000000000000ULL, 0x1AB0000000000000ULL, 0x1860000000000000ULL, 0x19D0000000000000ULL, + 0x1DC0000000000000ULL, 0x1C70000000000000ULL, 0x1EA0000000000000ULL, 0x1F10000000000000ULL, + 0x1680000000000000ULL, 0x1730000000000000ULL, 0x15E0000000000000ULL, 0x1450000000000000ULL, + 0x1040000000000000ULL, 0x11F0000000000000ULL, 0x1320000000000000ULL, 0x1290000000000000ULL, + 0x3600000000000000ULL, 0x37B0000000000000ULL, 0x3560000000000000ULL, 0x34D0000000000000ULL, + 0x30C0000000000000ULL, 0x3170000000000000ULL, 0x33A0000000000000ULL, 0x3210000000000000ULL, + 0x3B80000000000000ULL, 0x3A30000000000000ULL, 0x38E0000000000000ULL, 0x3950000000000000ULL, + 0x3D40000000000000ULL, 0x3CF0000000000000ULL, 0x3E20000000000000ULL, 0x3F90000000000000ULL, + 0x2D00000000000000ULL, 0x2CB0000000000000ULL, 0x2E60000000000000ULL, 0x2FD0000000000000ULL, + 0x2BC0000000000000ULL, 0x2A70000000000000ULL, 0x28A0000000000000ULL, 0x2910000000000000ULL, + 0x2080000000000000ULL, 0x2130000000000000ULL, 0x23E0000000000000ULL, 0x2250000000000000ULL, + 0x2640000000000000ULL, 0x27F0000000000000ULL, 0x2520000000000000ULL, 0x2490000000000000ULL, + 0x6C00000000000000ULL, 0x6DB0000000000000ULL, 0x6F60000000000000ULL, 0x6ED0000000000000ULL, + 0x6AC0000000000000ULL, 0x6B70000000000000ULL, 0x69A0000000000000ULL, 0x6810000000000000ULL, + 0x6180000000000000ULL, 0x6030000000000000ULL, 0x62E0000000000000ULL, 0x6350000000000000ULL, + 0x6740000000000000ULL, 0x66F0000000000000ULL, 0x6420000000000000ULL, 0x6590000000000000ULL, + 0x7700000000000000ULL, 0x76B0000000000000ULL, 0x7460000000000000ULL, 0x75D0000000000000ULL, + 0x71C0000000000000ULL, 0x7070000000000000ULL, 0x72A0000000000000ULL, 0x7310000000000000ULL, + 0x7A80000000000000ULL, 0x7B30000000000000ULL, 0x79E0000000000000ULL, 0x7850000000000000ULL, + 0x7C40000000000000ULL, 0x7DF0000000000000ULL, 0x7F20000000000000ULL, 0x7E90000000000000ULL, + 0x5A00000000000000ULL, 0x5BB0000000000000ULL, 0x5960000000000000ULL, 0x58D0000000000000ULL, + 0x5CC0000000000000ULL, 0x5D70000000000000ULL, 0x5FA0000000000000ULL, 0x5E10000000000000ULL, + 0x5780000000000000ULL, 0x5630000000000000ULL, 0x54E0000000000000ULL, 0x5550000000000000ULL, + 0x5140000000000000ULL, 0x50F0000000000000ULL, 0x5220000000000000ULL, 0x5390000000000000ULL, + 0x4100000000000000ULL, 0x40B0000000000000ULL, 0x4260000000000000ULL, 0x43D0000000000000ULL, + 0x47C0000000000000ULL, 0x4670000000000000ULL, 0x44A0000000000000ULL, 0x4510000000000000ULL, + 0x4C80000000000000ULL, 0x4D30000000000000ULL, 0x4FE0000000000000ULL, 0x4E50000000000000ULL, + 0x4A40000000000000ULL, 0x4BF0000000000000ULL, 0x4920000000000000ULL, 0x4890000000000000ULL, + 0xD800000000000000ULL, 0xD9B0000000000000ULL, 0xDB60000000000000ULL, 0xDAD0000000000000ULL, + 0xDEC0000000000000ULL, 0xDF70000000000000ULL, 0xDDA0000000000000ULL, 0xDC10000000000000ULL, + 0xD580000000000000ULL, 0xD430000000000000ULL, 0xD6E0000000000000ULL, 0xD750000000000000ULL, + 0xD340000000000000ULL, 0xD2F0000000000000ULL, 0xD020000000000000ULL, 0xD190000000000000ULL, + 0xC300000000000000ULL, 0xC2B0000000000000ULL, 0xC060000000000000ULL, 0xC1D0000000000000ULL, + 0xC5C0000000000000ULL, 0xC470000000000000ULL, 0xC6A0000000000000ULL, 0xC710000000000000ULL, + 0xCE80000000000000ULL, 0xCF30000000000000ULL, 0xCDE0000000000000ULL, 0xCC50000000000000ULL, + 0xC840000000000000ULL, 0xC9F0000000000000ULL, 0xCB20000000000000ULL, 0xCA90000000000000ULL, + 0xEE00000000000000ULL, 0xEFB0000000000000ULL, 0xED60000000000000ULL, 0xECD0000000000000ULL, + 0xE8C0000000000000ULL, 0xE970000000000000ULL, 0xEBA0000000000000ULL, 0xEA10000000000000ULL, + 0xE380000000000000ULL, 0xE230000000000000ULL, 0xE0E0000000000000ULL, 0xE150000000000000ULL, + 0xE540000000000000ULL, 0xE4F0000000000000ULL, 0xE620000000000000ULL, 0xE790000000000000ULL, + 0xF500000000000000ULL, 0xF4B0000000000000ULL, 0xF660000000000000ULL, 0xF7D0000000000000ULL, + 0xF3C0000000000000ULL, 0xF270000000000000ULL, 0xF0A0000000000000ULL, 0xF110000000000000ULL, + 0xF880000000000000ULL, 0xF930000000000000ULL, 0xFBE0000000000000ULL, 0xFA50000000000000ULL, + 0xFE40000000000000ULL, 0xFFF0000000000000ULL, 0xFD20000000000000ULL, 0xFC90000000000000ULL, + 0xB400000000000000ULL, 0xB5B0000000000000ULL, 0xB760000000000000ULL, 0xB6D0000000000000ULL, + 0xB2C0000000000000ULL, 0xB370000000000000ULL, 0xB1A0000000000000ULL, 0xB010000000000000ULL, + 0xB980000000000000ULL, 0xB830000000000000ULL, 0xBAE0000000000000ULL, 0xBB50000000000000ULL, + 0xBF40000000000000ULL, 0xBEF0000000000000ULL, 0xBC20000000000000ULL, 0xBD90000000000000ULL, + 0xAF00000000000000ULL, 0xAEB0000000000000ULL, 0xAC60000000000000ULL, 0xADD0000000000000ULL, + 0xA9C0000000000000ULL, 0xA870000000000000ULL, 0xAAA0000000000000ULL, 0xAB10000000000000ULL, + 0xA280000000000000ULL, 0xA330000000000000ULL, 0xA1E0000000000000ULL, 0xA050000000000000ULL, + 0xA440000000000000ULL, 0xA5F0000000000000ULL, 0xA720000000000000ULL, 0xA690000000000000ULL, + 0x8200000000000000ULL, 0x83B0000000000000ULL, 0x8160000000000000ULL, 0x80D0000000000000ULL, + 0x84C0000000000000ULL, 0x8570000000000000ULL, 0x87A0000000000000ULL, 0x8610000000000000ULL, + 0x8F80000000000000ULL, 0x8E30000000000000ULL, 0x8CE0000000000000ULL, 0x8D50000000000000ULL, + 0x8940000000000000ULL, 0x88F0000000000000ULL, 0x8A20000000000000ULL, 0x8B90000000000000ULL, + 0x9900000000000000ULL, 0x98B0000000000000ULL, 0x9A60000000000000ULL, 0x9BD0000000000000ULL, + 0x9FC0000000000000ULL, 0x9E70000000000000ULL, 0x9CA0000000000000ULL, 0x9D10000000000000ULL, + 0x9480000000000000ULL, 0x9530000000000000ULL, 0x97E0000000000000ULL, 0x9650000000000000ULL, + 0x9240000000000000ULL, 0x93F0000000000000ULL, 0x9120000000000000ULL, 0x9090000000000000ULL +}; + + +// F U N C T I O N S /////////////////////////////////////////////// + +String Util::getHomeFolder() +{ + #ifdef _MSC_VER + TCHAR homedir[MAX_PATH]; + if (SHGetSpecialFolderPath(0, homedir, CSIDL_PROFILE, TRUE) != TRUE) + return String(); + #else + const char *homedir; + if ((homedir = getenv("HOME")) == NULL) + homedir = getpwuid(getuid())->pw_dir; + #endif // _MSC_VER + String dir(String(homedir) + PATH_SEPARATOR); + return ensureUnifySlash(dir); +} + +String Util::getApplicationFolder() +{ + #ifdef _MSC_VER + TCHAR appdir[MAX_PATH]; + if (SHGetSpecialFolderPath(0, appdir, CSIDL_APPDATA, TRUE) != TRUE) + return String(); + String dir(String(appdir) + PATH_SEPARATOR); + #else + const char *homedir; + if ((homedir = getenv("HOME")) == NULL) + homedir = getpwuid(getuid())->pw_dir; + String dir(String(homedir) + PATH_SEPARATOR + String(_T(".config")) + PATH_SEPARATOR); + #endif // _MSC_VER + return ensureUnifySlash(dir); +} + +String Util::getCurrentFolder() +{ + TCHAR pathname[MAX_PATH+1]; + #ifdef _MSC_VER + if (!GetCurrentDirectory(MAX_PATH, pathname)) + #else // _MSC_VER + if (!getcwd(pathname, MAX_PATH)) + #endif // _MSC_VER + return String(); + String dir(String(pathname) + PATH_SEPARATOR); + return ensureUnifySlash(dir); +} +/*----------------------------------------------------------------*/ + + +uint64_t Util::CRC64(const void *pv, size_t cb) +{ + const uint8_t* pu8 = (const uint8_t *)pv; + uint64_t uCRC64 = 0ULL; + while (cb--) + uCRC64 = gs_au64CRC64[(uCRC64 ^ *pu8++) & 0xff] ^ (uCRC64 >> 8); + return uCRC64; +} + +uint64_t Util::CRC64Process(uint64_t uCRC64, const void *pv, size_t cb) +{ + const uint8_t *pu8 = (const uint8_t *)pv; + while (cb--) + uCRC64 = gs_au64CRC64[(uCRC64 ^ *pu8++) & 0xff] ^ (uCRC64 >> 8); + return uCRC64; +} +/*----------------------------------------------------------------*/ + + +String Util::GetCPUInfo() +{ + const CPUINFO info(::GetCPUInfo()); + String cpu(info.name[0] == 0 ? info.vendor : info.name); + #if 0 + if (info.bFMA) + cpu += _T(" FMA"); + else if (info.bAVX) + cpu += _T(" AVX"); + else if (info.bSSE42) + cpu += _T(" SSE4.2"); + else if (info.bSSE41) + cpu += _T(" SSE4.1"); + else if (info.bSSE3) + cpu += _T(" SSE3"); + else if (info.bSSE2) + cpu += _T(" SSE2"); + else if (info.bSSE) + cpu += _T(" SSE"); + if (info.b3DNOWEX) + cpu += _T(" 3DNOWEX"); + else if (info.b3DNOW) + cpu += _T(" 3DNOW"); + #endif + return cpu; +} +/*----------------------------------------------------------------*/ + +String Util::GetRAMInfo() +{ + #if defined(_MSC_VER) + + #ifdef _WIN64 + MEMORYSTATUSEX memoryStatus; + memset(&memoryStatus, sizeof(MEMORYSTATUSEX), 0); + memoryStatus.dwLength = sizeof(memoryStatus); + ::GlobalMemoryStatusEx(&memoryStatus); + const size_t nTotalPhys((size_t)memoryStatus.ullTotalPhys); + const size_t nTotalVirtual((size_t)memoryStatus.ullTotalVirtual); + #else + MEMORYSTATUS memoryStatus; + memset(&memoryStatus, sizeof(MEMORYSTATUS), 0); + memoryStatus.dwLength = sizeof(MEMORYSTATUS); + ::GlobalMemoryStatus(&memoryStatus); + const size_t nTotalPhys((size_t)memoryStatus.dwTotalPhys); + const size_t nTotalVirtual((size_t)memoryStatus.dwTotalVirtual); + #endif + + #elif defined(__APPLE__) + + int mib[2] = {CTL_HW, HW_MEMSIZE}; + const unsigned namelen = sizeof(mib) / sizeof(mib[0]); + size_t len = sizeof(size_t); + size_t nTotalPhys; + sysctl(mib, namelen, &nTotalPhys, &len, NULL, 0); + const size_t nTotalVirtual(nTotalPhys); + + #else // __GNUC__ + + struct sysinfo info; + sysinfo(&info); + const size_t nTotalPhys((size_t)info.totalram); + const size_t nTotalVirtual((size_t)info.totalswap); + + #endif // _MSC_VER + return formatBytes(nTotalPhys) + _T(" Physical Memory ") + formatBytes(nTotalVirtual) + _T(" Virtual Memory"); +} +/*----------------------------------------------------------------*/ + +String Util::GetOSInfo() +{ + #ifdef _MSC_VER + + String os; + #ifdef _USE_WINSDKOS + #ifndef _WIN32_WINNT_WIN10 + #define _WIN32_WINNT_WIN10 0x0A00 + if (IsWindowsVersionOrGreater(HIBYTE(_WIN32_WINNT_WIN10), LOBYTE(_WIN32_WINNT_WIN10), 0)) + #else + if (IsWindows10OrGreater()) + #endif + os = _T("Windows 10+"); + else if (IsWindows8Point1OrGreater()) + os = _T("Windows 8.1"); + else if (IsWindows8OrGreater()) + os = _T("Windows 8"); + else if (IsWindows7SP1OrGreater()) + os = _T("Windows 7 (SP1)"); + else if (IsWindows7OrGreater()) + os = _T("Windows 7"); + else if (IsWindowsVistaSP2OrGreater()) + os = _T("Windows Vista (SP2)"); + else if (IsWindowsVistaSP1OrGreater()) + os = _T("Windows Vista (SP1)"); + else if (IsWindowsVistaOrGreater()) + os = _T("Windows Vista"); + else if (IsWindowsXPSP3OrGreater()) + os = _T("Windows XP (SP3)"); + else if (IsWindowsXPSP2OrGreater()) + os = _T("Windows XP (SP2)"); + else if (IsWindowsXPSP1OrGreater()) + os = _T("Windows XP (SP1)"); + else if (IsWindowsXPOrGreater()) + os = _T("Windows XP"); + else + os = _T("Windows (unknown version)"); + #else + OSVERSIONINFOEX ver; + memset(&ver, 0, sizeof(OSVERSIONINFOEX)); + ver.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); + + if (!GetVersionEx((OSVERSIONINFO*)&ver)) { + ver.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); + if (!GetVersionEx((OSVERSIONINFO*)&ver)) { + return "Windows (unknown version)"; + } + } + + if (ver.dwPlatformId != VER_PLATFORM_WIN32_NT) { + os = "Win9x/ME"; + } else { + switch (ver.dwMajorVersion) + { + case 4: + os = "WinNT4"; + break; + + case 5: + switch (ver.dwMinorVersion) + { + case 0: os = "Win2000"; break; + case 1: os = "WinXP"; break; + case 2: os = "Win2003"; break; + default:os = "Unknown WinNT5"; + } + break; + + case 6: + switch (ver.dwMinorVersion) + { + case 0: os = (ver.wProductType == VER_NT_WORKSTATION ? "WinVista" : "Win2008"); break; + case 1: os = (ver.wProductType == VER_NT_WORKSTATION ? "Win7" : "Win2008R2"); break; + case 2: os = (ver.wProductType == VER_NT_WORKSTATION ? "Win8" : "Win2012"); break; + case 3: os = (ver.wProductType == VER_NT_WORKSTATION ? "Win8.1" : "Win2012R2"); break; + case 4: os = "Win10"; break; + default:os = "Unknown WinNT6"; + } + break; + + default: + os = "Windows (version unknown)"; + } + if (ver.wProductType & VER_NT_WORKSTATION) + os += " Pro"; + else if (ver.wProductType & VER_NT_SERVER) + os += " Server"; + else if (ver.wProductType & VER_NT_DOMAIN_CONTROLLER) + os += " DC"; + } + + if (ver.wServicePackMajor != 0) { + os += " (SP"; + os += String::ToString(ver.wServicePackMajor); + if (ver.wServicePackMinor != 0) { + os += '.'; + os += String::ToString(ver.wServicePackMinor); + } + os += ")"; + } + #endif + + #ifdef _WIN64 + os += " x64"; + #else + typedef BOOL (WINAPI *LPFN_ISWOW64PROCESS) (HANDLE, PBOOL); + const LPFN_ISWOW64PROCESS fnIsWow64Process = (LPFN_ISWOW64PROCESS)GetProcAddress(GetModuleHandle("kernel32"),"IsWow64Process"); + BOOL bIsWow64 = FALSE; + if (fnIsWow64Process && fnIsWow64Process(GetCurrentProcess(),&bIsWow64) && bIsWow64) + os += " x64"; + #endif + + return os; + + #else // _MSC_VER + + utsname n; + if (uname(&n) != 0) + return "linux (unknown version)"; + return String(n.sysname) + " " + String(n.release) + " (" + String(n.machine) + ")"; + + #endif // _MSC_VER +} +/*----------------------------------------------------------------*/ + +String Util::GetDiskInfo(const String& path) +{ + #if defined(_SUPPORT_CPP17) && (!defined(__GNUC__) || (__GNUC__ > 7)) + + const std::filesystem::space_info si = std::filesystem::space(path.c_str()); + return String::FormatString("%s (%s) space", formatBytes(si.available).c_str(), formatBytes(si.capacity).c_str()); + + #else + + return String(); + + #endif // _SUPPORT_CPP17 +} +/*----------------------------------------------------------------*/ + + +// Initialize various global variables (ex: random-number-generator state). +void Util::Init() +{ + #ifdef _RELEASE + const time_t t(Util::getTime()); + std::srand((unsigned)t); + #if CV_MAJOR_VERSION > 3 || (CV_MAJOR_VERSION == 3 && CV_MINOR_VERSION >= 4) + cv::setRNGSeed((int)t); + #endif + #else + std::srand((unsigned)0); + #if CV_MAJOR_VERSION > 3 || (CV_MAJOR_VERSION == 3 && CV_MINOR_VERSION >= 4) + cv::setRNGSeed((int)0); + #endif + #endif +} +/*----------------------------------------------------------------*/ + + +/** + * Set global variable for availability of SSE instructions. + */ +Flags InitCPU() +{ + const CPUINFO info(GetCPUInfo()); + Flags cpufncs(0); + if (info.bSSE) { + #if defined(_MSC_VER) && !defined(_WIN64) + _set_SSE2_enable(1); + #endif + if (OSSupportsSSE()) + cpufncs.set(Util::SSE); + } + if (info.bAVX && OSSupportsAVX()) + cpufncs.set(Util::AVX); + return (cpufncs); +} +/*----------------------------------------------------------------*/ + + +#if _PLATFORM_X86 +#ifdef _MSC_VER +#include +inline void CPUID(int CPUInfo[4], int level) { + __cpuid(CPUInfo, level); +} +#else +#include +inline void CPUID(int CPUInfo[4], int level) { + unsigned* p((unsigned*)CPUInfo); + __get_cpuid((unsigned&)level, p+0, p+1, p+2, p+3); +} +#endif +#else // _PLATFORM_X86 +inline void CPUID(int CPUInfo[4], int level) { + memset(CPUInfo, 0, sizeof(int)*4); +} +#endif // _PLATFORM_X86 + +/** + * Function to detect SSE availability in CPU. + */ +CPUINFO GetCPUInfo() +{ + CPUINFO info; + + // set all values to 0 (false) + memset(&info, 0, sizeof(CPUINFO)); + + int CPUInfo[4]; + + // CPUID with an InfoType argument of 0 returns the number of + // valid Ids in CPUInfo[0] and the CPU identification string in + // the other three array elements. The CPU identification string is + // not in linear order. The code below arranges the information + // in a human readable form. + CPUID(CPUInfo, 0); + *((int*)info.vendor) = CPUInfo[1]; + *((int*)(info.vendor+4)) = CPUInfo[3]; + *((int*)(info.vendor+8)) = CPUInfo[2]; + + // Interpret CPU feature information. + CPUID(CPUInfo, 1); + info.bMMX = (CPUInfo[3] & 0x800000) != 0; // test bit 23 for MMX + info.bSSE = (CPUInfo[3] & 0x2000000) != 0; // test bit 25 for SSE + info.bSSE2 = (CPUInfo[3] & 0x4000000) != 0; // test bit 26 for SSE2 + info.bSSE3 = (CPUInfo[2] & 0x1) != 0; // test bit 0 for SSE3 + info.bSSE41 = (CPUInfo[2] & 0x80000) != 0; // test bit 19 for SSE4.1 + info.bSSE42 = (CPUInfo[2] & 0x100000) != 0; // test bit 20 for SSE4.2 + info.bAVX = (CPUInfo[2] & 0x18000000) == 0x18000000; // test bits 28,27 for AVX + info.bFMA = (CPUInfo[2] & 0x18001000) == 0x18001000; // test bits 28,27,12 for FMA + + // EAX=0x80000000 => CPUID returns extended features + CPUID(CPUInfo, 0x80000000); + const unsigned nExIds = CPUInfo[0]; + info.bEXT = (nExIds >= 0x80000000); + + // must be greater than 0x80000004 to support CPU name + if (nExIds > 0x80000004) { + size_t idx(0); + CPUID(CPUInfo, 0x80000002); // CPUID returns CPU name part1 + while (((uint8_t*)CPUInfo)[idx] == ' ') + ++idx; + memcpy(info.name, (uint8_t*)CPUInfo + idx, sizeof(CPUInfo) - idx); + idx = sizeof(CPUInfo) - idx; + + CPUID(CPUInfo, 0x80000003); // CPUID returns CPU name part2 + memcpy(info.name+idx, CPUInfo, sizeof(CPUInfo)); + idx += 16; + + CPUID(CPUInfo, 0x80000004); // CPUID returns CPU name part3 + memcpy(info.name+idx, CPUInfo, sizeof(CPUInfo)); + } + + if ((strncmp(info.vendor, "AuthenticAMD", 12)==0) && info.bEXT) { // AMD + CPUID(CPUInfo, 0x80000001); // CPUID will copy ext. feat. bits to EDX and cpu type to EAX + info.b3DNOWEX = (CPUInfo[3] & 0x40000000) != 0; // indicates AMD extended 3DNow+! + info.bMMXEX = (CPUInfo[3] & 0x400000) != 0; // indicates AMD extended MMX + } + + return info; +} +/*----------------------------------------------------------------*/ + +#if _PLATFORM_X86 +#ifdef _MSC_VER +// Function to detect SSE availability in operating system. +bool OSSupportsSSE() +{ + #ifndef _WIN64 + // try SSE instruction and look for crash + __try + { + _asm xorps xmm0, xmm0 + } + __except(EXCEPTION_EXECUTE_HANDLER) { + if (_exception_code() == STATUS_ILLEGAL_INSTRUCTION) + return false; // sse not supported by os + return false; // unknown exception occurred + } + #endif // _WIN64 + + return true; +} +// Function to detect AVX availability in operating system. +bool OSSupportsAVX() +{ + #ifndef _WIN64 + // try AVX instruction + unsigned flag; + _asm { + mov ecx, 0; //specify 0 for XFEATURE_ENABLED_MASK register + XGETBV; //result in EDX:EAX + and eax, 06H; + cmp eax, 06H; // check OS has enabled both XMM and YMM state support + jne not_supported + mov eax, 1; // mark as supported + jmp done + not_supported: + mov eax, 0; // mark as not supported + done: + mov esi, flag + mov [esi], eax + } + return flag != 0; + #else + // check if the OS will save the YMM registers + unsigned long long xcrFeatureMask(_xgetbv(_XCR_XFEATURE_ENABLED_MASK)); + return (xcrFeatureMask & 0x6) == 0x6; + #endif // _WIN64 +} +/*----------------------------------------------------------------*/ + +#else // _MSC_VER + +// Function to detect SSE availability in operating system. +bool OSSupportsSSE() +{ + // try SSE instruction and look for crash + try { + asm("xorps %xmm0, %xmm0"); + } + catch(int e) { + return false; // unknown exception occurred + } + return true; +} +// Function to detect AVX availability in operating system. +bool OSSupportsAVX() +{ + // check if the OS will save the YMM registers + unsigned int index(0); //specify 0 for XFEATURE_ENABLED_MASK register + unsigned int eax, edx; + __asm__ __volatile__("xgetbv" : "=a"(eax), "=d"(edx) : "c"(index)); + unsigned long long xcrFeatureMask(((unsigned long long)edx << 32) | eax); + return (xcrFeatureMask & 0x6) == 0x6; +} +/*----------------------------------------------------------------*/ +#endif // _MSC_VER + +#else // _PLATFORM_X86 + +// Function to detect SSE availability in operating system. +bool OSSupportsSSE() +{ + return false; +} +// Function to detect AVX availability in operating system. +bool OSSupportsAVX() +{ + return false; +} +/*----------------------------------------------------------------*/ +#endif // _PLATFORM_X86 + + +// print details about the current build and PC +void Util::LogBuild() +{ + LOG(_T("OpenMVS %s v%u.%u.%u"), + #ifdef _ENVIRONMENT64 + _T("x64"), + #else + _T("x32"), + #endif + OpenMVS_MAJOR_VERSION, OpenMVS_MINOR_VERSION, OpenMVS_PATCH_VERSION); + #if TD_VERBOSE == TD_VERBOSE_OFF + LOG(_T("Build date: ") __DATE__); + #else + LOG(_T("Build date: ") __DATE__ _T(", ") __TIME__); + #endif + LOG(_T("CPU: %s (%u cores)"), Util::GetCPUInfo().c_str(), Thread::hardwareConcurrency()); + LOG((_T("RAM: ") + Util::GetRAMInfo()).c_str()); + LOG((_T("OS: ") + Util::GetOSInfo()).c_str()); + #ifdef _SUPPORT_CPP17 + LOG((_T("Disk: ") + Util::GetDiskInfo(WORKING_FOLDER_FULL)).c_str()); + #endif + if (!SIMD_ENABLED.isSet(Util::SSE)) LOG(_T("warning: no SSE compatible CPU or OS detected")); + else if (!SIMD_ENABLED.isSet(Util::AVX)) LOG(_T("warning: no AVX compatible CPU or OS detected")); + else LOG(_T("SSE & AVX compatible CPU & OS detected")); +} + +// print information about the memory usage +#if _PLATFORM_X86 +#ifdef _MSC_VER +#include +#pragma comment(lib, "Psapi.lib") +void Util::LogMemoryInfo() +{ + PROCESS_MEMORY_COUNTERS pmc; + if (!GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) + return; + LOG(_T("MEMORYINFO: {")); + LOG(_T("\tPageFaultCount %d"), pmc.PageFaultCount); + LOG(_T("\tPeakWorkingSetSize %s"), SEACAVE::Util::formatBytes(pmc.PeakWorkingSetSize).c_str()); + LOG(_T("\tWorkingSetSize %s"), SEACAVE::Util::formatBytes(pmc.WorkingSetSize).c_str()); + LOG(_T("\tQuotaPeakPagedPoolUsage %s"), SEACAVE::Util::formatBytes(pmc.QuotaPeakPagedPoolUsage).c_str()); + LOG(_T("\tQuotaPagedPoolUsage %s"), SEACAVE::Util::formatBytes(pmc.QuotaPagedPoolUsage).c_str()); + LOG(_T("\tQuotaPeakNonPagedPoolUsage %s"), SEACAVE::Util::formatBytes(pmc.QuotaPeakNonPagedPoolUsage).c_str()); + LOG(_T("\tQuotaNonPagedPoolUsage %s"), SEACAVE::Util::formatBytes(pmc.QuotaNonPagedPoolUsage).c_str()); + LOG(_T("\tPagefileUsage %s"), SEACAVE::Util::formatBytes(pmc.PagefileUsage).c_str()); + LOG(_T("\tPeakPagefileUsage %s"), SEACAVE::Util::formatBytes(pmc.PeakPagefileUsage).c_str()); + LOG(_T("} ENDINFO")); +} +#else // _MSC_VER +void Util::LogMemoryInfo() +{ + std::ifstream proc("/proc/self/status"); + if (!proc.is_open()) + return; + String s; + LOG(_T("MEMORYINFO: {")); + while (std::getline(proc, s), !proc.fail()) { + if (s.substr(0, 6) == "VmPeak" || s.substr(0, 6) == "VmSize") + LOG(_T("\t%s"), s.c_str()); + } + LOG(_T("} ENDINFO")); +} +#endif // _MSC_VER +#else // _PLATFORM_X86 +void Util::LogMemoryInfo() +{ +} +#endif // _PLATFORM_X86 + + +// Parses a ASCII command line string and returns an array of pointers to the command line arguments, +// along with a count of such arguments, in a way that is similar to the standard C run-time +// argv and argc values. +LPSTR* Util::CommandLineToArgvA(LPCSTR CmdLine, size_t& _argc) +{ + bool in_QM(false); + bool in_TEXT(false); + bool in_SPACE(true); + + size_t argc(0); + size_t len = strlen(CmdLine); + size_t i = ((len+2)/2)*sizeof(void*) + sizeof(void*); + LPSTR* argv = (LPSTR*)(new uint8_t[i + (len+2)*sizeof(CHAR)]); + LPSTR _argv = (LPSTR)(((CHAR*)argv)+i); + argv[argc] = _argv; + size_t j(0); i = 0; + + CHAR a; + while ((a = CmdLine[i]) != 0) { + if (in_QM) { + if (a == '\"') { + in_QM = false; + } else { + _argv[j] = a; + j++; + } + } else { + switch (a) { + case '\"': + in_QM = true; + in_TEXT = true; + if (in_SPACE) { + argv[argc] = _argv+j; + argc++; + } + in_SPACE = false; + break; + case ' ': + case '\t': + case '\n': + case '\r': + if (in_TEXT) { + _argv[j] = '\0'; + j++; + } + in_TEXT = false; + in_SPACE = true; + break; + default: + in_TEXT = true; + if (in_SPACE) { + argv[argc] = _argv+j; + argc++; + } + _argv[j] = a; + j++; + in_SPACE = false; + break; + } + } + i++; + } + _argv[j] = '\0'; + argv[argc] = NULL; + + _argc = argc; + return argv; +} +/*----------------------------------------------------------------*/ diff --git a/libs/Common/Util.h b/libs/Common/Util.h new file mode 100644 index 0000000..fd313f9 --- /dev/null +++ b/libs/Common/Util.h @@ -0,0 +1,835 @@ +//////////////////////////////////////////////////////////////////// +// Util.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_UTIL_H__ +#define __SEACAVE_UTIL_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#ifdef _MSC_VER +#include +#else +#include +#include +#endif + + +// D E F I N E S /////////////////////////////////////////////////// + +#define PATH_SEPARATOR _T('/') +#define PATH_SEPARATOR_STR _T("/") +#define REVERSE_PATH_SEPARATOR _T('\\') + +#ifdef _MSC_VER +#define LINE_SEPARATOR_STR _T("\r\n") +#define LINE_SEPARATOR_LEN 2 +#else +#define LINE_SEPARATOR_STR _T("\n") +#define LINE_SEPARATOR_LEN 1 +#endif + +#ifdef _MSC_VER +#define SETTINGS_PATH _T("%APPDATA%") +#else +#define SETTINGS_PATH _T("~/.config") +#endif + +#define ensureUnifySlashWin ensureUnifyReverseSlash +#define ensureUnifySlashUnix ensureUnifySlash + +#define GET_TICK() Util::getTick() +#define GET_TIME() Util::getTime() +#define SIMD_ENABLED Util::ms_CPUFNC + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +// Manage setting/removing bit flags +template +class TFlags +{ +public: + typedef TYPE Type; + +public: + inline TFlags() : flags(0) { } + inline TFlags(const TFlags& rhs) : flags(rhs.flags) { } + inline TFlags(Type f) : flags(f) { } + inline bool isSet(Type aFlag) const { return (flags & aFlag) == aFlag; } + inline bool isSet(Type aFlag, Type nF) const { return (flags & (aFlag|nF)) == aFlag; } + inline bool isSetExclusive(Type aFlag) const { return flags == aFlag; } + inline bool isAnySet(Type aFlag) const { return (flags & aFlag) != 0; } + inline bool isAnySet(Type aFlag, Type nF) const { const Type m(flags & (aFlag|nF)); return m != 0 && (m & nF) == 0; } + inline bool isAnySetExclusive(Type aFlag) const { return (flags & aFlag) != 0 && (flags & ~aFlag) == 0; } + inline void set(Type aFlag, bool bSet) { if (bSet) set(aFlag); else unset(aFlag); } + inline void set(Type aFlag) { flags |= aFlag; } + inline void unset(Type aFlag) { flags &= ~aFlag; } + inline void flip(Type aFlag) { flags ^= aFlag; } + inline void operator=(TFlags rhs) { flags = rhs.flags; } + inline operator Type() const { return flags; } + inline operator Type&() { return flags; } +protected: + Type flags; + #ifdef _USE_BOOST + // implement BOOST serialization + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int /*version*/) { + ar & flags; + } + #endif +}; +typedef class GENERAL_API TFlags Flags; +/*----------------------------------------------------------------*/ + + +// A histogram class, that computes the distribution function (df) +// of unique sended value or iterable data inside the provided range. +// The Histogram object can keep a tally of values within a range, +// the range is arranged into some number of bins specified during +// construction. +// Jansson Consulting +// 2009-06-30, updated 2011-06-17 and 2011-08-03 +// 2011-12-17 Modified by Pierre Moulon +// - use vector array to avoid memory management +// - add value by sequence with iterator +// 2015-04-04 Modified by cDc +// - rewrite +// - add GetApproximatePermille() +// Dedicated to the public domain. +template +class THistogram +{ +public: + typedef TYPE Type; + +public: + // Construct a histogram that can count within a range of values. + // All bins of the histogram are set to zero. + THistogram(const std::pair& range, size_t bins=10) : + Start(range.first), + End(range.second), + BinInterval(Type(bins)/(End-Start)), + Freq(bins, 0), + Overflow(0), + Underflow(0) {} + + // Construct a histogram from a sequence of data + template + void Add(DataInputIterator begin, DataInputIterator end) { + for (DataInputIterator iter = begin; iter != end; ++iter) + Add(static_cast(*iter)); + } + // Increase the count for the bin that holds a value that is in range + // for this histogram or the under-/overflow count if it is not in range + void Add(const Type& x) { + if (x < Start) { + ++Underflow; + } else { + const size_t i(static_cast((x-Start)*BinInterval)); + if (i < Freq.size()) ++Freq[i]; + else ++Overflow; + } + } + + // Get the sum of all counts in the histogram + inline size_t GetTotalCount() const { return std::accumulate(Freq.begin(), Freq.end(), 0); } + // Get the overflow count + inline size_t GetOverflow() const { return Overflow; } + // Get the underflow count + inline size_t GetUnderflow() const { return Underflow; } + // Get frequencies + inline const std::vector& GetHist() const { return Freq; } + // Get XbinsValue + std::vector GetXbinsValue() const { + const size_t NBins(Freq.size()); + std::vector vec_XbinValue(NBins); + const Type val((End-Start)/static_cast(NBins-1)); + for (size_t i = 0; i < NBins; ++i) + vec_XbinValue[i] = (val*static_cast(i) + Start); + return vec_XbinValue; + } + // Get start + inline Type GetStart() const { return Start; } + // Get End + inline Type GetEnd() const { return End; } + + // Returns the approximate permille + Type GetApproximatePermille(float permille) const { + ASSERT(permille >= 0.f && permille <= 1.f); + size_t NumValues(0); + for (size_t n: Freq) + NumValues += n; + size_t Num(0); + Type UpperBound(Start); + for (size_t i = 0; i < Freq.size(); ++i) { + if (static_cast(Num)/NumValues > permille) + return UpperBound; + Num += Freq[i]; + UpperBound = (static_cast(i)*End)/(Freq.size()-1)+Start; + } + return End; + } + + // Text display of the histogram + std::string ToString(const std::string& sTitle = "") const { + std::ostringstream os; + os.precision(3); + os << sTitle << "\n"; + const size_t n(Freq.size()); + for (size_t i = 0; i < n; ++i) + os << static_cast(End-Start)/n*static_cast(i) << "\t|\t" << Freq[i] << "\n"; + os << End << "\n"; + return os.str(); + } + +protected: + const Type Start, End, BinInterval; // min/max/step of values + std::vector Freq; // histogram + size_t Overflow, Underflow; // count under/over flow +}; +typedef class GENERAL_API THistogram Histogram32F; +typedef class GENERAL_API THistogram Histogram64F; +/*----------------------------------------------------------------*/ + + +class GENERAL_API Util +{ +public: + static String getAppName() { + #ifdef _MSC_VER + String buf(MAX_PATH+1, '\0'); + GetModuleFileName(NULL, &buf.front(), MAX_PATH); + return ensureUnifySlash(buf); + #else // _MSC_VER + LPTSTR home = getenv("HOME"); + if (home == NULL) + return String(); + String name(String(home) + "/app"); + return ensureUnifySlash(name); + #endif // _MSC_VER + } + + // generate a unique name based on process ID and time + static String getUniqueName(TCHAR dash='-') + { + TCHAR szDate[256]; + #ifdef _MSC_VER + SYSTEMTIME st; + GetLocalTime(&st); + LPTSTR szTime = szDate+ + GetDateFormat(LOCALE_USER_DEFAULT,0,&st,_T("yy''MM''dd"),szDate,80); + GetTimeFormat(LOCALE_USER_DEFAULT,0,&st,_T("HH''mm''ss"),szTime,80); + #else // _MSC_VER + const time_t t = time(NULL); + const struct tm *tmp = localtime(&t); + LPTSTR szTime = szDate+1+ + strftime(szDate, 80, "%y%m%d", tmp); + strftime(szTime, 80, "%H%M%S", tmp); + #endif // _MSC_VER + const uint32_t ID((uint32_t(__PROCESS__) + RAND())&0x00FFFFFF); + if (dash) + return String::FormatString("%s%c%s%c%06X", szDate, dash, szTime, dash, ID); + return String::FormatString("%s%s%06X", szDate, szTime, ID); + } + + static String translateError(int aError) { + #ifdef _MSC_VER + LPVOID lpMsgBuf; + FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + aError, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language + (LPTSTR) &lpMsgBuf, + 0, + NULL + ); + String tmp((LPCTSTR)lpMsgBuf); + // Free the buffer. + LocalFree(lpMsgBuf); + String::size_type i; + + while ((i = tmp.find_last_of(LINE_SEPARATOR_STR)) != String::npos) + tmp.erase(i, LINE_SEPARATOR_LEN); + return tmp; + #else // _MSC_VER + return strerror(aError); + #endif // _MSC_VER + } + + static String& trimUnifySlash(String& path) + { + String::size_type start = 1; + while ((start = path.find(PATH_SEPARATOR, start)) != String::npos) + if (path[start-1] == PATH_SEPARATOR) + path.erase(start, 1); + else + ++start; + return path; + } + static String& ensureUnifySlash(String& path) + { + String::size_type start = 0; + while ((start = path.find(REVERSE_PATH_SEPARATOR, start)) != String::npos) + path[start] = PATH_SEPARATOR; + return trimUnifySlash(path); + } + static String& ensureUnifyReverseSlash(String& path) + { + String::size_type start = 0; + while ((start = path.find(PATH_SEPARATOR, start)) != String::npos) + path[start] = REVERSE_PATH_SEPARATOR; + return path; + } + + static String& ensureFolderSlash(String& path) + { + if (path.empty()) + return path; + String::size_type nEnd = path.size()-1; + if (path[nEnd] != PATH_SEPARATOR) + path += PATH_SEPARATOR; + return path; + } + + static void ensureFolder(const String& path) + { + String::size_type start = 0; + while ((start = path.find(PATH_SEPARATOR, start)) != String::npos) + #ifdef _MSC_VER + CreateDirectory(path.substr(0, ++start).c_str(), NULL); + #else + mkdir(path.substr(0, ++start).c_str(), 0755); + #endif + } + + static String& ensureValidPath(String& path) { + return simplifyPath(ensureUnifySlash(strTrim(path, _T("\"")))); + } + static String& ensureValidFolderPath(String& path) { + return simplifyPath(ensureFolderSlash(ensureUnifySlash(strTrim(path, _T("\""))))); + } + + static inline bool isFullPath(LPCTSTR path) { + // returns true if local drive full path or network path + return (path && ( + #ifdef _MSC_VER + (path[1]==_T(':') && path[0]!=_T('\0')) || + #else // _MSC_VER + path[0]==_T('/') || + #endif // _MSC_VER + #ifdef UNICODE + *reinterpret_cast(path)==0x5C005C00/*"\\\\"*/)); + #else + *reinterpret_cast(path)==0x5C5C/*"\\\\"*/)); + #endif // UNICODE + } + static String getFullPath(const String& str) { + if (isFullPath(str)) + return str; + return getCurrentFolder()+str; + } + + static inline bool isParentFolder(LPCTSTR path, int off=0) { + // returns true if the folder starting at the given position in path is the parent folder ".." + if (off < 0 || path[off] != _T('.')) + return false; + if (off > 0 && path[off-1] != PATH_SEPARATOR) + return false; + if (path[off+1] != _T('.')) + return false; + return path[off+2] == _T('\0') || path[off+2] == PATH_SEPARATOR; + } + + static String getHomeFolder(); + static String getApplicationFolder(); + static String getCurrentFolder(); + static String getProcessFolder() { + return getFilePath(getAppName()); + } + + static String ensureUnitPath(const String& path) + { + if (path.find(_T(" ")) == String::npos) + return path; + return String(_T("\"")+path+_T("\"")); + } + + static String& simplifyPath(String& path) { + // compress path by removing all "./" and "folder/../" occurrences + // (if path only, it should end in path-separator) + { + // removes all "./" occurrences + String::size_type i(0); + while ((i = path.find(_T(".") PATH_SEPARATOR_STR, i)) != String::npos) { + if (i > 0 && path[i-1] != PATH_SEPARATOR) + i += 2; + else + path.erase(i, 2); + }} + { + // removes all "folder/../" occurrences + String::size_type i(0); + while ((i = path.find(_T("..") PATH_SEPARATOR_STR, i)) != String::npos) { + if (i > 1 && path[i-1] == PATH_SEPARATOR) { + String::size_type prev = path.rfind(PATH_SEPARATOR, i-2); + if (prev == String::npos) prev = 0; else ++prev; + if (!isParentFolder(path, (int)prev)) { + path.erase(prev, i+3-prev); + i = prev; + continue; + } + } + i += 3; + }} + return path; + } + static String getSimplifiedPath(String path) { + return simplifyPath(path); + } + + static String getRelativePath(const String& currentPath, const String& targetPath) { + // returns the path to the target relative to the current path; + // both current and target paths must be full paths; + // current path is assumed to be a folder, while target path can be also a file + CLISTDEF2(String) currentPathValues, targetPathValues; + Util::strSplit(currentPath, PATH_SEPARATOR, currentPathValues); + if (currentPathValues.back().empty()) + currentPathValues.pop_back(); + Util::strSplit(targetPath, PATH_SEPARATOR, targetPathValues); + size_t idxCurrentPath(0), idxTargetPath(0); + while ( + idxCurrentPath < currentPathValues.size() && + idxTargetPath < targetPathValues.size() && + #ifdef _MSC_VER + _tcsicmp(currentPathValues[idxCurrentPath], targetPathValues[idxTargetPath]) == 0 + #else + _tcscmp(currentPathValues[idxCurrentPath], targetPathValues[idxTargetPath]) == 0 + #endif + ) + ++idxCurrentPath, ++idxTargetPath; + if (idxCurrentPath == 0) + return targetPath; + String relativePath; + relativePath.reserve(targetPath.size()); + while (idxCurrentPath < currentPathValues.size()) { + relativePath += _T("..") PATH_SEPARATOR_STR; + ++idxCurrentPath; + } + const size_t idxFirstTarget(idxTargetPath); + while (idxTargetPath < targetPathValues.size()) { + if (idxTargetPath > idxFirstTarget) + relativePath += PATH_SEPARATOR; + relativePath += targetPathValues[idxTargetPath++]; + } + return relativePath; + } + + static String& getCommonPath(String& commonPath, const String& path) { + // returns the path shared by the given paths + #ifdef _MSC_VER + while (_tcsnicmp(commonPath, path, commonPath.length()) != 0) { + #else + while (_tcsncmp(commonPath, path, commonPath.length()) != 0) { + #endif + commonPath.pop_back(); + commonPath = getFilePath(commonPath); + } + return commonPath; + } + static String getCommonPath(const String* arrPaths, size_t numPaths) { + // returns the path shared by all given paths + ASSERT(numPaths > 0); + String commonPath(arrPaths[0]); + for (size_t i=1; !commonPath.empty() && i 3 && nrNumbers > 0) + return buf; + rez = (uint32_t)((sTime%((int64_t)24*3600*1000)) / (3600*1000)); + if (rez) { + ++nrNumbers; + len += _sntprintf(buf+len, 128, "%uh", rez); + } + if (nAproximate > 2 && nrNumbers > 0) + return buf; + rez = (uint32_t)((sTime%((int64_t)3600*1000)) / (60*1000)); + if (rez) { + ++nrNumbers; + len += _sntprintf(buf+len, 128, "%um", rez); + } + if (nAproximate > 1 && nrNumbers > 0) + return buf; + rez = (uint32_t)((sTime%((int64_t)60*1000)) / (1*1000)); + if (rez) { + ++nrNumbers; + len += _sntprintf(buf+len, 128, "%us", rez); + } + if (nAproximate > 0 && nrNumbers > 0) + return buf; + rez = (uint32_t)(sTime%((int64_t)1*1000)); + if (rez || !nrNumbers) + len += _sntprintf(buf+len, 128, "%ums", rez); + + return String(buf, len); + } + + static String toString(const wchar_t* wsz) { + if (wsz == NULL) + return String(); + #if 1 + return std::wstring_convert, wchar_t>().to_bytes(wsz); + #elif 1 + std::mbstate_t state = std::mbstate_t(); + const size_t len(std::wcsrtombs(NULL, &wsz, 0, &state)); + if (len == static_cast(-1)) + return String(); + std::vector mbstr(len+1); + if (std::wcsrtombs(&mbstr[0], &wsz, mbstr.size(), &state) == static_cast(-1)) + return String(); + return String(&mbstr[0]); + #else + const std::wstring ws(wsz); + const std::locale locale(""); + typedef std::codecvt converter_type; + const converter_type& converter = std::use_facet(locale); + std::vector to(ws.length() * converter.max_length()); + std::mbstate_t state; + const wchar_t* from_next; + char* to_next; + if (converter.out(state, ws.data(), ws.data() + ws.length(), from_next, &to[0], &to[0] + to.size(), to_next) != converter_type::ok) + return String(); + return std::string(&to[0], to_next); + #endif + } + + static int64_t toInt64(LPCTSTR aString) { + #ifdef _MSC_VER + return _atoi64(aString); + #else + return atoll(aString); + #endif + } + static int toInt(LPCTSTR aString) { + return atoi(aString); + } + static uint32_t toUInt32Hex(LPCTSTR aString) { + uint32_t val; + sscanf(aString, "%x", &val); + return val; + } + static uint32_t toUInt32(LPCTSTR aString) { + return (uint32_t)atoi(aString); + } + static double toDouble(LPCTSTR aString) { + return atof(aString); + } + static float toFloat(LPCTSTR aString) { + return (float)atof(aString); + } + + static time_t getTime() { + return (time_t)time(NULL); + } + + static uint32_t getTick() { + #ifdef _MSC_VER + return GetTickCount(); + #else + timeval tv; + gettimeofday(&tv, NULL); + return (uint32_t)(tv.tv_sec * 1000 ) + (tv.tv_usec / 1000); + #endif + } + + + /** + * IPRT - CRC64. + * + * The method to compute the CRC64 is referred to as CRC-64-ISO: + * http://en.wikipedia.org/wiki/Cyclic_redundancy_check + * The generator polynomial is x^64 + x^4 + x^3 + x + 1. + * Reverse polynom: 0xd800000000000000ULL + * + * As in: http://www.virtualbox.org/svn/vbox/trunk/src/VBox/Runtime/common/checksum/crc64.cpp + */ + + /** + * Calculate CRC64 for a memory block. + * + * @returns CRC64 for the memory block. + * @param pv Pointer to the memory block. + * @param cb Size of the memory block in bytes. + */ + static uint64_t CRC64(const void *pv, size_t cb); + + /** + * Start a multiblock CRC64 calculation. + * + * @returns Start CRC64. + */ + static uint64_t CRC64Start() { + return 0ULL; + } + /** + * Processes a multiblock of a CRC64 calculation. + * + * @returns Intermediate CRC64 value. + * @param uCRC64 Current CRC64 intermediate value. + * @param pv The data block to process. + * @param cb The size of the data block in bytes. + */ + static uint64_t CRC64Process(uint64_t uCRC64, const void *pv, size_t cb); + /** + * Complete a multiblock CRC64 calculation. + * + * @returns CRC64 value. + * @param uCRC64 Current CRC64 intermediate value. + */ + static uint64_t CRC64Finish(uint64_t uCRC64) { + return uCRC64; + } + + + static void Init(); + + static String GetCPUInfo(); + static String GetRAMInfo(); + static String GetOSInfo(); + static String GetDiskInfo(const String&); + enum CPUFNC {NA=0, SSE, AVX}; + static const Flags ms_CPUFNC; + + static void LogBuild(); + static void LogMemoryInfo(); + + static LPSTR* CommandLineToArgvA(LPCSTR CmdLine, size_t& _argc); + static String CommandLineToString(size_t argc, LPCTSTR* argv) { + String strCmdLine; + for (size_t i=1; i msgLen) + std::cout << String(lastMsgLen-msgLen, _T(' ')); + std::cout << std::flush; + lastMsgLen = msgLen; + } + }; +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_UTIL_H__ diff --git a/libs/Common/Util.inl b/libs/Common/Util.inl new file mode 100644 index 0000000..b0ffa0f --- /dev/null +++ b/libs/Common/Util.inl @@ -0,0 +1,1289 @@ +//////////////////////////////////////////////////////////////////// +// Util.inl +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// I N L I N E ///////////////////////////////////////////////////// + +// normalize inhomogeneous 2D point by the given camera intrinsics K +// K is assumed to be the [3,3] triangular matrix with: fx, fy, s, cx, cy and scale 1 +template +inline void NormalizeProjection(const TYPE1* K, const TYPE2* x, TYPE3* n) { + ASSERT(ISZERO(K[3]) && ISZERO(K[6]) && ISZERO(K[7]) && ISEQUAL(K[8], TYPE1(1))); + n[0] = TYPE3(K[0]*x[0] + K[1]*x[1] + K[2]); + n[1] = TYPE3( K[4]*x[1] + K[5]); +} // NormalizeProjection +// same as above, but using K inverse +template +inline void NormalizeProjectionInv(const TYPE1* K, const TYPE2* x, TYPE3* n) { + ASSERT(ISZERO(K[3]) && ISZERO(K[6]) && ISZERO(K[7]) && ISEQUAL(K[8], TYPE1(1))); + n[0] = TYPE3((K[4]*x[0] - K[1]*x[1] + (K[5]*K[1]-K[2]*K[4])) / (K[0]*K[4])); + n[1] = TYPE3((x[1] - K[5]) / K[4]); +} // NormalizeProjectionInv +/*----------------------------------------------------------------*/ + +// compute relative pose of the given two cameras +template +inline void ComputeRelativeRotation(const TMatrix& Ri, const TMatrix& Rj, TMatrix& Rij) { + Rij = Rj * Ri.t(); +} // ComputeRelativeRotation +template +inline void ComputeRelativePose(const TMatrix& Ri, const TPoint3& Ci, const TMatrix& Rj, const TPoint3& Cj, TMatrix& Rij, TPoint3& Cij) { + Rij = Rj * Ri.t(); + Cij = Ri * (Cj - Ci); +} // ComputeRelativePose +/*----------------------------------------------------------------*/ + +// Triangulate the position of a 3D point +// given two corresponding normalized projections and the pose of the second camera relative to the first one; +// returns the triangulated 3D point from the point of view of the first camera +template +bool TriangulatePoint3D( + const TMatrix& R, const TPoint3& C, + const TPoint2& pt1, const TPoint2& pt2, + TPoint3& X +) { + // convert image points to 3-vectors (of unit length) + // used to describe landmark observations/bearings in camera frames + const TPoint3 f1(pt1.x,pt1.y,1); + const TPoint3 f2(pt2.x,pt2.y,1); + const TPoint3 f2_unrotated = R.t() * f2; + const TPoint2 b(C.dot(f1), C.dot(f2_unrotated)); + // optimized inversion of A + const TYPE1 a = normSq(f1); + const TYPE1 c = f1.dot(f2_unrotated); + const TYPE1 d = -normSq(f2_unrotated); + const TYPE1 det = a*d+c*c; + if (ABS(det) < EPSILONTOLERANCE()) + return false; + const TYPE1 invDet = TYPE1(1)/det; + const TPoint2 lambda((d*b.x+c*b.y)*invDet, (a*b.y-c*b.x)*invDet); + const TPoint3 xm = lambda.x * f1; + const TPoint3 xn = C + lambda.y * f2_unrotated; + X = (xm + xn)*TYPE1(0.5); + return true; +} // TriangulatePoint3D +// same as above, but using the two camera poses; +// returns the 3D point in world coordinates +template +bool TriangulatePoint3D( + const TMatrix& K1, const TMatrix& K2, + const TMatrix& R1, const TMatrix& R2, + const TPoint3& C1, const TPoint3& C2, + const TPoint2& x1, const TPoint2& x2, + TPoint3& X +) { + TPoint2 pt1, pt2; + NormalizeProjectionInv(K1.val, x1.ptr(), pt1.ptr()); + NormalizeProjectionInv(K2.val, x2.ptr(), pt2.ptr()); + TMatrix R; TPoint3 C; + ComputeRelativePose(R1, C1, R2, C2, R, C); + if (!TriangulatePoint3D(R, C, pt1, pt2, X)) + return false; + X = R1.t() * X + C1; + return true; +} // TriangulatePoint3D +/*----------------------------------------------------------------*/ + +// compute the homography matrix transforming points from image A to image B, +// given the relative pose of B with respect to A, and the plane +// (see R. Hartley, "Multiple View Geometry," 2004, pp. 234) +template +TMatrix HomographyMatrixComposition(const TMatrix& R, const TPoint3& C, const TMatrix& n, const TYPE& d) { + const TMatrix t(R*(-C)); + return TMatrix(R - t*(n.t()*INVERT(d))); +} +template +TMatrix HomographyMatrixComposition(const TMatrix& R, const TPoint3& C, const TMatrix& n, const TMatrix& X) { + return HomographyMatrixComposition(R, C, n, -n.dot(X)); +} +template +TMatrix HomographyMatrixComposition(const TMatrix& Ri, const TPoint3& Ci, const TMatrix& Rj, const TPoint3& Cj, const TMatrix& n, const TYPE& d) { + #if 0 + Matrix3x3 R; Point3 C; + ComputeRelativePose(Ri, Ci, Rj, Cj, R, C); + return HomographyMatrixComposition(R, C, n, d); + #else + const TMatrix t((Ci - Cj)*INVERT(d)); + return TMatrix(Rj * (Ri.t() - t*n.t())); + #endif +} +template +TMatrix HomographyMatrixComposition(const TMatrix& Ri, const TPoint3& Ci, const TMatrix& Rj, const TPoint3& Cj, const TMatrix& n, const TMatrix& X) { + #if 0 + Matrix3x3 R; Point3 C; + ComputeRelativePose(Ri, Ci, Rj, Cj, R, C); + return HomographyMatrixComposition(R, C, n, X); + #else + const TMatrix t((Ci - Cj)*INVERT(n.dot(X))); + return TMatrix(Rj * (Ri.t() + t*n.t())); + #endif +} +/*----------------------------------------------------------------*/ + +// transform essential matrix to fundamental matrix +template +inline TMatrix TransformE2F(const TMatrix& E, const TMatrix& K1, const TMatrix& K2) { + return K2.inv().t() * E * K1.inv(); +} // TransformE2F +// transform fundamental matrix to essential matrix +template +inline TMatrix TransformF2E(const TMatrix& F, const TMatrix& K1, const TMatrix& K2) { + return K2.t() * F * K1; +} // TransformF2E +/*----------------------------------------------------------------*/ + +// Creates a skew symmetric cross product matrix from the provided tuple. +// compose the cross-product matrix from the given vector: axb=[a]xb +template +inline TMatrix CreateCrossProductMatrix3(const TMatrix& x) { + TMatrix X; + X(0, 0) = TYPE(0); X(1, 0) = -x(2); X(2, 0) = x(1); + X(0, 1) = x(2); X(1, 1) = TYPE(0); X(2, 1) = -x(0); + X(0, 2) = -x(1); X(1, 2) = x(0); X(2, 2) = TYPE(0); + return X; +} // CreateCrossProductMatrix3 +// same as above, but transposed: axb=[b]xTa +template +inline TMatrix CreateCrossProductMatrix3T(const TMatrix& x) { + TMatrix X; + X(0, 0) = TYPE(0); X(1, 0) = x(2); X(2, 0) = -x(1); + X(0, 1) = -x(2); X(1, 1) = TYPE(0); X(2, 1) = x(0); + X(0, 2) = x(1); X(1, 2) = -x(0); X(2, 2) = TYPE(0); + return X; +} // CreateCrossProductMatrix3T +/*----------------------------------------------------------------*/ + +// compose essential matrix from the given rotation and direction +template +inline TMatrix CreateE(const TMatrix& R, const TMatrix& C) { + const TMatrix t = -(R * C); + return CreateCrossProductMatrix3(t) * R; +} // CreateE +// compose fundamental matrix from the given rotation, direction and camera matrix +template +inline TMatrix CreateF(const TMatrix& R, const TMatrix& C, const TMatrix& K1, const TMatrix& K2) { + const TMatrix E = CreateE(R, C); + return TransformE2F(E, K1, K2); +} // CreateF +/*----------------------------------------------------------------*/ + + +// computes an RQ decomposition of 3x3 matrices as in: +// "Computing euler angles from a rotation matrix", Gregory G Slabaugh, 1999 +template +inline void RQDecomp3x3(Eigen::Matrix M, Eigen::Matrix& R, Eigen::Matrix& Q) { + // find Givens rotation for x axis: + // | 1 0 0 | + // Qx = | 0 c s |, c = m33/sqrt(m32^2 + m33^2), s = m32/sqrt(m32^2 + m33^2) + // | 0 -s c | + Eigen::Matrix cs = Eigen::Matrix(M(2,2), M(2,1)).normalized(); + Eigen::Matrix Qx{ {1, 0, 0}, {0, cs.x(), cs.y()}, {0, -cs.y(), cs.x()} }; + R.noalias() = M * Qx; + ASSERT(std::abs(R(2,1)) < FLT_EPSILON); + R(2,1) = 0; + // find Givens rotation for y axis: + // | c 0 -s | + // Qy = | 0 1 0 |, c = m33/sqrt(m31^2 + m33^2), s = -m31/sqrt(m31^2 + m33^2) + // | s 0 c | + cs = Eigen::Matrix(R(2,2), -R(2,0)).normalized(); + Eigen::Matrix Qy{ {cs.x(), 0, -cs.y()}, {0, 1, 0}, {cs.y(), 0, cs.x()} }; + M.noalias() = R * Qy; + ASSERT(std::abs(M(2,0)) < FLT_EPSILON); + M(2,0) = 0; + // find Givens rotation for z axis: + // | c s 0 | + // Qz = |-s c 0 |, c = m22/sqrt(m21^2 + m22^2), s = m21/sqrt(m21^2 + m22^2) + // | 0 0 1 | + cs = Eigen::Matrix(M(1,1), M(1,0)).normalized(); + Eigen::Matrix Qz{ {cs.x(), cs.y(), 0}, {-cs.y(), cs.x(), 0}, {0, 0, 1} }; + R.noalias() = M * Qz; + ASSERT(std::abs(R(1,0)) < FLT_EPSILON); + R(1,0) = 0; + // solve the decomposition ambiguity: + // - diagonal entries of R, except the last one, shall be positive + // - rotate R by 180 degree if necessary + if (R(0,0) < 0) { + if (R(1,1) < 0) { + // rotate around z for 180 degree: + // |-1, 0, 0| + // | 0, -1, 0| + // | 0, 0, 1| + R(0,0) *= -1; + R(0,1) *= -1; + R(1,1) *= -1; + Qz(0,0) *= -1; + Qz(0,1) *= -1; + Qz(1,0) *= -1; + Qz(1,1) *= -1; + } else { + // rotate around y for 180 degree: + // |-1, 0, 0| + // | 0, 1, 0| + // | 0, 0, -1| + R(0,0) *= -1; + R(0,2) *= -1; + R(1,2) *= -1; + R(2,2) *= -1; + Qz.transposeInPlace(); + Qy(0,0) *= -1; + Qy(0,2) *= -1; + Qy(2,0) *= -1; + Qy(2,2) *= -1; + } + } else if (R(1,1) < 0) { + // rotate around x for 180 degree: + // | 1, 0, 0| + // | 0, -1, 0| + // | 0, 0, -1| + R(0,1) *= -1; + R(0,2) *= -1; + R(1,1) *= -1; + R(1,2) *= -1; + R(2,2) *= -1; + Qz.transposeInPlace(); + Qy.transposeInPlace(); + Qx(1,1) *= -1; + Qx(1,2) *= -1; + Qx(2,1) *= -1; + Qx(2,2) *= -1; + } + // calculate orthogonal matrix + Q.noalias() = Qz.transpose() * Qy.transpose() * Qx.transpose(); +} +template +inline void RQDecomp3x3(const TMatrix& M, TMatrix& R, TMatrix& Q) { + const Eigen::Matrix _M((typename TMatrix::CEMatMap)M); + Eigen::Matrix _R, _Q; + RQDecomp3x3(_M, _R, _Q); + R = _R; Q = _Q; +} // RQDecomp3x3 +/*----------------------------------------------------------------*/ + + +// compute the angle between the two rotations given +// as in: "Disambiguating Visual Relations Using Loop Constraints", 2010 +// returns cos(angle) (same as cos(ComputeAngleSO3(I)) +template +inline TYPE ComputeAngle(const TMatrix& I) { + return CLAMP(((TYPE)cv::trace(I)-TYPE(1))*TYPE(0.5), TYPE(-1), TYPE(1)); +} // ComputeAngle +template +FORCEINLINE TYPE ComputeAngle(const TMatrix& R1, const TMatrix& R2) { + return ComputeAngle(TMatrix(R1*R2.t())); +} // ComputeAngle +/*----------------------------------------------------------------*/ + + +// compute the Frobenius (or Euclidean) norm of the distance between the two matrices +template +inline TYPE FrobeniusNorm(const TMatrix& M) { + return SQRT(((typename TMatrix::EMatMap)M).cwiseAbs2().sum()); +} // FrobeniusNorm +template +FORCEINLINE TYPE FrobeniusNorm(const TMatrix& M1, const TMatrix& M2) { + return FrobeniusNorm(TMatrix(M1-M2)); +} // FrobeniusNorm +/*----------------------------------------------------------------*/ + + +// check if any three of the given 2D points are on the same line +template +bool CheckCollinearity(const TPoint2* ptr, int count, bool checkPartialSubsets=false) { + for (int i = (checkPartialSubsets?count-1:2); i < count; ++i) { + // check that the i-th selected point does not belong + // to a line connecting some previously selected points + for (int j = 1; j < i; ++j) { + const TPoint2 d1 = ptr[j] - ptr[i]; + for (int k = 0; k < j; ++k) { + const TPoint2 d2 = ptr[k] - ptr[i]; + if (ABS(d2.x*d1.y - d2.y*d1.x) <= FLT_EPSILON*(ABS(d1.x) + ABS(d1.y) + ABS(d2.x) + ABS(d2.y))) + return true; + } + } + } + return false; +} +// check if any three of the given 3D points are on the same line +template +bool CheckCollinearity(const TPoint3* ptr, int count, bool checkPartialSubsets=false) { + for (int i = (checkPartialSubsets?count-1:2); i < count; ++i) { + // check that the i-th selected point does not belong + // to a line connecting some previously selected points + for (int j = 1; j < i; ++j) { + const TPoint3 d1 = ptr[j] - ptr[i]; + for (int k = 0; k < j; ++k) { + const TPoint3 d2 = ptr[k] - ptr[i]; + if (normSq(d1.cross(d2)) < 1.e-16) + return true; + } + } + } + return false; +} +/*----------------------------------------------------------------*/ + + +// compute the corresponding ray for a given projection matrix P[3,4] and image point pt[2,1] +// output ray[3,1] +template +inline void RayPoint_3x4_2_3(const TYPE1* P, const TYPE2* pt, TYPE1* ray) { + Eigen::Map< const Eigen::Matrix > mP(P); + const Eigen::Matrix mM(mP.template topLeftCorner<3,3>()); + TYPE1 M[9]; + InvertMatrix3x3(mM.data(), M); + ray[0] = M[0*3+0]*pt[0] + M[0*3+1]*pt[1] + M[0*3+2]; + ray[1] = M[1*3+0]*pt[0] + M[1*3+1]*pt[1] + M[1*3+2]; + ray[2] = M[2*3+0]*pt[0] + M[2*3+1]*pt[1] + M[2*3+2]; +} // RayPoint_3x4_2_3 +/*----------------------------------------------------------------*/ + +// project column vertex - used only for visualization purposes +// (optimized ProjectVertex for P[3,4] and X[3,1], output pt[3,1]) +template +inline void ProjectVertex_3x4_3_3(const TYPE1* P, const TYPE1* X, TYPE2* pt) { + pt[0] = (TYPE2)(P[0*4+0]*X[0] + P[0*4+1]*X[1] + P[0*4+2]*X[2] + P[0*4+3]); + pt[1] = (TYPE2)(P[1*4+0]*X[0] + P[1*4+1]*X[1] + P[1*4+2]*X[2] + P[1*4+3]); + pt[2] = (TYPE2)(P[2*4+0]*X[0] + P[2*4+1]*X[1] + P[2*4+2]*X[2] + P[2*4+3]); +} // ProjectVertex_3x4_3_3 +// (optimized ProjectVertex for P[3,4] and X[4,1], output pt[3,1]) +template +inline void ProjectVertex_3x4_4_3(const TYPE1* P, const TYPE1* X, TYPE2* pt) { + pt[0] = (TYPE2)(P[0*4+0]*X[0] + P[0*4+1]*X[1] + P[0*4+2]*X[2] + P[0*4+3]*X[3]); + pt[1] = (TYPE2)(P[1*4+0]*X[0] + P[1*4+1]*X[1] + P[1*4+2]*X[2] + P[1*4+3]*X[3]); + pt[2] = (TYPE2)(P[2*4+0]*X[0] + P[2*4+1]*X[1] + P[2*4+2]*X[2] + P[2*4+3]*X[3]); +} // ProjectVertex_3x4_4_3 +// (optimized ProjectVertex for R[3,3], C[3,1] and X[3,1], output pt[2,1]) +template +inline void ProjectVertex_3x3_3_3_2(const TYPE1* R, const TYPE1* C, const TYPE1* X, TYPE2* pt) { + const TYPE1 T[3] = {X[0]-C[0], X[1]-C[1], X[2]-C[2]}; + const TYPE1 invW(INVERT(R[2*3+0]*T[0] + R[2*3+1]*T[1] + R[2*3+2]*T[2])); + pt[0] = (TYPE2)((R[0*3+0]*T[0] + R[0*3+1]*T[1] + R[0*3+2]*T[2]) * invW); + pt[1] = (TYPE2)((R[1*3+0]*T[0] + R[1*3+1]*T[1] + R[1*3+2]*T[2]) * invW); +} // ProjectVertex_3x3_3_3_2 +// (optimized ProjectVertex for R[3,3], C[3,1] and X[3,1], output pt[3,1]) +template +inline void ProjectVertex_3x3_3_3_3(const TYPE1* R, const TYPE1* C, const TYPE1* X, TYPE2* pt) { + const TYPE1 T[3] = {X[0]-C[0], X[1]-C[1], X[2]-C[2]}; + pt[0] = (TYPE2)(R[0*3+0]*T[0] + R[0*3+1]*T[1] + R[0*3+2]*T[2]); + pt[1] = (TYPE2)(R[1*3+0]*T[0] + R[1*3+1]*T[1] + R[1*3+2]*T[2]); + pt[2] = (TYPE2)(R[2*3+0]*T[0] + R[2*3+1]*T[1] + R[2*3+2]*T[2]); +} // ProjectVertex_3x3_3_3_3 +// (optimized ProjectVertex for H[3,3] and X[2,1], output pt[3,1]) +template +inline void ProjectVertex_3x3_2_3(const TYPE1* H, const TYPE2* X, TYPE3* pt) { + pt[0] = (TYPE3)(H[0*3+0]*X[0] + H[0*3+1]*X[1] + H[0*3+2]); + pt[1] = (TYPE3)(H[1*3+0]*X[0] + H[1*3+1]*X[1] + H[1*3+2]); + pt[2] = (TYPE3)(H[2*3+0]*X[0] + H[2*3+1]*X[1] + H[2*3+2]); +} // ProjectVertex_3x3_2_3 +// (optimized ProjectVertex for H[3,3] and X[2,1], output pt[2,1]) +template +inline void ProjectVertex_3x3_2_2(const TYPE1* H, const TYPE2* X, TYPE3* pt) { + const TYPE1 invZ(INVERT(H[2*3+0]*X[0] + H[2*3+1]*X[1] + H[2*3+2])); + pt[0] = (TYPE3)((H[0*3+0]*X[0] + H[0*3+1]*X[1] + H[0*3+2])*invZ); + pt[1] = (TYPE3)((H[1*3+0]*X[0] + H[1*3+1]*X[1] + H[1*3+2])*invZ); +} // ProjectVertex_3x3_2_2 +/*----------------------------------------------------------------*/ + +// project column vertex - used only for visualization purposes +// (optimized ProjectVertex for P[3,4] and X[3,1], output pt[2,1]) +template +inline void ProjectVertex_3x4_3_2(const TYPE1* P, const TYPE1* X, TYPE2* pt) { + const TYPE1 invW(INVERT(P[2*4+0]*X[0] + P[2*4+1]*X[1] + P[2*4+2]*X[2] + P[2*4+3])); + pt[0] = (TYPE2)((P[0*4+0]*X[0] + P[0*4+1]*X[1] + P[0*4+2]*X[2] + P[0*4+3]) * invW); + pt[1] = (TYPE2)((P[1*4+0]*X[0] + P[1*4+1]*X[1] + P[1*4+2]*X[2] + P[1*4+3]) * invW); +} // ProjectVertex_3x4_3_2 +// (optimized ProjectVertex for P[3,4] and X[4,1], output pt[2,1]) +template +inline void ProjectVertex_3x4_4_2(const TYPE1* P, const TYPE1* X, TYPE2* pt) { + const TYPE1 invW(INVERT(P[2*4+0]*X[0] + P[2*4+1]*X[1] + P[2*4+2]*X[2] + P[2*4+3]*X[3])); + pt[0] = (TYPE2)((P[0*4+0]*X[0] + P[0*4+1]*X[1] + P[0*4+2]*X[2] + P[0*4+3]*X[3]) * invW); + pt[1] = (TYPE2)((P[1*4+0]*X[0] + P[1*4+1]*X[1] + P[1*4+2]*X[2] + P[1*4+3]*X[3]) * invW); +} // ProjectVertex_3x4_4_2 +/*----------------------------------------------------------------*/ + +// project column vertex using the transpose of the given projective matrix +// (optimized ProjectVertex for P[3,4].t() and X[3,1], output pt[4,1]) +template +inline void ProjectVertex_3x4t_3_4(const TYPE1* P, const TYPE2* X, TYPE3* pt) { + pt[0] = (TYPE3)(P[0*4+0]*X[0] + P[1*4+0]*X[1] + P[2*4+0]*X[2]); + pt[1] = (TYPE3)(P[0*4+1]*X[0] + P[1*4+1]*X[1] + P[2*4+1]*X[2]); + pt[2] = (TYPE3)(P[0*4+2]*X[0] + P[1*4+2]*X[1] + P[2*4+2]*X[2]); + pt[3] = (TYPE3)(P[0*4+3]*X[0] + P[1*4+3]*X[1] + P[2*4+3]*X[2]); +} // ProjectVertex_3x4t_3_4 +// (optimized ProjectVertex for P[3,4].t() and X[3,1], output pt[3,1] - the first 3 coordinates) +template +inline void ProjectVertex_3x4t_3_3(const TYPE1* P, const TYPE2* X, TYPE3* pt) { + pt[0] = (TYPE3)(P[0*4+0]*X[0] + P[1*4+0]*X[1] + P[2*4+0]*X[2]); + pt[1] = (TYPE3)(P[0*4+1]*X[0] + P[1*4+1]*X[1] + P[2*4+1]*X[2]); + pt[2] = (TYPE3)(P[0*4+2]*X[0] + P[1*4+2]*X[1] + P[2*4+2]*X[2]); +} // ProjectVertex_3x4t_3_3 +/*----------------------------------------------------------------*/ + + +// given the camera matrix, compute the error between the given 2D point and the reprojected 3D point; +// note that here we're using "dirty" projection method which is suitable only for visualization; on the upside, it shouldn't matter because +// if a point is projected as infinite, there's something wrong with it anyway; +// (optimized ComputeReprojectionError for P[3,4] and X[3,1], output pt[2,1]) +template +inline TYPE2 ComputeReprojectionError_3x4_3_2(const TYPE1* P, const TYPE1* X, const TYPE2* pt) { + TYPE2 reprojection[2]; + ProjectVertex_3x4_3_2(P, X, reprojection); + return SQUARE(reprojection[0]-pt[0])+SQUARE(reprojection[1]-pt[1]); +} // ComputeReprojectionError_3x4_3_2 +// (optimized ComputeReprojectionError for P[3,4] and X[4,1], output pt[2,1]) +template +inline TYPE2 ComputeReprojectionError_3x4_4_2(const TYPE1* P, const TYPE1* X, const TYPE2* pt) { + TYPE2 reprojection[2]; + ProjectVertex_3x4_4_2(P, X, reprojection); + return SQUARE(reprojection[0]-pt[0])+SQUARE(reprojection[1]-pt[1]); +} // ComputeReprojectionError_3x4_4_2 +// (optimized ComputeReprojectionError for R[3,3], C[3,1] and X[3,1], output pt[2,1]) +template +inline TYPE2 ComputeReprojectionError_3x3_3_3_2(const TYPE1* R, const TYPE1* C, const TYPE1* X, const TYPE2* pt) { + TYPE2 reprojection[2]; + ProjectVertex_3x3_3_3_2(R, C, X, reprojection); + return SQUARE(reprojection[0]-pt[0])+SQUARE(reprojection[1]-pt[1]); +} // ComputeReprojectionError_3x3_3_3_2 +/*----------------------------------------------------------------*/ + + +// computes the depth of the given point +// from the point of view of the given camera pose +template +inline TYPE PointDepth(const TYPE* R, const TYPE* C, const TYPE* X) { + return R[2*3+0]*X[0] + R[2*3+1]*X[1] + R[2*3+2]*X[2] + C[2]; +} // PointDepth +template +inline TYPE PointDepth(const TYPE* P, const TYPE* X) { + return P[2*4+0]*X[0] + P[2*4+1]*X[1] + P[2*4+2]*X[2] + P[2*4+3]; +} // PointDepth +/*----------------------------------------------------------------*/ + + +// reproject the given 3D point with the given view; +// return squared reprojection error +// or FLT_MAX if the point is behind the camera +// reproj return the homogeneous reprojection +template +inline TYPE2 ProjectPoint(const TYPE1* P, const TYPE1* X, const TYPE2* proj, TYPE2* reproj) { + // reproject the 3D point on this view + ProjectVertex_3x4_3_3(P, X, reproj); + // filter out points behind + if (reproj[2] <= TYPE2(0)) + return FLT_MAX; + // return the reprojection error + return SQUARE(reproj[0]/reproj[2]-proj[0])+SQUARE(reproj[1]/reproj[2]-proj[1]); +} // ProjectPoint +template +inline TYPE2 ProjectPoint(const TYPE1* R, const TYPE1* C, const TYPE1* X, const TYPE2* proj, TYPE2* reproj) { + // reproject the 3D point on this view + ProjectVertex_3x3_3_3_3(R, C, X, reproj); + // filter out points behind + if (reproj[2] <= TYPE2(0)) + return FLT_MAX; + // return the reprojection error + return SQUARE(reproj[0]/reproj[2]-proj[0])+SQUARE(reproj[1]/reproj[2]-proj[1]); +} // ProjectPoint +/*----------------------------------------------------------------*/ + +// reproject the given 3D point with the given view; +// return true if the point is not behind the camera +// and the reprojection error is small enough; +// returns false otherwise +// reproj returns the homogeneous reprojection (first 3) and errSq (last 1) +template +inline bool IsPointInlier(const TYPE1* P, const TYPE1* X, const TYPE2* proj, TYPE2* reproj, TYPE2 thresholdSq) { + // reproject the 3D point on this view + ProjectVertex_3x4_3_3(P, X, reproj); + // filter out points behind the camera or + // having the reprojection error bigger than the given threshold + return (reproj[2] > TYPE2(0) && + (reproj[3]=(SQUARE(reproj[0]/reproj[2]-proj[0])+SQUARE(reproj[1]/reproj[2]-proj[1]))) <= thresholdSq); +} // IsPointInlier +template +inline bool IsPointInlier(const TYPE1* R, const TYPE1* C, const TYPE1* X, const TYPE2* proj, TYPE2* reproj, TYPE2 thresholdSq) { + // reproject the 3D point on this view + ProjectVertex_3x3_3_3_3(R, C, X, reproj); + // filter out points behind the camera or + // having the reprojection error bigger than the given threshold + return (reproj[2] > TYPE2(0) && + (reproj[3]=(SQUARE(reproj[0]/reproj[2]-proj[0])+SQUARE(reproj[1]/reproj[2]-proj[1]))) <= thresholdSq); +} // IsPointInlier +/*----------------------------------------------------------------*/ + + +// given a rotation matrix, return the angle in radians corresponding to the axis/angle decomposition +template +inline TYPE AngleFromRotationMatrix(const TYPE* R) { + const TYPE a = (R[0*3+0]+R[1*3+1]+R[2*3+2]-TYPE(1))/TYPE(2); + if (a < TYPE(-1)) + return acos(TYPE(-1)); + if (a > TYPE(1)) + return acos(TYPE(1)); + return acos(a); +} +template +FORCEINLINE TYPE AngleFromRotationMatrix(const TMatrix& R) { + return AngleFromRotationMatrix(R.val); +} +/*----------------------------------------------------------------*/ + +// given two 3D vectors, +// compute the angle between them +// returns cos(angle) +template +inline TYPE2 ComputeAngle(const TYPE1* V1, const TYPE1* V2) { + return CLAMP(TYPE2((V1[0]*V2[0]+V1[1]*V2[1]+V1[2]*V2[2])/SQRT((V1[0]*V1[0]+V1[1]*V1[1]+V1[2]*V1[2])*(V2[0]*V2[0]+V2[1]*V2[1]+V2[2]*V2[2]))), TYPE2(-1), TYPE2(1)); +} // ComputeAngle +// same as above, but with the vectors normalized +template +inline TYPE2 ComputeAngleN(const TYPE1* V1, const TYPE1* V2) { + return CLAMP(TYPE2(V1[0]*V2[0]+V1[1]*V2[1]+V1[2]*V2[2]), TYPE2(-1), TYPE2(1)); +} // ComputeAngleN +// given three 3D points, +// compute the angle between the vectors formed by the first point with the other two +// returns cos(angle) +template +inline TYPE2 ComputeAngle(const TYPE1* B, const TYPE1* X1, const TYPE1* X2) { + // create the two vectors + const TYPE1 V1[] = {X1[0]-B[0], X1[1]-B[1], X1[2]-B[2]}; + const TYPE1 V2[] = {X2[0]-B[0], X2[1]-B[1], X2[2]-B[2]}; + return ComputeAngle(V1, V2); +} // ComputeAngle +// given four 3D points, +// compute the angle between the two vectors formed by the first and second pair of points +// returns cos(angle) +template +inline TYPE2 ComputeAngle(const TYPE1* X1, const TYPE1* C1, const TYPE1* X2, const TYPE1* C2) { + // subtract out the camera center + const TYPE1 V1[] = {X1[0]-C1[0], X1[1]-C1[1], X1[2]-C1[2]}; + const TYPE1 V2[] = {X2[0]-C2[0], X2[1]-C2[1], X2[2]-C2[2]}; + return ComputeAngle(V1, V2); +} // ComputeAngle +/*----------------------------------------------------------------*/ + +// given a triangle defined by three 3D points, +// compute its normal (plane's normal oriented according to the given points order) +template +inline TPoint3 ComputeTriangleNormal(const TPoint3& v0, const TPoint3& v1, const TPoint3& v2) { + return (v1-v0).cross(v2-v0); +} // ComputeTriangleNormal +/*----------------------------------------------------------------*/ + +// compute the area of a triangle using Heron's formula +template +TYPE ComputeTriangleAreaSqLen(TYPE lena, TYPE lenb, TYPE lenc) { + const TYPE s((lena+lenb+lenc)/TYPE(2)); + return s*(s-lena)*(s-lenb)*(s-lenc); +} +template +TYPE ComputeTriangleAreaLen(TYPE lena, TYPE lenb, TYPE lenc) { + return (TYPE)sqrt(ComputeTriangleAreaSqLen(lena, lenb, lenc)); +} +template +TYPE ComputeTriangleAreaSqLenSq(TYPE lenaSq, TYPE lenbSq, TYPE lencSq) { + return SQUARE(lenaSq+lenbSq+lencSq)/TYPE(2)-(lenaSq*lenaSq+lenbSq*lenbSq+lencSq*lencSq); +} +template +TYPE ComputeTriangleAreaLenSq(TYPE lenaSq, TYPE lenbSq, TYPE lencSq) { + return (TYPE)sqrt(ComputeTriangleAreaSqLenSq(lenaSq, lenbSq, lencSq)); +} +// compute area for a triangle defined by three 2D points +template +TYPE EdgeFunction(const TPoint2& x0, const TPoint2& x1, const TPoint2& x2) { + return TYPE((x2-x0).cross(x1-x0)); +} +template +TYPE ComputeTriangleArea(const TPoint2& x0, const TPoint2& x1, const TPoint2& x2) { + return (TYPE)abs(EdgeFunction(x0, x1, x2)/TYPE(2)); +} +// compute area for a triangle defined by three 3D points +template +TYPE ComputeTriangleAreaSq(const TPoint3& x0, const TPoint3& x1, const TPoint3& x2) { + return (TYPE)normSq((x1-x0).cross(x2-x0))/TYPE(4); +} +template +TYPE ComputeTriangleArea(const TPoint3& x0, const TPoint3& x1, const TPoint3& x2) { + return (TYPE)sqrt(ComputeTriangleAreaSq(x0, x1, x2)); +} // ComputeTriangleArea +/*----------------------------------------------------------------*/ + +// compute signed volume of the tetrahedron defined by the given triangle connected to origin +template +TYPE ComputeTriangleVolume(const TPoint3& x0, const TPoint3& x1, const TPoint3& x2) { + return (TYPE)(x0.dot(x1.cross(x2)) / TYPE(6)); +} // ComputeTriangleVolume +/*----------------------------------------------------------------*/ + +// compute a shape quality measure of the triangle composed by vertices (v0,v1,v2) +// returns 2*AreaTri/(MaxEdge^2) in range [0, 0.866] +// (ex: equilateral sqrt(3)/2, half-square 1/2, up to a line that has zero quality) +template +inline TYPE ComputeTriangleQuality(const TPoint3& v0, const TPoint3& v1, const TPoint3& v2) { + const TPoint3 d10(v1-v0); + const TPoint3 d20(v2-v0); + const TPoint3 d12(v1-v2); + const TYPE a((TYPE)norm(d10.cross(d20))); + if (a == 0) return 0; // area zero triangles have surely zero quality + const TYPE nd10(normSq(d10)); + if (nd10 == 0) return 0; // area zero triangles have surely zero quality + const TYPE b(MAXF3(nd10, normSq(d20), normSq(d12))); + return a/b; +} // ComputeTriangleQuality +/*----------------------------------------------------------------*/ + +// given a triangle defined by 3 vertex positions and a point, +// compute the barycentric coordinates corresponding to that point +// (only the first two values: alpha and beta) +template +inline TPoint2 BarycentricCoordinatesUV(const TPoint3& A, const TPoint3& B, const TPoint3& C, const TPoint3& P) { + #if 0 + // the triangle normal + const TPoint3 normalVec((B-A).cross(C-A)); + //const TYPE normalLen(norm(normalVec)); + // the area of the triangles + const TYPE invAreaABC(INVERT(normSq(normalVec)/*/(2*normalLen)*/)); + const TPoint3 CP(C-P); + const TYPE areaPBC(normalVec.dot((B-P).cross(CP))/*/(2*normalLen)*/); + const TYPE areaPCA(normalVec.dot(CP.cross(A-P))/*/(2*normalLen)*/); + // the barycentric coordinates + return TPoint2( + areaPBC * invAreaABC, // alpha + areaPCA * invAreaABC // beta + ); + #else + // using the Cramer's rule for solving a linear system + const TPoint3 v0(A-C), v1(B-C), v2(P-C); + const TYPE d00(normSq(v0)); + const TYPE d01(v0.dot(v1)); + const TYPE d11(normSq(v1)); + const TYPE d20(v2.dot(v0)); + const TYPE d21(v2.dot(v1)); + const TYPE invDenom(INVERT(d00 * d11 - d01 * d01)); + return TPoint2( + (d11 * d20 - d01 * d21) * invDenom, // alpha + (d00 * d21 - d01 * d20) * invDenom // beta + ); + #endif +} +// same as above, but returns all three barycentric coordinates +template +inline TPoint3 BarycentricCoordinates(const TPoint3& A, const TPoint3& B, const TPoint3& C, const TPoint3& P) { + TPoint2 b(BarycentricCoordinatesUV(A, B, C, P)); + return TPoint3( + b.x, // alpha + b.y, // beta + TYPE(1)-b.x-b.y // gamma + ); +} +// same as above, but for 2D triangle case +template +inline TPoint2 BarycentricCoordinatesUV(const TPoint2& A, const TPoint2& B, const TPoint2& C, const TPoint2& P) { + const TPoint2 D(P - C); + const TYPE d00(A.x - C.x); + const TYPE d01(B.x - C.x); + const TYPE d10(A.y - C.y); + const TYPE d11(B.y - C.y); + const TYPE invDet(INVERT(d00 * d11 - d10 * d01)); + return TPoint2( + (d11 * D.x - d01 * D.y) * invDet, // alpha + (d00 * D.y - d10 * D.x) * invDet // beta + ); +} +// same as above, but returns all three barycentric coordinates +template +inline TPoint3 BarycentricCoordinates(const TPoint2& A, const TPoint2& B, const TPoint2& C, const TPoint2& P) { + TPoint2 b(BarycentricCoordinatesUV(A, B, C, P)); + return TPoint3( + b.x, // alpha + b.y, // beta + TYPE(1)-b.x-b.y // gamma + ); +} +// correct the barycentric coordinates in case of numerical errors +// (the corresponding point is assumed to be inside the triangle) +template +inline TPoint3 CorrectBarycentricCoordinates(TPoint2 b) { + if (b.x < TYPE(0)) // alpha + b.x = TYPE(0); + else if (b.x > TYPE(1)) + b.x = TYPE(1); + if (b.y < TYPE(0)) // beta + b.y = TYPE(0); + else if (b.y > TYPE(1)) + b.y = TYPE(1); + TYPE z(TYPE(1) - b.x - b.y); // gamma + if (z < 0) { + // equally distribute the error + const TYPE half(-z/TYPE(2)); + if (half > b.x) { + b.x = TYPE(0); + b.y = TYPE(1); + } else + if (half > b.y) { + b.x = TYPE(1); + b.y = TYPE(0); + } else { + b.x -= half; + b.y -= half; + } + z = TYPE(0); + } + // check that the given point is inside the triangle + ASSERT((b.x >= 0) && (b.y >= 0) && (b.x+b.y <= 1) && ISEQUAL(b.x+b.y+z, TYPE(1))); + return TPoint3(b.x, b.y, z); +} +template +inline TPoint3 PerspectiveCorrectBarycentricCoordinates(const TPoint3& b, TYPE z0, TYPE z1, TYPE z2) { + const TPoint3 pb(b.x * z1 * z2, b.y * z0 * z2, b.z * z0 * z1); + return pb / (pb.x + pb.y + pb.z); +} +/*----------------------------------------------------------------*/ + +// Encodes/decodes a normalized 3D vector in two parameters for the direction +template +inline void Normal2Dir(const TPoint3& d, TPoint2& p) { + ASSERT(ISEQUAL(norm(d), T(1))); + p.x = TR(atan2(d.y, d.x)); + p.y = TR(acos(d.z)); +} +template +inline void Dir2Normal(const TPoint2& p, TPoint3& d) { + const T siny(sin(p.y)); + d.x = TR(cos(p.x)*siny); + d.y = TR(sin(p.x)*siny); + d.z = TR(cos(p.y)); + ASSERT(ISEQUAL(norm(d), TR(1))); +} +// Encodes/decodes a 3D vector in two parameters for the direction and one parameter for the scale +template +inline void Vector2DirScale(const T vect[3], TR dir[2], TR* scale) +{ + const T scl(sqrt(SQUARE(vect[0])+SQUARE(vect[1])+SQUARE(vect[2]))); + ASSERT(!ISZERO(scl)); + scale[0] = TR(scl); + dir[0] = TR(atan2(vect[1], vect[0])); + dir[1] = TR(acos(vect[2] / scl)); +} +template +inline void DirScale2Vector(const T dir[2], const T* scale, TR vect[3]) +{ + ASSERT(!ISZERO(*scale)); + const T siny(*scale*sin(dir[1])); + vect[0] = TR(cos(dir[0])*siny); + vect[1] = TR(sin(dir[0])*siny); + vect[2] = TR(*scale*cos(dir[1])); +} +/*----------------------------------------------------------------*/ + + +template +inline T MaxDepthDifference(T d, T threshold) { + #if 0 + return (d*threshold*T(2))/(threshold+T(2)); + #else + return d*threshold; + #endif +} +template +inline T DepthSimilarity(T d0, T d1) { + ASSERT(d0 > 0); + #if 0 + return ABS(d0-d1)*T(2)/(d0+d1); + #else + return ABS(d0-d1)/d0; + #endif +} +template +inline bool IsDepthSimilar(T d0, T d1, T threshold=T(0.01)) { + return DepthSimilarity(d0, d1) < threshold; +} +template +inline bool IsNormalSimilar(const TPoint3& n0, const TPoint3& n1, T threshold=T(0.996194698)/*COS(FD2R(5.f))*/) { + return ComputeAngle(n0.ptr(), n1.ptr()) > threshold; +} +/*----------------------------------------------------------------*/ + + +// searches min/max value +template +inline TYPE FindMinElement(const TYPE* values, size_t n) { + TYPE m = std::numeric_limits::max(); + while (n) if (values[--n] < m) m = values[n]; + return m; +} +template +inline TYPE FindMaxElement(const TYPE* values, size_t n) { + TYPE m = -std::numeric_limits::max(); + while (n) if (values[--n] > m) m = values[n]; + return m; +} +// like above, but considers absolute value only +template +inline TYPE FindAbsMinElement(const TYPE* values, size_t n) { + TYPE m = std::numeric_limits::max(); + while (n) if (ABS(values[--n]) < m) m = ABS(values[n]); + return m; +} +template +inline TYPE FindAbsMaxElement(const TYPE* values, size_t n) { + TYPE m = -std::numeric_limits::max(); + while (n) if (ABS(values[--n]) > m) m = ABS(values[n]); + return m; +} +/*----------------------------------------------------------------*/ + + +// given an array of values and their bound, approximate the area covered, in percentage +template +inline TYPE ComputeCoveredArea(const TYPE* values, size_t size, const TYPE* bound, int stride=n) { + ASSERT(size > 0); + typedef Eigen::Matrix Vector; + typedef Eigen::Map MapVector; + typedef Eigen::Matrix Matrix; + typedef Eigen::Map > MapMatrix; + typedef Eigen::Matrix MatrixSurface; + const MapMatrix points(values, size, n, Eigen::OuterStride<>(stride)); + const Vector norm = MapVector(bound); + const Vector offset(Vector::Constant(bCentered ? TYPE(0.5) : TYPE(0))); + MatrixSurface surface; + surface.setZero(); + for (size_t i=0; i=0 && point(0)=0 && point(1) +inline void ComputeCovarianceMatrix(const TYPE* values, size_t size, TYPE* cov, int stride=n) { + ASSERT(size > 0); + typedef Eigen::Matrix Vector; + typedef Eigen::Matrix Matrix; + typedef Eigen::Map > MapMatrix; + typedef Eigen::Matrix MatrixOut; + typedef Eigen::Map MapMatrixOut; + const MapMatrix points(values, size, n, Eigen::OuterStride<>(stride)); + const Vector mean(points.colwise().sum() * (TYPE(1)/size)); + const Matrix transPoints(points.rowwise() - mean); + MapMatrixOut mapCov(cov); + mapCov = transPoints.transpose() * transPoints * (TYPE(1)/(size-1)); +} // ComputeCovarianceMatrix +/*----------------------------------------------------------------*/ + +// given an array of values, compute the mean +template +inline void ComputeMean(const TYPE* values, size_t size, TYPEW& mean) { + ASSERT(size > 0); + TYPEW sum(0); + for (size_t i=0; i +inline void ComputeGeometricMean(const TYPE* values, size_t size, TYPEW& gmean) { + ASSERT(size > 0); + TYPEW prod(1); + for (size_t i=0; i +inline void ComputeMeanStdOffline(const TYPE* values, size_t size, TYPEW& mean, TYPEW& stddev) { + ASSERT(size > 0); + TYPEW sum(0); + FOREACHRAWPTR(pVal, values, size) + sum += TYPEW(*pVal); + mean = sum / (float)size; + TYPEW sumSq(0); + FOREACHRAWPTR(pVal, values, size) + sumSq += SQUARE(TYPEW(*pVal)-mean); + const TYPEW variance(sumSq / (size - 1)); + stddev = SQRT(variance); +} // ComputeMeanStdOffline +// same as above, but uses one pass only (online) +template +inline void ComputeMeanStdOnline(const TYPE* values, size_t size, TYPEW& mean, TYPEW& stddev) { + ASSERT(size > 0); + TYPEW sum(0), sumSq(0); + FOREACHRAWPTR(pVal, values, size) { + const TYPEW val(*pVal); + sum += val; + sumSq += SQUARE(val); + } + const TYPEW invSize(TYPEW(1)/(float)size); + mean = sum * invSize; + const TYPEW variance((sumSq - SQUARE(sum) * invSize) / (float)(size - 1)); + stddev = SQRT(variance); +} // ComputeMeanStdOnlineFast +#define ComputeMeanStd ComputeMeanStdOnline +// same as above, but an interactive version +template +struct MeanStd { + typedef TYPE Type; + typedef TYPEW TypeW; + typedef TYPER TypeR; + typedef ARGTYPE ArgType; + TYPEW sum, sumSq; + size_t size; + MeanStd() : sum(0), sumSq(0), size(0) {} + MeanStd(const Type* values, size_t _size) : MeanStd() { Compute(values, _size); } + void Update(ArgType v) { + const TYPEW val(static_cast(v)); + sum += val; + sumSq += SQUARE(val); + ++size; + } + void Compute(const Type* values, size_t _size) { + for (size_t i=0; i<_size; ++i) + Update(values[i]); + } + TYPEW GetSum() const { return sum; } + TYPEW GetMean() const { return static_cast(sum / static_cast(size)); } + TYPEW GetRMS() const { return static_cast(SQRT(sumSq / static_cast(size))); } + TYPEW GetVarianceN() const { return static_cast(sumSq - SQUARE(sum) / static_cast(size)); } + TYPEW GetVariance() const { return static_cast(GetVarianceN() / static_cast(size)); } + TYPEW GetStdDev() const { return SQRT(GetVariance()); } + void Clear() { sum = sumSq = TYPEW(0); size = 0; } +}; +// same as above, but records also min/max values +template +struct MeanStdMinMax : MeanStd { + typedef MeanStd Base; + typedef TYPE Type; + typedef TYPEW TypeW; + typedef TYPER TypeR; + typedef ARGTYPE ArgType; + Type minVal, maxVal; + MeanStdMinMax() : minVal(std::numeric_limits::max()), maxVal(std::numeric_limits::lowest()) {} + MeanStdMinMax(const Type* values, size_t _size) : MeanStdMinMax() { Compute(values, _size); } + void Update(ArgType v) { + if (minVal > v) + minVal = v; + if (maxVal < v) + maxVal = v; + Base::Update(v); + } + void Compute(const Type* values, size_t _size) { + for (size_t i=0; i<_size; ++i) + Update(values[i]); + } +}; +/*----------------------------------------------------------------*/ + +// given an array of values, compute the X84 threshold as in: +// Hampel FR, Rousseeuw PJ, Ronchetti EM, Stahel WA +// "Robust Statistics: the Approach Based on Influence Functions" +// Wiley Series in Probability and Mathematical Statistics, John Wiley & Sons, 1986 +// returns the pair(median,trust_region) +// upper-bound threshold = median+trust_region +// lower-bound threshold = median-trust_region +template +inline std::pair ComputeX84Threshold(const TYPE* const values, size_t size, TYPEW mul=TYPEW(5.2), const uint8_t* mask=NULL) { + ASSERT(size > 0); + // median = MEDIAN(values); + cList data; + if (mask) { + // use only masked data + data.Reserve(size); + for (size_t i=0; i::type; + for (TYPE& val: data) + val = TYPE(ABS(TYPEI(val)-TYPEI(median))); + std::nth_element(data.Begin(), mid, data.End()); + return std::make_pair(median, mul*TYPEW(*mid)); +} // ComputeX84Threshold +/*----------------------------------------------------------------*/ + +// given an array of values, compute the upper/lower threshold using quartiles +template +inline std::tuple ComputeQuartileThreshold(const TYPE* const values, size_t size, TYPEW mul=TYPEW(1.5), const uint8_t* mask=NULL) { + ASSERT(size > 0); + cList data(size); + if (mask) { + // use only masked data + data.Reserve(size); + for (size_t i=0; i +inline REAL ComputeSNR(const TDMatrix& x0, const TDMatrix& x) { + ASSERT(x0.area() == x.area()); + const REAL err(norm(TDMatrix(x0 - x))); + const REAL x0Norm(norm(x0)); + REAL ret(std::numeric_limits::infinity()); + if (ISZERO(x0Norm) && err > 0) ret = 0; + else if (err > 0) ret = 20.0 * std::log10(x0Norm / err); + return ret; +} // ComputeSNR +template +inline REAL ComputeSNR(const TMatrix& x0, const TMatrix& x) { + return ComputeSNR(TDMatrix(N,1,(TYPE*)x0.val), TDMatrix(N,1,(TYPE*)x.val)); +} // ComputeSNR +template +inline REAL ComputePSNR(const TDMatrix& x0, const TDMatrix& x) { + ASSERT(x0.area() == x.area()); + const size_t N(x0.area()); + const REAL err(normSq(TDMatrix(x0 - x)) / N); + TYPE max1(0), max2(0); + for (unsigned i=0; i::infinity()); + if (ISZERO(maxBoth) && err > 0) ret = 0; + else if (err > 0) ret = 10.0 * std::log10(static_cast(maxBoth*maxBoth) / err); + return ret; +} // ComputePSNR +template +inline REAL ComputePSNR(const TMatrix& x0, const TMatrix& x) { + return ComputePSNR(TDMatrix(N,1,(TYPE*)x0.val), TDMatrix(N,1,(TYPE*)x.val)); +} // ComputePSNR +/*----------------------------------------------------------------*/ + + +// Builds histograms using "Kernel Density Estimation" and Gaussian kernels; +// see: L. Wassermann: "All of Statistics" for example. +// Samples should be composed of two point: value and weight +template +inline TYPE BandwidthGaussianKDE(const TMatrix* samples, size_t numSamples, TYPE sigma=0, TYPE* pSigma=NULL) { + if (sigma == 0) { + TYPE avg(0); + FOREACHRAWPTR(pSample, samples, numSamples) { + const TYPE& sample((*pSample)(0)); + avg += sample; + sigma += SQUARE(sample); + } + avg /= numSamples; + sigma = SQRT(sigma/numSamples - avg*avg); // Standard Deviation + if (pSigma) *pSigma = sigma; + } + // This is the optimal bandwidth if the point distribution is Gaussian. + // (see "Applied Smoothing Techniques for Data Analysis" by Adrian W, Bowman & Adelchi Azzalini (1997)) + return POW(TYPE(4)/(TYPE(3)*numSamples), TYPE(1)/TYPE(5))*sigma; +} +// estimate density at the given value +template +inline TYPE KernelDensityEstimation(const TMatrix* samples, size_t numSamples, TYPE x, TYPE bandwidth) { + TYPE y(0); + const TYPE invBandwidth(TYPE(1)/bandwidth); + FOREACHRAWPTR(pSample, samples, numSamples) { + const TMatrix& sample(*pSample); + const TYPE val((x - sample(0))*invBandwidth); + y += sample(1) * EXP(-TYPE(0.5)*val*val)*invBandwidth; + } + return (y / numSamples) * INV_SQRT_2PI; +} +// estimate density at the given range of values; +// INSERTER should have defined the operator(TYPE,TYPE) that receives a pair of value and density +template +void KernelDensityEstimation(const TMatrix* samples, size_t numSamples, TYPE bandwidth, TYPE xMin, TYPE xMax, size_t steps, INSERTER& inserter) { + const TYPE step((xMax-xMin)/steps); + for (size_t i=0; i +void KernelDensityEstimation(const TMatrix* samples, size_t numSamples, TYPE bandwidth, TYPE xMin, TYPE xMax, TYPE stepMax, INSERTER& inserter) { + size_t smallSteps(0); + TYPE step(stepMax); + TYPE x(xMin), last_x; + TYPE last_y(KernelDensityEstimation(samples, numSamples, x, bandwidth)); + TYPE last_delta(FLT_EPSILON); + inserter(x, last_y); + x += step; + do { + const TYPE y(KernelDensityEstimation(samples, numSamples, x, bandwidth)); + inserter(x, y); + if (smallSteps) { + if (--smallSteps == 0) { + // continue from the last coarse step + step = stepMax; + inserter.EndPeak(); + x = last_x; + } + } else { + const TYPE delta(y-last_y); + if (ABS(SIGN(last_delta)+SIGN(delta)) < 2 && inserter.SamplePeak(last_delta>0)) { + // last step contains a local peak (minimum/maximum), + // so go back and sample more often; + // assuming the curve is locally symmetric and the step small enough, + // the peak is inside the first or second step behind + last_x = x; + x -= (ABS(delta) < ABS(last_delta) ? step : step*TYPE(2)); + smallSteps = 5; + step /= TYPE(smallSteps+1); + } + last_y = y; + last_delta = delta; + } + } while ((x+=step) < xMax); +} +#if TD_VERBOSE == TD_VERBOSE_DEBUG +// example from http://en.wikipedia.org/wiki/Kernel_density_estimation +inline void ExampleKDE() { + typedef REAL Real; + const Real values[6] ={-2.1, -1.3, -0.4, 1.9, 5.1, 6.2}; + TMatrix samples[6]; + for (int i=0; i<6; ++i) { + samples[i](0) = values[i]; + samples[i](1) = 1; + } + struct SamplesInserter { + File f; + SamplesInserter(const String& fileName) + : f(fileName, File::WRITE, File::CREATE | File::TRUNCATE) {} + inline void operator() (Real x, Real y) { + f.print("%g\t%g\n", x, y); + } + inline bool SamplePeak(bool bMaxRegion) const { + return bMaxRegion; + } + inline void EndPeak() { + } + }; + const size_t steps(30); + const Real bandwidth = BandwidthGaussianKDE(samples, 6)/2; + const Real xMin(-15), xMax(25), stepMax((xMax-xMin)/steps); + SamplesInserter inserter("kde.txt"); + KernelDensityEstimation(samples, 6, bandwidth, xMin, xMax, stepMax, inserter); +} +#endif +/*----------------------------------------------------------------*/ + + +// normalize image points (inhomogeneous 2D) so that their centroid and typical magnitude of the vector is (1,1) +template +void NormalizePoints(const CLISTDEF0(TPoint2)& pointsIn, CLISTDEF0(TPoint2)& pointsOut, TMatrix* pH=NULL) { + // find centroid + TPoint2 ptAvg(0,0); + FOREACHPTR(pPt, pointsIn) + ptAvg.x += TPoint2(*pPt); + ptAvg *= HTYPE(1)/HTYPE(pointsIn.GetSize()); + // move centroid to origin + if (pointsOut.GetSize() != pointsIn.GetSize()) + pointsOut.Resize(pointsIn.GetSize()); + HTYPE var(0); + const TPoint2 ptAvgF(ptAvg); + FOREACH(i, pointsIn) { + const TPoint2& ptIn = pointsIn[i]; + TPoint2& ptOut = pointsOut[i]; + ptOut = ptIn - ptAvgF; + var += norm(ptOut); + } + // calculate variance + var /= HTYPE(pointsIn.GetSize()); + // scale points + HTYPE scale(1); + if (!ISZERO(var)) + scale = HTYPE(SQRT_2) / var; + FOREACHPTR(pPt, pointsOut) + *pPt *= scale; + // initialize normalizing homography matrix from Scale and Translation (H = S * T); + if (pH != NULL) { + TMatrix& H = *pH; + H = TMatrix::IDENTITY; + H(0,2) = -scale * ptAvg.x; + H(1,2) = -scale * ptAvg.y; + H(0,0) = scale; + H(1,1) = scale; + } +} +// normalize scene points (inhomogeneous 3D) so that their centroid and typical magnitude of the vector is (1,1,1) +// This normalization algorithm is suitable only for compact distribution of points (see Hartley04 p180) +template +void NormalizePoints(const CLISTDEF0(TPoint3)& pointsIn, CLISTDEF0(TPoint3)& pointsOut, TMatrix* pH=NULL) { + // find centroid + TPoint3 ptAvg(0, 0, 0); + FOREACHPTR(pPt, pointsIn) + ptAvg += TPoint3(*pPt); + ptAvg *= HTYPE(1)/HTYPE(pointsIn.GetSize()); + // move centroid to origin + if (pointsOut.GetSize() != pointsIn.GetSize()) + pointsOut.Resize(pointsIn.GetSize()); + HTYPE var(0); + FOREACH(i, pointsIn) { + const TPoint3& ptIn = pointsIn[i]; + TPoint3& ptOut = pointsOut[i]; + ptOut = ptIn - ptAvg; + var += norm(ptOut); + } + // calculate variance + var /= HTYPE(pointsIn.GetSize()); + // scale points + HTYPE scale(1); + if (!ISZERO(var)) + scale = HTYPE(SQRT_3) / var; + FOREACHPTR(pPt, pointsOut) + *pPt *= scale; + // initialize normalizing homography matrix + if (pH != NULL) { + TMatrix& H = *pH; + H = TMatrix::IDENTITY; + H(0,3) = -scale * ptAvg.x; + H(1,3) = -scale * ptAvg.y; + H(2,3) = -scale * ptAvg.z; + H(0,0) = scale; + H(1,1) = scale; + H(2,2) = scale; + } +} +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE diff --git a/libs/Common/UtilCUDA.cpp b/libs/Common/UtilCUDA.cpp new file mode 100644 index 0000000..dddc6d5 --- /dev/null +++ b/libs/Common/UtilCUDA.cpp @@ -0,0 +1,436 @@ +//////////////////////////////////////////////////////////////////// +// UtilCUDA.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" +#include "UtilCUDA.h" + +#ifdef _USE_CUDA + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +namespace CUDA { + +// S T R U C T S /////////////////////////////////////////////////// + +int desiredDeviceID = -1; +Devices devices; + +// GPU Architecture definitions +int _convertSMVer2Cores(int major, int minor) +{ + if (major == 9999 && minor == 9999) + return 1; + + // Defines for GPU Architecture types (using the SM version to determine the # of cores per SM + struct sSMtoCores { + int SM; // 0xMm (hexadecimal notation), M = SM Major version, and m = SM minor version + int Cores; + }; + const sSMtoCores nGpuArchCoresPerSM[] = { + {0x20, 32}, // Fermi Generation (SM 2.0) GF100 class + {0x21, 48}, // Fermi Generation (SM 2.1) GF10x class + {0x30, 192}, // Kepler Generation (SM 3.0) GK10x class + {0x32, 192}, // Kepler Generation (SM 3.2) GK10x class + {0x35, 192}, // Kepler Generation (SM 3.5) GK11x class + {0x37, 192}, // Kepler Generation (SM 3.7) GK21x class + {0x50, 128}, // Maxwell Generation (SM 5.0) GM10x class + {0x52, 128}, // Maxwell Generation (SM 5.2) GM20x class + {0x53, 128}, // Maxwell Generation (SM 5.3) GM20x class + {0x60, 64 }, // Pascal Generation (SM 6.0) GP100 class + {0x61, 128}, // Pascal Generation (SM 6.1) GP10x class + {0x62, 128}, // Pascal Generation (SM 6.2) GP10x class + {0x70, 64 }, // Volta Generation (SM 7.0) GV100 class + {0x72, 64 }, // Volta Generation (SM 7.2) GV10B class + {0x75, 64 }, // Turing Generation (SM 7.5) TU1xx class + {0x80, 64 }, // Ampere Generation (SM 8.0) GA100 class + {-1, -1} + }; + + int index(0); + while (nGpuArchCoresPerSM[index].SM != -1) { + if (nGpuArchCoresPerSM[index].SM == ((major << 4) + minor)) + return nGpuArchCoresPerSM[index].Cores; + index++; + } + + // If we don't find the values, we default use the previous one to run properly + VERBOSE("MapSMtoCores for SM %d.%d is undefined; default to use %d cores/SM", major, minor, nGpuArchCoresPerSM[index-1].Cores); + return nGpuArchCoresPerSM[index-1].Cores; +} + +// checks that the given device ID is valid; +// if successful returns the device info in the given structure +CUresult _gpuCheckDeviceId(int devID, Device& device) +{ + int device_count; + checkCudaError(cuDeviceGetCount(&device_count)); + if (device_count == 0) { + VERBOSE("CUDA error: no devices supporting CUDA"); + return CUDA_ERROR_NO_DEVICE; + } + if (devID < 0) + devID = 0; + if (devID >= device_count) { + VERBOSE("CUDA error: device [%d] is not a valid GPU device (%d CUDA capable GPU device(s) detected)", devID, device_count); + return CUDA_ERROR_INVALID_DEVICE; + } + checkCudaError(cuDeviceGetAttribute(&device.computeMode, CU_DEVICE_ATTRIBUTE_COMPUTE_MODE, devID)); + if (device.computeMode == CU_COMPUTEMODE_PROHIBITED) { + VERBOSE("CUDA error: device is running in "); + return CUDA_ERROR_PROFILER_DISABLED; + } + checkCudaError(cuDeviceComputeCapability(&device.major, &device.minor, devID)); + if (device.major < 1) { + VERBOSE("CUDA error: GPU device does not support CUDA"); + return CUDA_ERROR_INVALID_DEVICE; + } + checkCudaError(cuDeviceGetProperties(&device.prop, devID)); + device.ID = (CUdevice)devID; + return CUDA_SUCCESS; +} + +// finds the best GPU (with maximum GFLOPS); +// if successful returns the device info in the given structure +CUresult _gpuGetMaxGflopsDeviceId(Device& bestDevice) +{ + int device_count = 0; + checkCudaError(cuDeviceGetCount(&device_count)); + if (device_count == 0) { + VERBOSE("CUDA error: no devices supporting CUDA"); + return CUDA_ERROR_NO_DEVICE; + } + + // Find the best major SM Architecture GPU device + Devices devices; + int best_SM_arch = 0; + for (int current_device = 0; current_device < device_count; ++current_device) { + Device device; + if (reportCudaError(cuDeviceGetAttribute(&device.computeMode, CU_DEVICE_ATTRIBUTE_COMPUTE_MODE, current_device)) != CUDA_SUCCESS) + continue; + // If this GPU is not running on Compute Mode prohibited, then we can add it to the list + if (device.computeMode == CU_COMPUTEMODE_PROHIBITED) + continue; + if (reportCudaError(cuDeviceComputeCapability(&device.major, &device.minor, current_device)) != CUDA_SUCCESS) + continue; + if (device.major > 0 && device.major < 9999) { + best_SM_arch = MAXF(best_SM_arch, device.major); + device.ID = (CUdevice)current_device; + devices.Insert(device); + } + } + if (devices.IsEmpty()) { + VERBOSE("CUDA error: all devices have compute mode prohibited"); + return CUDA_ERROR_PROFILER_DISABLED; + } + + // Find the best CUDA capable GPU device + Device* max_perf_device = NULL; + size_t max_compute_perf = 0; + FOREACHPTR(pDevice, devices) { + ASSERT(pDevice->computeMode != CU_COMPUTEMODE_PROHIBITED); + int sm_per_multiproc = _convertSMVer2Cores(pDevice->major, pDevice->minor); + int multiProcessorCount; + if (reportCudaError(cuDeviceGetAttribute(&multiProcessorCount, CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT, (CUdevice)pDevice->ID)) != CUDA_SUCCESS) + continue; + int clockRate; + if (reportCudaError(cuDeviceGetAttribute(&clockRate, CU_DEVICE_ATTRIBUTE_CLOCK_RATE, pDevice->ID)) != CUDA_SUCCESS) + continue; + size_t compute_perf = (size_t)multiProcessorCount * sm_per_multiproc * clockRate; + if (compute_perf > max_compute_perf && + (best_SM_arch < 3 || // if we find GPU with SM major > 2, search only these + pDevice->major == best_SM_arch) ) // if our device==dest_SM_arch, choose this, or else pass + { + max_compute_perf = compute_perf; + max_perf_device = pDevice; + } + } + if (max_perf_device == NULL) + return CUDA_ERROR_INVALID_DEVICE; + + bestDevice = *max_perf_device; + checkCudaError(cuDeviceGetProperties(&bestDevice.prop, bestDevice.ID)); + return CUDA_SUCCESS; +} + +// initialize the given CUDA device and add it to the array of initialized devices; +// if the given device is -1, the best available device is selected +CUresult initDevice(int deviceID) +{ + if (deviceID < -1) + return CUDA_ERROR_INVALID_DEVICE; + + checkCudaError(cuInit(0)); + + Device device; + if (deviceID >= 0) { + checkCudaError(_gpuCheckDeviceId(deviceID, device)); + } else { + // Otherwise pick the device with the highest Gflops/s + checkCudaError(_gpuGetMaxGflopsDeviceId(device)); + } + if (device.major < 3) { + VERBOSE("CUDA error: compute capability 3.2 or greater required (available %d.%d for device[%d])", device.ID, device.major, device.minor); + return CUDA_ERROR_INVALID_DEVICE; + } + devices.Insert(device); + checkCudaError(cuCtxCreate(&devices.Last().ctx, CU_CTX_SCHED_AUTO, device.ID)); + + #if TD_VERBOSE != TD_VERBOSE_OFF + char name[2048]; + checkCudaError(cuDeviceGetName(name, 2048, device.ID)); + size_t memSize; + checkCudaError(cuDeviceTotalMem(&memSize, device.ID)); + DEBUG("CUDA device %d initialized: %s (compute capability %d.%d; memory %s)", device.ID, name, device.major, device.minor, Util::formatBytes(memSize).c_str()); + #endif + return CUDA_SUCCESS; +} + +// load/read module (program) from file/string and compile it +CUresult ptxJIT(LPCSTR program, CUmodule& hModule, int mode) +{ + CUlinkState lState; + CUjit_option options[6]; + void *optionVals[6]; + float walltime(0); + const unsigned logSize(8192); + char error_log[logSize], info_log[logSize]; + void *cuOut; + size_t outSize; + + // Setup linker options + // Return walltime from JIT compilation + options[0] = CU_JIT_WALL_TIME; + optionVals[0] = (void*)&walltime; + // Pass a buffer for info messages + options[1] = CU_JIT_INFO_LOG_BUFFER; + optionVals[1] = (void*)info_log; + // Pass the size of the info buffer + options[2] = CU_JIT_INFO_LOG_BUFFER_SIZE_BYTES; + optionVals[2] = (void*)(long)logSize; + // Pass a buffer for error message + options[3] = CU_JIT_ERROR_LOG_BUFFER; + optionVals[3] = (void*)error_log; + // Pass the size of the error buffer + options[4] = CU_JIT_ERROR_LOG_BUFFER_SIZE_BYTES; + optionVals[4] = (void*)(long)logSize; + // Make the linker verbose + options[5] = CU_JIT_LOG_VERBOSE; + optionVals[5] = (void*)1; + + // Create a pending linker invocation + checkCudaError(cuLinkCreate(6, options, optionVals, &lState)); + + const size_t programLen(strlen(program)); + CUresult myErr; + if (mode == JIT::FILE || (mode == JIT::AUTO && programLen < 256)) { + // Load the PTX from the file + myErr = cuLinkAddFile(lState, CU_JIT_INPUT_PTX, program, 0, 0, 0); + } else { + // Load the PTX from the string + myErr = cuLinkAddData(lState, CU_JIT_INPUT_PTX, (void*)program, programLen+1, 0, 0, 0, 0); + } + if (myErr != CUDA_SUCCESS) { + // Errors will be put in error_log, per CU_JIT_ERROR_LOG_BUFFER option above + VERBOSE("PTX Linker Error: %s", error_log); + return myErr; + } + + // Complete the linker step + checkCudaError(cuLinkComplete(lState, &cuOut, &outSize)); + + // Linker walltime and info_log were requested in options above + DEBUG_LEVEL(3, "CUDA link completed (%gms):\n%s", walltime, info_log); + + // Load resulting cuBin into module + checkCudaError(cuModuleLoadData(&hModule, cuOut)); + + // Destroy the linker invocation + return reportCudaError(cuLinkDestroy(lState)); +} + +// requested function (kernel) from module (program) +CUresult ptxGetFunc(const CUmodule& hModule, LPCSTR functionName, CUfunction& hKernel) +{ + // Locate the kernel entry point + checkCudaError(cuModuleGetFunction(&hKernel, hModule, functionName)); + DEBUG_LEVEL(3, "Kernel '%s' loaded", functionName); + return CUDA_SUCCESS; +} +/*----------------------------------------------------------------*/ + + +void MemDevice::Release() { + if (pData) { + reportCudaError(cuMemFree(pData)); + pData = 0; + } +} +CUresult MemDevice::Reset(size_t size) { + if (pData) { + if (nSize == size) + return CUDA_SUCCESS; + Release(); + } + if (cuMemAlloc(&pData, size) != CUDA_SUCCESS) { + pData = 0; + return CUDA_ERROR_OUT_OF_MEMORY; + } + nSize = size; + return CUDA_SUCCESS; +} +CUresult MemDevice::Reset(const void* pDataHost, size_t size) { + if (pData && nSize != size) + Release(); + if (!pData && cuMemAlloc(&pData, size) != CUDA_SUCCESS) { + pData = 0; + return CUDA_ERROR_OUT_OF_MEMORY; + } + nSize = size; + return cuMemcpyHtoD(pData, pDataHost, size); +} + +CUresult MemDevice::SetData(const void* pDataHost, size_t size) { + ASSERT(IsValid()); + return cuMemcpyHtoD(pData, pDataHost, size); +} + +CUresult MemDevice::GetData(void* pDataHost, size_t size) const { + ASSERT(IsValid()); + return cuMemcpyDtoH(pDataHost, pData, size); +} +/*----------------------------------------------------------------*/ + + +void EventRT::Release() { + if (hEvent) { + reportCudaError(cuEventDestroy(hEvent)); + hEvent = NULL; + } +} +CUresult EventRT::Reset(unsigned flags) { + CUresult ret(cuEventCreate(&hEvent, flags)); + if (ret != CUDA_SUCCESS) + hEvent = NULL; + return ret; +} +/*----------------------------------------------------------------*/ + + +void StreamRT::Release() { + if (hStream) { + reportCudaError(cuStreamDestroy(hStream)); + hStream = NULL; + } +} +CUresult StreamRT::Reset(unsigned flags) { + CUresult ret(cuStreamCreate(&hStream, flags)); + if (ret != CUDA_SUCCESS) + hStream = NULL; + return ret; +} + +CUresult StreamRT::Wait(CUevent hEvent) { + ASSERT(IsValid()); + return cuStreamWaitEvent(hStream, hEvent, 0); +} +/*----------------------------------------------------------------*/ + + +void ModuleRT::Release() { + if (hModule) { + reportCudaError(cuModuleUnload(hModule)); + hModule = NULL; + } +} +CUresult ModuleRT::Reset(LPCSTR program, int mode) { + // compile the module (program) from PTX and get its handle (Driver API) + CUresult result(ptxJIT(program, hModule, mode)); + if (result != CUDA_SUCCESS) + hModule = NULL; + return result; +} +/*----------------------------------------------------------------*/ + + +void KernelRT::Release() { + inDatas.Release(); + outDatas.Release(); + ptrModule.Release(); + hKernel = NULL; +} +void KernelRT::Reset() { + paramOffset = 0; + inDatas.Empty(); + outDatas.Empty(); +} +CUresult KernelRT::Reset(LPCSTR functionName) { + // get the function handle (Driver API) + ASSERT(ptrModule != NULL && ptrModule->IsValid()); + CUresult result(ptxGetFunc(*ptrModule, functionName, hKernel)); + if (result != CUDA_SUCCESS) { + ptrModule.Release(); + hKernel = NULL; + } + return result; +} +CUresult KernelRT::Reset(const ModuleRTPtr& _ptrModule, LPCSTR functionName) { + // set module + ptrModule = _ptrModule; + // set function + return Reset(functionName); +} +CUresult KernelRT::Reset(LPCSTR program, LPCSTR functionName, int mode) { + // compile the module (program) from PTX and get the function handle (Driver API) + ptrModule = new ModuleRT(program, mode); + if (!ptrModule->IsValid()) { + ptrModule.Release(); + hKernel = NULL; + return CUDA_ERROR_INVALID_HANDLE; + } + return Reset(functionName); +} + + +// append a generic input parameter (allocate© input buffer) +CUresult KernelRT::_AddParam(const InputParam& param) { + MemDevice& data = inDatas.AddEmpty(); + if (data.Reset(param.data, param.size) != CUDA_SUCCESS) + return CUDA_ERROR_OUT_OF_MEMORY; + return addKernelParam(hKernel, paramOffset, (CUdeviceptr)data); +} +// append a generic output parameter (allocate output buffer) +CUresult KernelRT::_AddParam(const OutputParam& param) { + MemDevice& data = outDatas.AddEmpty(); + if (data.Reset(param.size) != CUDA_SUCCESS) + return CUDA_ERROR_OUT_OF_MEMORY; + return addKernelParam(hKernel, paramOffset, (CUdeviceptr)data); +} + + +// copy result from the given output parameter index back to the host +CUresult KernelRT::GetResult(const CUdeviceptr data, const ReturnParam& param) const { + return fetchMemDevice(param.data, param.size, data); +} +// read from the device the variadic output parameters +CUresult KernelRT::GetResult(const std::initializer_list& params) const { + MemDeviceArr::IDX idx(0); + for (auto param : params) + if (outDatas[idx++].GetData(param.data, param.size) != CUDA_SUCCESS) + return CUDA_ERROR_INVALID_VALUE; + return CUDA_SUCCESS; +} +/*----------------------------------------------------------------*/ + +} // namespace CUDA + +} // namespace SEACAVE + +#endif // _USE_CUDA diff --git a/libs/Common/UtilCUDA.h b/libs/Common/UtilCUDA.h new file mode 100644 index 0000000..44d869d --- /dev/null +++ b/libs/Common/UtilCUDA.h @@ -0,0 +1,719 @@ +//////////////////////////////////////////////////////////////////// +// UtilCUDA.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_CUDA_H__ +#define __SEACAVE_CUDA_H__ + +#ifdef _USE_CUDA + + +// I N C L U D E S ///////////////////////////////////////////////// + +// CUDA driver +#include + +// CUDA toolkit +#include +#include +#include +#include +#include + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +namespace SEACAVE { + +namespace CUDA { + +extern int desiredDeviceID; + +// global list of initialized devices +struct Device { + CUdevice ID; + int major, minor; + int computeMode; + CUdevprop prop; + CUcontext ctx; + + inline Device() : ctx(NULL) {} + inline ~Device() { if (ctx != NULL) cuCtxDestroy(ctx); } +}; +typedef CLISTDEF0(Device) Devices; +extern Devices devices; + +// outputs the proper CUDA error code in the event that a CUDA host call returns an error +inline CUresult __reportCudaError(CUresult result, LPCSTR errorMessage) { + if (result == CUDA_SUCCESS) + return CUDA_SUCCESS; + LPCSTR szName; + cuGetErrorName(result, &szName); + LPCSTR szError; + cuGetErrorString(result, &szError); + #ifdef _DEBUG + VERBOSE("CUDA error at %s:%d: %s (%s (code %d) - %s)", __FILE__, __LINE__, errorMessage, szName, static_cast(result), szError); + #else + DEBUG("CUDA error: %s (%s (code %d) - %s)", errorMessage, szName, static_cast(result), szError); + #endif + ASSERT("CudaError" == NULL); + return result; +} +#define reportCudaError(val) CUDA::__reportCudaError(val, #val) + +#define checkCudaError(val) { const CUresult ret(CUDA::__reportCudaError(val, #val)); if (ret != CUDA_SUCCESS) return ret; } + +// outputs the proper CUDA error code and abort in the event that a CUDA host call returns an error +inline void __ensureCudaResult(CUresult result, LPCSTR errorMessage) { + if (__reportCudaError(result, errorMessage)) + return; + ASSERT("CudaAbort" == NULL); + exit(EXIT_FAILURE); +} +#define ensureCudaResult(val) CUDA::__ensureCudaResult(val, #val) + +inline void checkCudaCall(const cudaError_t error) { + if (error == cudaSuccess) + return; + #ifdef _DEBUG + VERBOSE("CUDA error at %s:%d: %s (code %d)", __FILE__, __LINE__, cudaGetErrorString(error), error); + #else + DEBUG("CUDA error: %s (code %d)", cudaGetErrorString(error), error); + #endif + ASSERT("CudaError" == NULL); + exit(EXIT_FAILURE); +} + +// rounds up addr to the align boundary +template +inline T align(T o, T a) { + a -= T(1); + return (o + a)&~a; +} + +// initialize the given CUDA device and add it to the array of initialized devices; +// if the given device is -1, the best available device is selected +CUresult initDevice(int deviceID=-1); + +// load/read module (program) from file/string and compile it +enum JIT { AUTO=0, STRING=1, FILE=2 }; +CUresult ptxJIT(LPCSTR program, CUmodule& hModule, int mode=JIT::AUTO); + +// requested function (kernel) from module (program) +CUresult ptxGetFunc(const CUmodule& hModule, LPCSTR functionName, CUfunction& hKernel); + +// add a new parameter to the given kernel +template +inline CUresult addKernelParam(CUfunction& hKernel, int& paramOffset, const T& param) { + paramOffset = align(paramOffset, (int)alignof(T)); + const CUresult result(cuParamSetv(hKernel, paramOffset, (void*)¶m, sizeof(T))); + paramOffset += sizeof(T); + return result; +} + +// allocate on the CUDA device a chunk of memory of the given size +inline CUresult allocMemDevice(size_t size, CUdeviceptr& dataDevice) { + return cuMemAlloc(&dataDevice, size); +} +// copy on the CUDA device the given chunk of memory +inline CUresult copyMemDevice(const void* data, size_t size, CUdeviceptr dataDevice) { + return cuMemcpyHtoD(dataDevice, data, size); +} +// allocate and copy on the CUDA device the given chunk of memory +inline CUresult createReplicaDevice(const void* data, size_t size, CUdeviceptr& dataDevice) { + if (cuMemAlloc(&dataDevice, size) != CUDA_SUCCESS) + return CUDA_ERROR_OUT_OF_MEMORY; + return cuMemcpyHtoD(dataDevice, data, size); +} +// copy from the CUDA device the given chunk of memory +inline CUresult fetchMemDevice(void* data, size_t size, const CUdeviceptr dataDevice) { + return cuMemcpyDtoH(data, dataDevice, size); +} +// free the given memory on the CUDA device +inline CUresult freeMemDevice(CUdeviceptr& dataDevice) { + if (cuMemFree(dataDevice) != CUDA_SUCCESS) + return CUDA_ERROR_NOT_INITIALIZED; + dataDevice = 0; + return CUDA_SUCCESS; +} +/*----------------------------------------------------------------*/ + + +class MemDevice +{ +protected: + CUdeviceptr pData; + size_t nSize; + +public: + inline MemDevice() : pData(0) {} + inline MemDevice(size_t size) : pData(0) { reportCudaError(Reset(size)); } + inline MemDevice(const void* pDataHost, size_t size) : pData(0) { reportCudaError(Reset(pDataHost, size)); } + template + inline MemDevice(const TImage& param) : pData(0) { reportCudaError(Reset(param)); } + template + inline MemDevice(const cList& param) : pData(0) { reportCudaError(Reset(param)); } + inline ~MemDevice() { Release(); } + + MemDevice(MemDevice& rhs) : pData(rhs.pData) { rhs.pData = 0; } + MemDevice& operator=(MemDevice& rhs) { pData = rhs.pData; rhs.pData = 0; return *this; } + + inline bool IsValid() const { + return (pData != 0); + } + void Release(); + CUresult Reset(size_t size); + CUresult Reset(const void* pDataHost, size_t size); + template + inline CUresult Reset(const TImage& param) { + ASSERT(!param.empty() && param.isContinuous()); + return Reset(param.getData(), sizeof(TYPE)*param.area()); + } + template + inline CUresult Reset(const cList& param) { + ASSERT(!param.IsEmpty()); + return Reset(param.GetData(), param.GetDataSize()); + } + + CUresult SetData(const void* pDataHost, size_t size); + template + inline CUresult SetData(const TImage& param) { + ASSERT(!param.empty() && param.isContinuous()); + return SetData(param.getData(), sizeof(TYPE)*param.area()); + } + template + inline CUresult SetData(const cList& param) { + ASSERT(!param.IsEmpty()); + return SetData(param.GetData(), param.GetDataSize()); + } + + CUresult GetData(void* pDataHost, size_t size) const; + template + inline CUresult GetData(TImage& param) const { + ASSERT(!param.empty() && param.isContinuous()); + return GetData(param.getData(), sizeof(TYPE)*param.area()); + } + template + inline CUresult GetData(cList& param) const { + ASSERT(!param.IsEmpty()); + return GetData(param.GetData(), param.GetDataSize()); + } + + inline operator CUdeviceptr() const { + return pData; + } +}; +typedef CSharedPtr MemDevicePtr; +typedef CLISTDEFIDX(MemDevice,int) MemDeviceArr; +/*----------------------------------------------------------------*/ + + +class EventRT +{ +protected: + CUevent hEvent; + +protected: + EventRT(const EventRT&); + EventRT& operator=(const EventRT&); + +public: + inline EventRT(unsigned flags = CU_EVENT_DEFAULT) { reportCudaError(Reset(flags)); } + inline ~EventRT() { Release(); } + + inline bool IsValid() const { + return (hEvent != NULL); + } + void Release(); + CUresult Reset(unsigned flags = CU_EVENT_DEFAULT); + + inline operator CUevent() const { + return hEvent; + } +}; +typedef CSharedPtr EventRTPtr; +/*----------------------------------------------------------------*/ + + +class StreamRT +{ +protected: + CUstream hStream; + +protected: + StreamRT(const StreamRT&); + StreamRT& operator=(const StreamRT&); + +public: + inline StreamRT(unsigned flags = CU_STREAM_DEFAULT) { reportCudaError(Reset(flags)); } + inline ~StreamRT() { Release(); } + + inline bool IsValid() const { + return (hStream != NULL); + } + void Release(); + CUresult Reset(unsigned flags = CU_STREAM_DEFAULT); + + inline operator CUstream() const { + return hStream; + } + + CUresult Wait(CUevent hEvent); +}; +typedef CSharedPtr StreamRTPtr; +/*----------------------------------------------------------------*/ + + +class ModuleRT +{ +protected: + CUmodule hModule; + +protected: + ModuleRT(const ModuleRT&); + ModuleRT& operator=(const ModuleRT&); + +public: + inline ModuleRT() : hModule(NULL) {} + inline ModuleRT(LPCSTR program, int mode=JIT::AUTO) { Reset(program, mode); } + inline ~ModuleRT() { Release(); } + + inline bool IsValid() const { + return (hModule != NULL); + } + void Release(); + CUresult Reset(LPCSTR program, int mode=JIT::AUTO); + + inline operator CUmodule() const { + return hModule; + } +}; +typedef CSharedPtr ModuleRTPtr; +/*----------------------------------------------------------------*/ + + +class KernelRT +{ +public: + ModuleRTPtr ptrModule; + StreamRTPtr ptrStream; + CUfunction hKernel; + MemDeviceArr inDatas; // array of pointers to the allocated memory read by the program + MemDeviceArr outDatas; // array of pointers to the allocated memory written by the program + int paramOffset; // used during parameter insertion to remember current parameter position + +protected: + KernelRT(const KernelRT&); + KernelRT& operator=(const KernelRT&); + +public: + inline KernelRT() : hKernel(NULL) {} + inline KernelRT(const ModuleRTPtr& _ptrModule, LPCSTR functionName) : ptrModule(_ptrModule) { Reset(functionName); } + inline KernelRT(LPCSTR program, LPCSTR functionName, int mode=JIT::AUTO) { Reset(program, functionName, mode); } + + inline bool IsValid() const { + ASSERT(hKernel == NULL || (ptrModule != NULL && ptrModule->IsValid())); + return (hKernel != NULL); + } + void Release(); + void Reset(); + CUresult Reset(LPCSTR functionName); + CUresult Reset(const ModuleRTPtr& _ptrModule, LPCSTR functionName); + CUresult Reset(LPCSTR program, LPCSTR functionName, int mode=JIT::AUTO); + + struct InputParam { + const void* data; // pointer to host data to be allocated and copied to the CUDA device + size_t size; // size in bytes of the data + inline InputParam() {} + inline InputParam(const void* _data, size_t _size) : data(_data), size(_size) {} + }; + struct OutputParam { + size_t size; // size in bytes of the data to be allocated on the CUDA device + inline OutputParam() {} + inline OutputParam(size_t _size) : size(_size) {} + }; + // lunch the program with the given parameters; + // numThreads - total number of threads to run + // args - variadic parameters to be passed to the kernel + #ifdef _SUPPORT_CPP11 + template + CUresult operator()(int numThreads, Args&&... args) { + ASSERT(IsValid()); + Reset(); + CUresult result; + // set the kernel parameters (Driver API) + if ((result=AddParam(std::forward(args)...)) != CUDA_SUCCESS) + return result; + if ((result=cuParamSetSize(hKernel, paramOffset)) != CUDA_SUCCESS) + return result; + // launch the kernel (Driver API) + const CUdevprop& deviceProp = CUDA::devices.back().prop; + const int numBlockThreads(MINF(numThreads, deviceProp.maxThreadsPerBlock)); + const int nBlocks(MAXF((numThreads+numBlockThreads-1)/numBlockThreads, 1)); + if ((result=cuFuncSetBlockShape(hKernel, numBlockThreads, 1, 1)) != CUDA_SUCCESS) + return result; + if (ptrStream != NULL) + return cuLaunchGridAsync(hKernel, nBlocks, 1, *ptrStream); + return cuLaunchGrid(hKernel, nBlocks, 1); + } + // same for 2D data + template + CUresult operator()(const TPoint2& numThreads, Args&&... args) { + ASSERT(IsValid()); + Reset(); + CUresult result; + // set the kernel parameters (Driver API) + if ((result=AddParam(std::forward(args)...)) != CUDA_SUCCESS) + return result; + if ((result=cuParamSetSize(hKernel, paramOffset)) != CUDA_SUCCESS) + return result; + // launch the kernel (Driver API) + const CUdevprop& deviceProp = CUDA::devices.back().prop; + const REAL scale(MINF(REAL(1), SQRT((REAL)deviceProp.maxThreadsPerBlock/(REAL)(numThreads.x*numThreads.y)))); + const SEACAVE::TPoint2 numBlockThreads(FLOOR2INT(SEACAVE::TPoint2(numThreads)*scale)); + const TPoint2 nBlocks( + MAXF((numThreads.x+numBlockThreads.x-1)/numBlockThreads.x, 1), + MAXF((numThreads.y+numBlockThreads.y-1)/numBlockThreads.y, 1)); + if ((result=cuFuncSetBlockShape(hKernel, numBlockThreads.x, numBlockThreads.y, 1)) != CUDA_SUCCESS) + return result; + if (ptrStream != NULL) + return cuLaunchGridAsync(hKernel, nBlocks.x, nBlocks.y, *ptrStream); + return cuLaunchGrid(hKernel, nBlocks.x, nBlocks.y); + } + #endif // _SUPPORT_CPP11 + + struct ReturnParam { + void* data; // pointer to host data to be written with the output data from the CUDA device + size_t size; // size in bytes of the data + inline ReturnParam() {} + inline ReturnParam(void* _data, size_t _size) : data(_data), size(_size) {} + }; + CUresult GetResult(const CUdeviceptr data, const ReturnParam& param) const; + inline CUresult GetResult(const MemDevice& memDev, const ReturnParam& param) const { + return memDev.GetData(param.data, param.size); + } + inline CUresult GetResult(int idx, const ReturnParam& param) const { + return GetResult(outDatas[idx], param); + } + template + inline CUresult GetResult(int idx, const TImage& param) const { + ASSERT(!param.empty() && param.isContinuous()); + return GetResult(idx, ReturnParam(param.getData(), sizeof(TYPE)*param.area())); + } + template + inline CUresult GetResult(int idx, const cList& param) const { + ASSERT(!param.IsEmpty()); + return GetResult(idx, ReturnParam(param.GetData(), param.GetDataSize())); + } + CUresult GetResult(const std::initializer_list& params) const; + +protected: + CUresult _AddParam(const InputParam& param); + CUresult _AddParam(const OutputParam& param); + template + inline CUresult _AddParam(const T& param) { + return addKernelParam(hKernel, paramOffset, param); + } + inline CUresult _AddParam(const MemDevice& param) { + ASSERT(param.IsValid()); + return addKernelParam(hKernel, paramOffset, (CUdeviceptr)param); + } + template + inline CUresult _AddParam(const TImage& param) { + ASSERT(!param.empty() && param.isContinuous()); + return _AddParam(InputParam(param.getData(), sizeof(TYPE)*param.area())); + } + template + inline CUresult _AddParam(const cList& param) { + ASSERT(!param.IsEmpty()); + return _AddParam(InputParam(param.GetData(), param.GetDataSize())); + } + #ifdef _SUPPORT_CPP11 + template + inline CUresult AddParam(T&& param) { + return _AddParam(std::forward(param)); + } + template + inline CUresult AddParam(T&& param, Args&&... args) { + CUresult result(AddParam(std::forward(param))); + if (result != CUDA_SUCCESS) + return result; + if ((result=AddParam(std::forward(args)...)) != CUDA_SUCCESS) + return result; + return CUDA_SUCCESS; + } + #endif // _SUPPORT_CPP11 +}; +typedef CSharedPtr KernelRTPtr; +/*----------------------------------------------------------------*/ + + +namespace ARRAY { +template struct traits { static const CUarray_format format; }; +template<> struct traits { static const CUarray_format format = CU_AD_FORMAT_UNSIGNED_INT8; }; +template<> struct traits { static const CUarray_format format = CU_AD_FORMAT_UNSIGNED_INT16; }; +template<> struct traits { static const CUarray_format format = CU_AD_FORMAT_UNSIGNED_INT32; }; +template<> struct traits { static const CUarray_format format = CU_AD_FORMAT_SIGNED_INT8; }; +template<> struct traits { static const CUarray_format format = CU_AD_FORMAT_SIGNED_INT16; }; +template<> struct traits { static const CUarray_format format = CU_AD_FORMAT_SIGNED_INT32; }; +template<> struct traits { static const CUarray_format format = CU_AD_FORMAT_HALF; }; +template<> struct traits { static const CUarray_format format = CU_AD_FORMAT_FLOAT; }; +} // namespace ARRAY + +template +class TArrayRT +{ +public: + typedef TYPE Type; + typedef TImage ImageType; + +protected: + CUarray hArray; + +public: + inline TArrayRT() : hArray(NULL) {} + inline TArrayRT(const Image8U::Size& size, unsigned flags=0) : hArray(NULL) { reportCudaError(Reset(size, flags)); } + inline TArrayRT(unsigned width, unsigned height, unsigned depth=0, unsigned flags=0) : hArray(NULL) { reportCudaError(Reset(width, height, depth, flags)); } + inline ~TArrayRT() { Release(); } + + TArrayRT(TArrayRT& rhs) : hArray(rhs.hArray) { rhs.hArray = NULL; } + TArrayRT& operator=(TArrayRT& rhs) { + hArray = rhs.hArray; + rhs.hArray = NULL; + return *this; + } + + inline bool IsValid() const { + return (hArray != NULL); + } + void Release() { + if (hArray) { + reportCudaError(cuArrayDestroy(hArray)); + hArray = NULL; + } + } + inline CUresult Reset(const Image8U::Size& size, unsigned flags=0) { + return Reset((unsigned)size.width, (unsigned)size.height, 0, flags); + } + CUresult Reset(unsigned width, unsigned height, unsigned depth=0, unsigned flags=0) { + Release(); + CUDA_ARRAY3D_DESCRIPTOR prop; + prop.Width = width; + prop.Height = height; + prop.Depth = depth; + prop.Format = ARRAY::traits::format; + prop.NumChannels = cv::DataType::channels; + prop.Flags = flags; + CUresult ret(cuArray3DCreate(&hArray, &prop)); + if (ret != CUDA_SUCCESS) + hArray = NULL; + return ret; + } + + operator CUarray() const { + return hArray; + } + operator CUarray&() { + return hArray; + } + CUDA_ARRAY3D_DESCRIPTOR GetDescriptor() const { + CUDA_ARRAY3D_DESCRIPTOR prop; + cuArray3DGetDescriptor(&prop, hArray); + return prop; + } + unsigned Width() const { + return (unsigned)GetDescriptor().Width; + } + unsigned Height() const { + return (unsigned)GetDescriptor().Height; + } + unsigned Depth() const { + return (unsigned)GetDescriptor().Depth; + } + unsigned NumChannels() const { + return GetDescriptor().NumChannels; + } + CUarray_format Format() const { + return GetDescriptor().Format; + } + unsigned Flags() const { + return GetDescriptor().Flags; + } + size_t Size() const { + return sizeof(Type)*Width()*Height()*(Depth()>0?Depth():1)*NumChannels(); + } + + // copy some data from host memory to device memory + CUresult SetData(const ImageType& image) { + ASSERT(IsValid() && !image.empty()); + CUDA_MEMCPY2D param; + memset(¶m, 0, sizeof(CUDA_MEMCPY2D)); + param.dstMemoryType = CU_MEMORYTYPE_ARRAY; + param.dstArray = hArray; + param.srcMemoryType = CU_MEMORYTYPE_HOST; + param.srcHost = image.getData(); + param.srcPitch = image.row_stride(); + param.WidthInBytes = image.row_stride(); + param.Height = image.height(); + return cuMemcpy2D(¶m); + } + + // copy data from device memory to host memory + CUresult GetData(ImageType& image) const { + ASSERT(IsValid() && !image.empty()); + CUDA_MEMCPY2D param; + memset(¶m, 0, sizeof(CUDA_MEMCPY2D)); + param.dstMemoryType = CU_MEMORYTYPE_HOST; + param.dstHost = image.getData(); + param.dstPitch = image.row_stride(); + param.srcMemoryType = CU_MEMORYTYPE_ARRAY; + param.srcArray = hArray; + param.WidthInBytes = image.row_stride(); + param.Height = image.height(); + return cuMemcpy2D(¶m); + } +}; +typedef TArrayRT ArrayRT8U; +typedef TArrayRT ArrayRT32U; +typedef TArrayRT ArrayRT16F; +typedef TArrayRT ArrayRT32F; +/*----------------------------------------------------------------*/ + + +template +class TTextureRT +{ +public: + typedef TArrayRT ArrayType; + typedef typename ArrayType::Type Type; + typedef typename ArrayType::ImageType ImageType; + +public: + ModuleRTPtr ptrModule; + CUtexref hTexref; + +public: + inline TTextureRT() : hTexref(NULL) {} + inline TTextureRT(const ModuleRTPtr& _ptrModule, LPCSTR texrefName, CUfilter_mode filtermode=CU_TR_FILTER_MODE_POINT, CUaddress_mode addrmode=CU_TR_ADDRESS_MODE_CLAMP, bool bNormalizedCoords=false) : ptrModule(_ptrModule) { Reset(texrefName, filtermode, addrmode, bNormalizedCoords); } + inline ~TTextureRT() { Release(); } + + inline bool IsValid() const { + ASSERT(hTexref == NULL || (ptrModule != NULL && ptrModule->IsValid())); + return (hTexref != NULL); + } + void Release() { + ptrModule.Release(); + hTexref = NULL; + } + CUresult Reset(LPCSTR texrefName, CUfilter_mode filtermode=CU_TR_FILTER_MODE_POINT, CUaddress_mode addrmode=CU_TR_ADDRESS_MODE_CLAMP, bool bNormalizedCoords=false) { + // get the texture-reference handle (Driver API) + ASSERT(ptrModule != NULL && ptrModule->IsValid()); + CUresult result(cuModuleGetTexRef(&hTexref, *ptrModule, texrefName)); + if (result != CUDA_SUCCESS) + Release(); + // set texture parameters + checkCudaError(cuTexRefSetFilterMode(hTexref, filtermode)); + if (bNormalizedCoords) { + checkCudaError(cuTexRefSetFlags(hTexref, CU_TRSF_NORMALIZED_COORDINATES)); + for (int i=0; i<2; ++i) + checkCudaError(cuTexRefSetAddressMode(hTexref, i, addrmode)); + } else { + for (int i=0; i<2; ++i) + checkCudaError(cuTexRefSetAddressMode(hTexref, i, CU_TR_ADDRESS_MODE_CLAMP)); + } + cuTexRefSetFormat(hTexref, ARRAY::traits::format, cv::DataType::channels); + return result; + } + inline CUresult Reset(const ModuleRTPtr& _ptrModule, LPCSTR texrefName, CUfilter_mode filtermode=CU_TR_FILTER_MODE_POINT, CUaddress_mode addrmode=CU_TR_ADDRESS_MODE_CLAMP, bool bNormalizedCoords=false) { + // set module + ptrModule = _ptrModule; + // set texture + return Reset(texrefName, filtermode, addrmode, bNormalizedCoords); + } + + // bind the given array to the texture + CUresult Bind(ArrayType& array) { + return cuTexRefSetArray(hTexref, array, CU_TRSA_OVERRIDE_FORMAT); + } + + // fetch the array bind to the texture + CUresult Fetch(ArrayType& array) { + return cuTexRefGetArray(hTexref, array); + } +}; +typedef TTextureRT TextureRT8U; +typedef TTextureRT TextureRT32U; +typedef TTextureRT TextureRT16F; +typedef TTextureRT TextureRT32F; +/*----------------------------------------------------------------*/ + + +template +class TSurfaceRT +{ +public: + typedef TArrayRT ArrayType; + typedef typename ArrayType::Type Type; + typedef typename ArrayType::ImageType ImageType; + +public: + ModuleRTPtr ptrModule; + CUsurfref hSurfref; + +public: + inline TSurfaceRT() : hSurfref(NULL) {} + inline TSurfaceRT(const ModuleRTPtr& _ptrModule, LPCSTR surfrefName) : ptrModule(_ptrModule) { Reset(surfrefName); } + inline ~TSurfaceRT() { Release(); } + + inline bool IsValid() const { + ASSERT(hSurfref == NULL || (ptrModule != NULL && ptrModule->IsValid())); + return (hSurfref != NULL); + } + void Release() { + ptrModule.Release(); + hSurfref = NULL; + } + CUresult Reset(LPCSTR surfrefName) { + // get the surface-reference handle (Driver API) + ASSERT(ptrModule != NULL && ptrModule->IsValid()); + const CUresult result(cuModuleGetSurfRef(&hSurfref, *ptrModule, surfrefName)); + if (result != CUDA_SUCCESS) + Release(); + return result; + } + inline CUresult Reset(const ModuleRTPtr& _ptrModule, LPCSTR texrefName) { + // set module + ptrModule = _ptrModule; + // set texture + return Reset(texrefName); + } + + // bind the given array to the surface + CUresult Bind(const ArrayType& array) { + return cuSurfRefSetArray(hSurfref, array, 0); + } + + // fetch the array bind to the surface + CUresult Fetch(const ArrayType& array) const { + return cuSurfRefGetArray(hSurfref, array); + } +}; +typedef TSurfaceRT SurfaceRT8U; +typedef TSurfaceRT SurfaceRT32U; +typedef TSurfaceRT SurfaceRT16F; +typedef TSurfaceRT SurfaceRT32F; +/*----------------------------------------------------------------*/ + +} // namespace CUDA + +} // namespace SEACAVE + +#endif // _USE_CUDA + +#endif // __SEACAVE_CUDA_H__ diff --git a/libs/IO/CMakeLists.txt b/libs/IO/CMakeLists.txt new file mode 100644 index 0000000..0f05951 --- /dev/null +++ b/libs/IO/CMakeLists.txt @@ -0,0 +1,51 @@ +# Find required packages +FIND_PACKAGE(PNG QUIET) +if(PNG_FOUND) + INCLUDE_DIRECTORIES(${PNG_INCLUDE_DIRS}) + ADD_DEFINITIONS(${PNG_DEFINITIONS}) + SET(_USE_PNG TRUE CACHE INTERNAL "") +else() + SET(PNG_LIBRARIES "") +endif() +FIND_PACKAGE(JPEG QUIET) +if(JPEG_FOUND) + INCLUDE_DIRECTORIES(${JPEG_INCLUDE_DIR}) + ADD_DEFINITIONS(${JPEG_DEFINITIONS}) + SET(_USE_JPG TRUE CACHE INTERNAL "") +else() + SET(JPEG_LIBRARIES "") +endif() +FIND_PACKAGE(TIFF QUIET) +if(TIFF_FOUND) + INCLUDE_DIRECTORIES(${TIFF_INCLUDE_DIR}) + ADD_DEFINITIONS(${TIFF_DEFINITIONS}) + SET(_USE_TIFF TRUE CACHE INTERNAL "") +else() + SET(TIFF_LIBRARIES "") +endif() + +# List sources files +FILE(GLOB LIBRARY_FILES_C "*.cpp") +FILE(GLOB LIBRARY_FILES_H "*.h" "*.inl") + +cxx_library_with_type(IO "Libs" "" "${cxx_default}" + ${LIBRARY_FILES_C} ${LIBRARY_FILES_H} +) + +# Manually set Common.h as the precompiled header +IF(CMAKE_VERSION VERSION_GREATER_EQUAL 3.16.0) + TARGET_PRECOMPILE_HEADERS(IO PRIVATE "Common.h") +endif() + +# Link its dependencies +TARGET_LINK_LIBRARIES(IO Common ${PNG_LIBRARIES} ${JPEG_LIBRARIES} ${TIFF_LIBRARIES} ${EXIV2_LIBS}) + +# Install +SET_TARGET_PROPERTIES(IO PROPERTIES + PUBLIC_HEADER "${LIBRARY_FILES_H}") +INSTALL(TARGETS IO + EXPORT OpenMVSTargets + LIBRARY DESTINATION "${INSTALL_LIB_DIR}" + ARCHIVE DESTINATION "${INSTALL_LIB_DIR}" + RUNTIME DESTINATION "${INSTALL_BIN_DIR}" + PUBLIC_HEADER DESTINATION "${INSTALL_INCLUDE_DIR}/IO") diff --git a/libs/IO/Common.cpp b/libs/IO/Common.cpp new file mode 100644 index 0000000..b08fa42 --- /dev/null +++ b/libs/IO/Common.cpp @@ -0,0 +1,12 @@ +//////////////////////////////////////////////////////////////////// +// Common.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +// Source file that includes just the standard includes +// Common.pch will be the pre-compiled header +// Common.obj will contain the pre-compiled type information + +#include "Common.h" diff --git a/libs/IO/Common.h b/libs/IO/Common.h new file mode 100644 index 0000000..e43aaac --- /dev/null +++ b/libs/IO/Common.h @@ -0,0 +1,63 @@ +//////////////////////////////////////////////////////////////////// +// Common.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __IO_COMMON_H__ +#define __IO_COMMON_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#if defined(IO_EXPORTS) && !defined(Common_EXPORTS) +#define Common_EXPORTS +#endif + +#include "../Common/Common.h" + +#ifndef IO_API +#define IO_API GENERAL_API +#endif +#ifndef IO_TPL +#define IO_TPL GENERAL_TPL +#endif + +#define _IMAGE_BMP // add BMP support +#define _IMAGE_TGA // add TGA support +#define _IMAGE_DDS // add DDS support +#ifdef _USE_PNG +#define _IMAGE_PNG // add PNG support +#endif +#ifdef _USE_JPG +#define _IMAGE_JPG // add JPG support +#endif +#ifdef _USE_TIFF +#define _IMAGE_TIFF // add TIFF support +#endif + +#include "ImageSCI.h" +#ifdef _IMAGE_BMP +#include "ImageBMP.h" +#endif +#ifdef _IMAGE_TGA +#include "ImageTGA.h" +#endif +#ifdef _IMAGE_DDS +#include "ImageDDS.h" +#endif +#ifdef _IMAGE_PNG +#include "ImagePNG.h" +#endif +#ifdef _IMAGE_JPG +#include "ImageJPG.h" +#endif +#ifdef _IMAGE_TIFF +#include "ImageTIFF.h" +#endif +#include "PLY.h" +#include "OBJ.h" +/*----------------------------------------------------------------*/ + +#endif // __IO_COMMON_H__ diff --git a/libs/IO/Image.cpp b/libs/IO/Image.cpp new file mode 100644 index 0000000..3d3e1ce --- /dev/null +++ b/libs/IO/Image.cpp @@ -0,0 +1,935 @@ +//////////////////////////////////////////////////////////////////// +// Image.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" +#include "Image.h" + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +DEFINE_LOG(CImage, _T("IO ")); + +// Set the image details; +// if image's data is not NULL, but its size is too small, +// data's buffer is not allocated and _BUFFERSIZE is returned. +HRESULT CImage::Reset(Size width, Size height, PIXELFORMAT pixFormat, Size levels, bool bAllocate) +{ + // reinitialize image with given params + const size_t oldDataSize = GetDataSize(); + m_dataWidth = m_width = width; + m_dataHeight= m_height = height; + m_format = pixFormat; + m_stride = GetStride(pixFormat); + m_lineWidth = m_width * m_stride; + m_numLevels = levels; + m_level = 0; + if (bAllocate) { + if (m_data != NULL) { + if (oldDataSize < GetDataSize()) + return _BUFFERSIZE; + } else { + m_data = new uint8_t[GetDataSize()]; + } + } + return _OK; +} // Reset +/*----------------------------------------------------------------*/ + +HRESULT CImage::Reset(LPCTSTR szFileName, IMCREATE mode) +{ + // open the new image stream + m_fileName = szFileName; + // try to directly access the file + File* f; + if (mode == READ) { + f = new File(szFileName, File::READ, File::OPEN); + } else { + Util::ensureFolder(szFileName); + f = new File(szFileName, File::WRITE, File::CREATE | File::TRUNCATE); + } + if (!f->isOpen()) { + delete f; + return _INVALIDFILE; + } + if ((m_pStream = f) == NULL) { + LOG(LT_IMAGE, _T("error: failed opening image '%s'"), szFileName); + return _INVALIDFILE; + } + return _OK; +} // Reset +/*----------------------------------------------------------------*/ + +HRESULT CImage::Reset(IOSTREAMPTR& pStream) +{ + // use the already opened image stream + m_fileName.clear(); + m_pStream = pStream; + return _OK; +} // Reset +/*----------------------------------------------------------------*/ + +void CImage::Close() +{ + // close the image stream + m_pStream.Release(); + m_data.Release(); +} // Close +/*----------------------------------------------------------------*/ + + +HRESULT CImage::ReadHeader() +{ + return _OK; +} // ReadHeader +/*----------------------------------------------------------------*/ + +HRESULT CImage::ReadData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + // read data + if (dataFormat == m_format && nStride == m_stride) { + // read image directly to the data buffer + if (lineWidth == m_lineWidth) { + const size_t nSize = m_dataHeight*m_lineWidth; + if (nSize != m_pStream->read(pData, nSize)) + return _INVALIDFILE; + } else { + for (Size j=0; jread(pData, m_lineWidth)) + return _INVALIDFILE; + } + } else { + // read image to a buffer and convert it + CAutoPtrArr const buffer(new uint8_t[m_lineWidth]); + for (Size j=0; jread(buffer, m_lineWidth)) + return _INVALIDFILE; + if (!FilterFormat(pData, dataFormat, nStride, buffer, m_format, m_stride, m_dataWidth)) + return _FAIL; + } + } + // prepare next level + if (m_level+1 < m_numLevels) + m_lineWidth = GetDataSizes(++m_level, m_dataWidth, m_dataHeight); + return _OK; +} // ReadData +/*----------------------------------------------------------------*/ + + +HRESULT CImage::WriteHeader(PIXELFORMAT imageFormat, Size width, Size height, BYTE numLevels) +{ + // write header + m_numLevels = numLevels; + m_level = 0; + m_format = imageFormat; + m_stride = GetStride(m_format); + m_width = width; + m_height = height; + m_lineWidth = GetDataSizes(0, m_dataWidth, m_dataHeight); + return _OK; +} // WriteHeader +/*----------------------------------------------------------------*/ + +HRESULT CImage::WriteData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + // write data + if (dataFormat == m_format && nStride == m_stride) { + // write data buffer directly to the image + if (lineWidth == m_lineWidth) { + const size_t nSize = m_dataHeight*m_lineWidth; + if (nSize != m_pStream->write(pData, nSize)) + return _INVALIDFILE; + } else { + for (Size j=0; jwrite(pData, m_lineWidth)) + return _INVALIDFILE; + } + } else { + // convert data to a buffer and write it + CAutoPtrArr const buffer(new uint8_t[m_lineWidth]); + for (Size j=0; jwrite(buffer, m_lineWidth)) + return _INVALIDFILE; + } + } + // prepare next level + if (m_level+1 < m_numLevels) + m_lineWidth = GetDataSizes(++m_level, m_dataWidth, m_dataHeight); + return _OK; +} // WriteData +/*----------------------------------------------------------------*/ + + +CImage::Size CImage::GetDataSizes(Size mipLevel, Size& width, Size& height) const +{ + // Bump by the mip level. + height = MAXF((Size)1, m_height >> mipLevel); + width = MAXF((Size)1, m_width >> mipLevel); + + // if compressed, divide dims by 4 + if (m_format >= PF_DXT1) + { + width = (width+3)>>2; + height = (height+3)>>2; + } + + return width * m_stride; +} // GetDataSizes +/*----------------------------------------------------------------*/ + + +CImage::Size CImage::GetStride(PIXELFORMAT pixFormat) +{ + switch (pixFormat) + { + case PF_A8: + case PF_GRAY8: + return 1; + case PF_R5G6B5: + return 2; + case PF_B8G8R8: + case PF_R8G8B8: + return 3; + case PF_R8G8B8A8: + case PF_A8R8G8B8: + case PF_B8G8R8A8: + case PF_A8B8G8R8: + return 4; + case PF_DXT1: + return 8; + case PF_DXT2: + case PF_DXT3: + case PF_DXT4: + case PF_DXT5: + case PF_3DC: + return 16; + default: + LOG(LT_IMAGE, "error: unsupported SCI pixel format"); + } + return 0; +} // GetStride +/*----------------------------------------------------------------*/ + + +// If the given image format has alpha channel, returns true. +bool CImage::FormatHasAlpha(PIXELFORMAT format) +{ + switch (format) + { + case PF_A8: + case PF_R8G8B8A8: + case PF_A8R8G8B8: + case PF_B8G8R8A8: + case PF_A8B8G8R8: + case PF_DXT2: + case PF_DXT3: + case PF_DXT4: + case PF_DXT5: + return true; + case PF_GRAY8: + case PF_R5G6B5: + case PF_B8G8R8: + case PF_R8G8B8: + case PF_DXT1: + case PF_3DC: + return false; + default: + ASSERT("Unknown format" == NULL); + } + return false; +} // FormatHasAlpha +/*----------------------------------------------------------------*/ + + +// Convert from one format to another. +// nSzize is the number of pixels to process. +bool CImage::FilterFormat(void* pDst, PIXELFORMAT formatDst, Size strideDst, const void* pSrc, PIXELFORMAT formatSrc, Size strideSrc, Size nSzize) +{ + //ASSERT(formatDst != formatSrc || strideDst != strideSrc) + switch (formatDst) + { + case PF_A8: + case PF_GRAY8: + switch (formatSrc) + { + case PF_R8G8B8A8: + case PF_A8R8G8B8: + case PF_B8G8R8A8: + case PF_A8B8G8R8: + // from PF_R8G8B8A8 to PF_A8 (just copy the alpha channel) + (uint8_t*&)pSrc += 3; //skip the first RGB values + case PF_A8: + case PF_GRAY8: + // from PF_A8 to PF_A8 (just copy) + ASSERT(strideDst != strideSrc); + for (Size i=0; iReset(szName, mode))) { + delete pImage; + return NULL; + } + + return pImage; + +UNKNOWN_FORMAT: + // Unknown format + LOG(LT_IMAGE, "error: unknown image format '%s'", szName); + return NULL; +} // Create +/*----------------------------------------------------------------*/ + + +#ifndef _RELEASE + +// Save image as raw data. +void CImage::Dump(LPCTSTR szFileName) +{ + const String strName = Util::getFileName(szFileName)+String::ToString(m_width)+_T("x")+String::ToString(m_height)+_T("x")+String::ToString(m_stride)+Util::getFileExt(szFileName); + File f(strName, File::WRITE, File::CREATE | File::TRUNCATE); + if (!f.isOpen()) + return; + f.write(GetData(), GetDataSize()); +} // FlipRB24 +/*----------------------------------------------------------------*/ + +#endif diff --git a/libs/IO/Image.h b/libs/IO/Image.h new file mode 100644 index 0000000..44acd56 --- /dev/null +++ b/libs/IO/Image.h @@ -0,0 +1,128 @@ +//////////////////////////////////////////////////////////////////// +// Image.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_IMAGE_H__ +#define __SEACAVE_IMAGE_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + +#define LT_IMAGE CImage::ms_nLogType + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +// All formats are listed from left to right, most-significant bit to least-significant bit. +// For example, PF_A8R8G8B8 is ordered from the most-significant bit channel A (alpha), +// to the least-significant bit channel B (blue). When traversing image data, the data is +// stored in memory on a little-endian machine from least-significant bit to most-significant bit, +// which means that the channel order in memory is from least-significant bit (blue) to +// most-significant bit (alpha). +typedef enum PIXELFORMAT_TYPE { + PF_UNKNOWN = 0, + // gray + PF_A8, + PF_GRAY8, + // uncompressed RGB + PF_R5G6B5, + PF_R8G8B8, + PF_R8G8B8A8, + PF_A8R8G8B8, + // uncompressed BGR + PF_B8G8R8, + PF_B8G8R8A8, + PF_A8B8G8R8, + // compressed + PF_DXT1 = 128, + PF_DXT2, + PF_DXT3, + PF_DXT4, + PF_DXT5, + PF_3DC, +} PIXELFORMAT; + +class IO_API CImage +{ + DECLARE_LOG(); + +public: + enum IMCREATE { + READ, + WRITE + }; + typedef uint32_t Size; + + CImage() {} + virtual ~CImage() {} + + virtual HRESULT Reset(Size width, Size height, PIXELFORMAT pixFormat, Size levels = 1, bool bAllocate = false); + virtual HRESULT Reset(LPCTSTR szFileName, IMCREATE mode); + virtual HRESULT Reset(IOSTREAMPTR& pStream); + virtual void Close(); + + virtual HRESULT ReadHeader(); + virtual HRESULT ReadData(void*, PIXELFORMAT, Size nStride, Size lineWidth); + + virtual HRESULT WriteHeader(PIXELFORMAT, Size width, Size height, BYTE numLevels); + virtual HRESULT WriteData(void*, PIXELFORMAT, Size nStride, Size lineWidth); + + const IOSTREAMPTR& GetStream() const { return m_pStream; } + IOSTREAMPTR& GetStream() { return m_pStream; } + BYTE* GetData() const { return m_data; } + BYTE*& GetData() { return m_data; } + size_t GetDataSize() const { return m_lineWidth * m_dataHeight; } + Size GetWidth() const { return m_width; } + Size GetHeight() const { return m_height; } + Size GetDataWidth() const { return m_dataWidth; } + Size GetDataHeight() const { return m_dataHeight; } + Size GetStride() const { return m_stride; } + Size GetLineWidth() const { return m_lineWidth; } + BYTE GetNumLevels() const { return m_numLevels; } + PIXELFORMAT GetFormat() const { return m_format; } + bool FormatHasAlpha() const { return FormatHasAlpha(m_format); } + const String& GetFileName() const { return m_fileName; } + String& GetFileName() { return m_fileName; } + + Size GetDataSizes(Size mipLevel, Size& width, Size& height) const; + + static Size GetStride(PIXELFORMAT); //in bits + static bool FormatHasAlpha(PIXELFORMAT); + static bool FilterFormat(void*, PIXELFORMAT, Size, const void*, PIXELFORMAT, Size, Size nSzize); + static void FlipRB24(uint8_t* data, Size size, Size stride); + static void CopyFlipRB24(uint8_t* pDst, const uint8_t* pSrc, Size size, Size strideDst, Size strideSrc); + + static CImage* Create(LPCTSTR szName, IMCREATE mode); + + #ifndef _RELEASE + void Dump(LPCTSTR szFileName); + #endif + +protected: + IOSTREAMPTR m_pStream; // stream used to read/write the image data + CAutoPtrArr m_data; // image's data buffer + Size m_width; // image width in pixels + Size m_height; // image height in pixels + Size m_dataWidth; // image's data width including mipmaps + Size m_dataHeight; // image's data height + Size m_stride; // bytes per pixel + Size m_lineWidth; // image canvas width in bytes + PIXELFORMAT m_format; // image format (pixel type) + BYTE m_numLevels; // number of mipmap levels (0 = auto-generate) + BYTE m_level; // index of the mipmap level currently reading + String m_fileName; // image's file name +}; // class CImage +typedef CSharedPtr IMAGEPTR; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_IMAGE_H__ diff --git a/libs/IO/ImageBMP.cpp b/libs/IO/ImageBMP.cpp new file mode 100644 index 0000000..e67c205 --- /dev/null +++ b/libs/IO/ImageBMP.cpp @@ -0,0 +1,261 @@ +//////////////////////////////////////////////////////////////////// +// ImageBMP.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" + +#ifdef _IMAGE_BMP +#include "ImageBMP.h" + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + + +// S T R U C T S /////////////////////////////////////////////////// + +#ifndef _MSC_VER +typedef long LONG; +typedef struct tagBITMAPFILEHEADER { + WORD bfType; + DWORD bfSize; + WORD bfReserved1; + WORD bfReserved2; + DWORD bfOffBits; +} BITMAPFILEHEADER; +typedef struct tagBITMAPINFOHEADER { + DWORD biSize; + LONG biWidth; + LONG biHeight; + WORD biPlanes; + WORD biBitCount; + DWORD biCompression; + DWORD biSizeImage; + LONG biXPelsPerMeter; + LONG biYPelsPerMeter; + DWORD biClrUsed; + DWORD biClrImportant; +} BITMAPINFOHEADER; +/* constants for the biCompression field */ +#define BI_RGB 0L +#define BI_RLE8 1L +#define BI_RLE4 2L +#define BI_BITFIELDS 3L +#define BI_JPEG 4L +#define BI_PNG 5L +#endif + + +CImageBMP::CImageBMP() +{ +} // Constructor + +CImageBMP::~CImageBMP() +{ +} // Destructor +/*----------------------------------------------------------------*/ + + +HRESULT CImageBMP::ReadHeader() +{ + // Jump to the beginning of the file + ((ISTREAM*)m_pStream)->setPos(0); + + // Read the BITMAPFILEHEADER + // and check the type field to make sure we have a .bmp file + // bfType should contain the letters "BM" + BITMAPFILEHEADER bmp_fileheader; + if (sizeof(BITMAPFILEHEADER) != m_pStream->read(&bmp_fileheader, sizeof(BITMAPFILEHEADER)) || + memcmp(&bmp_fileheader.bfType, "BM", 2)) + { + LOG(LT_IMAGE, _T("error: invalid BMP image")); + return _INVALIDFILE; + } + + // Read the BITMAPINFOHEADER. + // and double Check to make sure we got a correct BITMAPINFOHEADER + BITMAPINFOHEADER bmp_infoheader; + if (sizeof(BITMAPINFOHEADER) != m_pStream->read(&bmp_infoheader, sizeof(BITMAPINFOHEADER)) || + bmp_infoheader.biSize != sizeof(bmp_infoheader)) + { + LOG(LT_IMAGE, _T("error: invalid BMP image")); + return _INVALIDFILE; + } + + // Check for unsupported format: biPlanes MUST equal 1 + // and we can only handle uncompressed .bmps + if (bmp_infoheader.biPlanes != 1 || + (bmp_infoheader.biCompression != BI_RGB && bmp_infoheader.biCompression != BI_BITFIELDS)) + { + LOG(LT_IMAGE, "error: unsupported BMP image"); + return _INVALIDFILE; + } + + // Initililize our width, height and format as the .bmp we are loading + m_dataWidth = m_width = bmp_infoheader.biWidth; + m_dataHeight= m_height = bmp_infoheader.biHeight; + m_numLevels = 0; + m_level = 0; + m_lineWidth = (bmp_fileheader.bfSize-bmp_fileheader.bfOffBits)/bmp_infoheader.biHeight; //bmp_infoheader.biSizeImage may be set to zero for BI_RGB bitmaps + m_stride = bmp_infoheader.biBitCount>>3; + switch (m_stride) + { + case 1: + m_format = PF_GRAY8; + break; + case 2: + m_format = PF_R5G6B5; + break; + case 3: + m_format = PF_R8G8B8; + break; + case 4: + m_format = PF_R8G8B8A8; + break; + default: + LOG(LT_IMAGE, "error: unsupported BMP image"); + return _INVALIDFILE; + } + // Ensure m_lineWidth is DWORD aligned + while ((m_lineWidth%4) != 0) ++m_lineWidth; + + // Jump to the location where the bitmap data is stored + ((ISTREAM*)m_pStream)->setPos(bmp_fileheader.bfOffBits); + + return _OK; +} // ReadHeader +/*----------------------------------------------------------------*/ + + +HRESULT CImageBMP::ReadData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + // read data + const size_t nSize = m_width*m_stride; + const size_t nPad = m_lineWidth-nSize; + CAutoPtrArr const bufferPad(nPad ? new uint8_t[nPad] : NULL); + if (dataFormat == m_format && nStride == m_stride) { + // read image directly to the data buffer + (BYTE*&)pData += (m_height-1)*lineWidth; + for (Size j=0; jread(pData, nSize) || + (nPad && nPad != m_pStream->read(bufferPad, nPad))) + return _INVALIDFILE; + } else { + // read image to a buffer and convert it + CAutoPtrArr const buffer(new uint8_t[m_lineWidth]); + for (Size j=0; jread(buffer, m_lineWidth)) + return _INVALIDFILE; + if (!FilterFormat((uint8_t*)pData+(m_height-j-1)*lineWidth, dataFormat, nStride, buffer, m_format, m_stride, m_width)) + return _FAIL; + } + } + return _OK; +} // ReadData +/*----------------------------------------------------------------*/ + + +HRESULT CImageBMP::WriteHeader(PIXELFORMAT imageFormat, Size width, Size height, BYTE /*numLevels*/) +{ + // write header + m_numLevels = 0; + m_level = 0; + DWORD dwCompression; + switch (imageFormat) + { + case PF_A8: + case PF_GRAY8: + m_stride = 1; + dwCompression = BI_RGB; + m_format = PF_GRAY8; + break; + case PF_R5G6B5: + m_stride = 2; + dwCompression = BI_BITFIELDS; + m_format = PF_R5G6B5; + break; + case PF_B8G8R8: + case PF_R8G8B8: + m_stride = 3; + dwCompression = BI_RGB; + m_format = PF_R8G8B8; + break; + case PF_R8G8B8A8: + case PF_A8R8G8B8: + case PF_B8G8R8A8: + case PF_A8B8G8R8: + m_stride = 4; + dwCompression = BI_BITFIELDS; + m_format = PF_R8G8B8A8; + break; + default: + LOG(LT_IMAGE, "error: unsupported BMP image format"); + return _INVALIDFILE; + } + m_dataWidth = m_width = width; + m_dataHeight= m_height = height; + m_lineWidth = m_width*m_stride; + // Ensure m_lineWidth is DWORD aligned + while ((m_lineWidth%4) != 0) ++m_lineWidth; + ((OSTREAM*)m_pStream)->setPos(0); + // Write the BITMAPFILEHEADER and the BITMAPINFOHEADER. + const BITMAPFILEHEADER bmp_fileheader = { + 0x4d42, //BM + (DWORD)sizeof(BITMAPFILEHEADER)+(DWORD)sizeof(BITMAPINFOHEADER)+m_lineWidth*m_height, //total file size + 0, 0, //reserved + (DWORD)sizeof(BITMAPFILEHEADER)+(DWORD)sizeof(BITMAPINFOHEADER) //offset to the bitmap bits + }; + const BITMAPINFOHEADER bmp_infoheader = { + (DWORD)sizeof(BITMAPINFOHEADER), //size of this structure + (LONG)m_width, (LONG)m_height, //dim + (WORD)1, //number of color planes being used (must be set to 1) + (WORD)(m_stride<<3), //number of bits per pixel + dwCompression, //compression method being used + m_lineWidth*m_height, //raw image size + 0, 0, //pixels-per-meter + 0, //number of colors in the color palette + 0 //number of important colors used + }; + if (sizeof(BITMAPFILEHEADER) != m_pStream->write(&bmp_fileheader, sizeof(BITMAPFILEHEADER)) || + sizeof(BITMAPINFOHEADER) != m_pStream->write(&bmp_infoheader, sizeof(BITMAPINFOHEADER))) + { + LOG(LT_IMAGE, "error: failed writing the BMP image"); + return _INVALIDFILE; + } + return _OK; +} // WriteHeader +/*----------------------------------------------------------------*/ + + +HRESULT CImageBMP::WriteData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + // write data + const size_t nSize = m_width*m_stride; + const size_t nPad = m_lineWidth-nSize; + CAutoPtrArr const bufferPad(nPad ? new uint8_t[nPad] : NULL); + if (nPad) memset(bufferPad, 0, nPad); + if (dataFormat == m_format && nStride == m_stride) { + // write data buffer directly to the image + for (Size j=0; jwrite((uint8_t*)pData+(m_height-j-1)*lineWidth, nSize) || + (nPad && nPad != m_pStream->write(bufferPad, nPad))) + return _INVALIDFILE; + } else { + // convert data to a buffer and write it + CAutoPtrArr const buffer(new uint8_t[m_lineWidth]); + for (Size j=0; jwrite(buffer, m_lineWidth)) + return _INVALIDFILE; + } + } + return _OK; +} // WriteData +/*----------------------------------------------------------------*/ + +#endif // _IMAGE_BMP diff --git a/libs/IO/ImageBMP.h b/libs/IO/ImageBMP.h new file mode 100644 index 0000000..43ae475 --- /dev/null +++ b/libs/IO/ImageBMP.h @@ -0,0 +1,36 @@ +//////////////////////////////////////////////////////////////////// +// ImageBMP.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_IMAGEBMP_H__ +#define __SEACAVE_IMAGEBMP_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Image.h" + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class IO_API CImageBMP : public CImage +{ +public: + CImageBMP(); + virtual ~CImageBMP(); + + HRESULT ReadHeader(); + HRESULT ReadData(void*, PIXELFORMAT, Size nStride, Size lineWidth); + HRESULT WriteHeader(PIXELFORMAT, Size width, Size height, BYTE numLevels); + HRESULT WriteData(void*, PIXELFORMAT, Size nStride, Size lineWidth); +}; // class CImageBMP +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_IMAGEBMP_H__ diff --git a/libs/IO/ImageDDS.cpp b/libs/IO/ImageDDS.cpp new file mode 100644 index 0000000..09e1e26 --- /dev/null +++ b/libs/IO/ImageDDS.cpp @@ -0,0 +1,341 @@ +//////////////////////////////////////////////////////////////////// +// ImageDDS.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" + +#ifdef _IMAGE_DDS +#include "ImageDDS.h" + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + +// These were copied from the DX9 docs. The names are changed +// from the "real" defines since not all platforms have them. + +#ifndef MakeFourCC +#define MakeFourCC(ch0, ch1, ch2, ch3) \ + ((uint32_t)(uint8_t)(ch0) | ((uint32_t)(uint8_t)(ch1) << 8) | \ + ((uint32_t)(uint8_t)(ch2) << 16) | ((uint32_t)(uint8_t)(ch3) << 24 )) +#endif + +#define IMAGE_DDS_TYPE MakeFourCC('D','D','S',' ') //"DDS " = 4 bytes + +#define FOURCC_DXT1 MakeFourCC('D','X','T','1') +#define FOURCC_DXT2 MakeFourCC('D','X','T','2') +#define FOURCC_DXT3 MakeFourCC('D','X','T','3') +#define FOURCC_DXT4 MakeFourCC('D','X','T','4') +#define FOURCC_DXT5 MakeFourCC('D','X','T','5') +#define FOURCC_3DC MakeFourCC('A','T','I','2') + +enum DDSSurfaceDescFlags +{ + DDSDCaps = 0x00000001l, + DDSDHeight = 0x00000002l, + DDSDWidth = 0x00000004l, + DDSDPitch = 0x00000008l, + DDSDPixelFormat = 0x00001000l, + DDSDMipMapCount = 0x00020000l, + DDSDLinearSize = 0x00080000l, + DDSDDepth = 0x00800000l, +}; + +enum DDSPixelFormatFlags +{ + DDPFAlphaPixels = 0x00000001, + DDPFFourCC = 0x00000004, + DDPFRGB = 0x00000040, +}; + +enum DDSCapFlags +{ + DDSCAPSComplex = 0x00000008, + DDSCAPSTexture = 0x00001000, + DDSCAPSMipMap = 0x00400000, + + DDSCAPS2Cubemap = 0x00000200, + DDSCAPS2Cubemap_POSITIVEX = 0x00000400, + DDSCAPS2Cubemap_NEGATIVEX = 0x00000800, + DDSCAPS2Cubemap_POSITIVEY = 0x00001000, + DDSCAPS2Cubemap_NEGATIVEY = 0x00002000, + DDSCAPS2Cubemap_POSITIVEZ = 0x00004000, + DDSCAPS2Cubemap_NEGATIVEZ = 0x00008000, + DDSCAPS2Volume = 0x00200000, +}; + +typedef struct DDPIXELFORMAT_TYPE +{ + DWORD dwSize; // size of structure + DWORD dwFlags; // pixel format flags + DWORD dwFourCC; // (FOURCC code) + union + { + DWORD dwRGBBitCount; // how many bits per pixel + DWORD dwYUVBitCount; // how many bits per pixel + DWORD dwZBufferBitDepth; // how many total bits/pixel in z buffer (including any stencil bits) + DWORD dwAlphaBitDepth; // how many bits for alpha channels + DWORD dwLuminanceBitCount; // how many bits per pixel + DWORD dwBumpBitCount; // how many bits per "buxel", total + DWORD dwPrivateFormatBitCount;// Bits per pixel of private driver formats. Only valid in texture + // format list and if DDPF_D3DFORMAT is set + }; + union + { + DWORD dwRBitMask; // mask for red bit + DWORD dwYBitMask; // mask for Y bits + DWORD dwStencilBitDepth; // how many stencil bits (note: dwZBufferBitDepth-dwStencilBitDepth is total Z-only bits) + DWORD dwLuminanceBitMask; // mask for luminance bits + DWORD dwBumpDuBitMask; // mask for bump map U delta bits + DWORD dwOperations; // DDPF_D3DFORMAT Operations + }; + union + { + DWORD dwGBitMask; // mask for green bits + DWORD dwUBitMask; // mask for U bits + DWORD dwZBitMask; // mask for Z bits + DWORD dwBumpDvBitMask; // mask for bump map V delta bits + struct + { + WORD wFlipMSTypes; // Multisample methods supported via flip for this D3DFORMAT + WORD wBltMSTypes; // Multisample methods supported via blt for this D3DFORMAT + }; + + }; + union + { + DWORD dwBBitMask; // mask for blue bits + DWORD dwVBitMask; // mask for V bits + DWORD dwStencilBitMask; // mask for stencil bits + DWORD dwBumpLuminanceBitMask; // mask for luminance in bump map + }; + union + { + DWORD dwRGBAlphaBitMask; // mask for alpha channel + DWORD dwYUVAlphaBitMask; // mask for alpha channel + DWORD dwLuminanceAlphaBitMask;// mask for alpha channel + DWORD dwRGBZBitMask; // mask for Z channel + DWORD dwYUVZBitMask; // mask for Z channel + }; +} DDSPF; + +typedef struct DDSCAPS_TYPE +{ + DWORD dwCaps; // capabilities of surface wanted + DWORD dwCaps2; + DWORD dwCaps3; + union + { + DWORD dwCaps4; + DWORD dwVolumeDepth; + }; +} DDSCAPS; + +typedef struct DDSINFOHEADER_TYPE { + DWORD dwHeader; // magic number "DDS " + DWORD dwSize; // size of the DDSURFACEDESC structure + DWORD dwFlags; // determines what fields are valid + DWORD dwHeight; // height of surface to be created + DWORD dwWidth; // width of input surface + DWORD dwPitchOrLinearSize; // distance to start of next line or formless late-allocated optimized surface size + DWORD dwDepth; // the depth if this is a volume texture + DWORD dwMipMapCount; // number of mip-map levels requestde + DWORD dwReserved1[11]; + DDSPF ddsPixelFormat; // pixel format description of the surface + DDSCAPS ddsCaps; // direct draw surface capabilities + DWORD dwReserved2; +} DDSINFOHEADER; + +#define IMAGE_DDS_DDSINFOHEADERSIZE (sizeof(DDSINFOHEADER)-sizeof(DWORD)/*dwHeader*/) // should be 124 + +#define ISBITMASK(r,g,b,a) (ddsInfo.ddsPixelFormat.dwRBitMask == r && ddsInfo.ddsPixelFormat.dwGBitMask == g && ddsInfo.ddsPixelFormat.dwBBitMask == b && ddsInfo.ddsPixelFormat.dwRGBAlphaBitMask == a ) + + + +// S T R U C T S /////////////////////////////////////////////////// + +CImageDDS::CImageDDS() +{ +} // Constructor + +CImageDDS::~CImageDDS() +{ +} // Destructor +/*----------------------------------------------------------------*/ + + +HRESULT CImageDDS::ReadHeader() +{ + // read header + ((ISTREAM*)m_pStream)->setPos(0); + DDSINFOHEADER ddsInfo; + m_pStream->read(&ddsInfo, sizeof(DDSINFOHEADER)); + if (ddsInfo.dwHeader != IMAGE_DDS_TYPE && + ddsInfo.dwSize != IMAGE_DDS_DDSINFOHEADERSIZE && + !(ddsInfo.dwFlags & (DDSDCaps | DDSDPixelFormat | DDSDWidth | DDSDHeight))) { // always include DDSD_CAPS, DDSD_PIXELFORMAT, DDSD_WIDTH, DDSD_HEIGHT + LOG(LT_IMAGE, "error: invalid DDS image"); + return _INVALIDFILE; + } + + m_width = ddsInfo.dwWidth; + m_height = ddsInfo.dwHeight; + m_numLevels = ddsInfo.dwFlags & DDSDMipMapCount ? (BYTE)ddsInfo.dwMipMapCount : 0; + m_level = 0; + + // get the format + m_stride = ddsInfo.ddsPixelFormat.dwRGBBitCount>>3; + m_format = PF_UNKNOWN; + if (ddsInfo.ddsPixelFormat.dwFlags & DDPFFourCC) + { + switch (ddsInfo.ddsPixelFormat.dwFourCC) + { + case FOURCC_DXT1: + m_format = PF_DXT1; + m_stride = 8; + break; + case FOURCC_DXT2: + m_format = PF_DXT2; + m_stride = 16; + break; + case FOURCC_DXT3: + m_format = PF_DXT3; + m_stride = 16; + break; + case FOURCC_DXT4: + m_format = PF_DXT4; + m_stride = 16; + break; + case FOURCC_DXT5: + m_format = PF_DXT5; + m_stride = 16; + break; + case FOURCC_3DC: + m_format = PF_3DC; + m_stride = 16; + break; + //case 0x74: + // m_format=FORMAT_R32G32B32A32F; + // break; + //case 0x71: + // m_format=FORMAT_R16G16B16A16F; + // break; + //case 0x70: + // m_format=FORMAT_G16R16F; + // break; + //case 0x73: + // m_format=FORMAT_G32R32F; + // break; + //case 0x6F: + // m_format=FORMAT_R16F; + // break; + //case 0x72: + // m_format=FORMAT_R32F; + // break; + } + } + else if (ddsInfo.ddsPixelFormat.dwFlags & DDPFRGB) + { + if (ddsInfo.ddsPixelFormat.dwFlags & DDPFAlphaPixels) + { + if (ISBITMASK(0xff, 0xff00, 0xff0000, 0xff000000)) + m_format = PF_B8G8R8A8; + else if (ISBITMASK(0xff0000, 0xff00, 0xff, 0xff000000)) + m_format = PF_R8G8B8A8; + //else if (ISBITMASK(0xff000000, 0xff0000, 0xff00, 0xff)) + // m_format = FORMAT_ABGR; + //else if (ISBITMASK(0x7C00, 0x3E0, 0x1F, 0x8000)) + // m_format = FORMAT_A1R5G5B5; + } + else //no DDPFAlphaPixels + { + if (ISBITMASK(0xff, 0xff00, 0xff0000, 0x00)) + m_format = PF_B8G8R8; + else if (ISBITMASK(0xff0000, 0xff00, 0xff, 0x00)) + m_format = PF_R8G8B8; + else if (ISBITMASK(0xF800, 0x7E0, 0x1F, 0x00)) + m_format = PF_R5G6B5; + //else if (ISBITMASK(0xffFF, 0xffFF0000, 0x00, 0x00)) + // m_format = FORMAT_G16R16; + //else if (ISBITMASK(0x7C00, 0x3E0, 0x1F, 0x00)) + // m_format = FORMAT_X1R5G5B5; + } + } + else //no DDPFRGB + { + if (ISBITMASK(0x00, 0x00, 0x00, 0xff)) + m_format = PF_A8; + else if (ISBITMASK(0xff, 0x00, 0x00, 0x00)) + m_format = PF_GRAY8; + //else if (ISBITMASK(0xffff, 0x00, 0x00, 0x00)) + // m_format = FORMAT_L16; + //else if (ISBITMASK(0xff, 0x00, 0x00, 0xff00)) + // m_format = FORMAT_A8L8; + //else if (ISBITMASK(0xff, 0xff00, 0x00, 0x00)) + // m_format = FORMAT_V8U8; + //else if (ISBITMASK(0xFF, 0xFF00, 0xFF0000, 0xFF000000)) + // m_format = FORMAT_Q8W8V8U8; + //else if (ISBITMASK(0xFFFF, 0xFFFF0000, 0x00, 0x00)) + // m_format = FORMAT_V16U16; + } + ASSERT(m_format != PF_UNKNOWN); + if (m_format == PF_UNKNOWN) + { + LOG(LT_IMAGE, "error: unsupported DDS image"); + return _INVALIDFILE; + } + + m_lineWidth = GetDataSizes(0, m_dataWidth, m_dataHeight); + return _OK; +} // ReadHeader +/*----------------------------------------------------------------*/ + + +HRESULT CImageDDS::ReadData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + // read data + if (dataFormat == m_format && nStride == m_stride) { + // read image directly to the data buffer + if (lineWidth == m_lineWidth) { + const size_t nSize = m_dataHeight*m_lineWidth; + if (nSize != m_pStream->read(pData, nSize)) + return _INVALIDFILE; + } else { + for (Size j=0; jread(pData, m_lineWidth)) + return _INVALIDFILE; + } + } else { + // read image to a buffer and convert it + CAutoPtrArr const buffer(new uint8_t[m_lineWidth]); + for (Size j=0; jread(buffer, m_lineWidth)) + return _INVALIDFILE; + if (!FilterFormat(pData, dataFormat, nStride, buffer, m_format, m_stride, m_dataWidth)) + return _FAIL; + } + } + // prepare next level + m_lineWidth = GetDataSizes(++m_level, m_dataWidth, m_dataHeight); + return _OK; +} // ReadData +/*----------------------------------------------------------------*/ + + +HRESULT CImageDDS::WriteHeader(PIXELFORMAT imageFormat, Size width, Size height, BYTE numLevels) +{ + return _FAIL; +} // WriteHeader +/*----------------------------------------------------------------*/ + + +HRESULT CImageDDS::WriteData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + return _FAIL; +} // WriteData +/*----------------------------------------------------------------*/ + +#endif // _IMAGE_DDS \ No newline at end of file diff --git a/libs/IO/ImageDDS.h b/libs/IO/ImageDDS.h new file mode 100644 index 0000000..268d963 --- /dev/null +++ b/libs/IO/ImageDDS.h @@ -0,0 +1,36 @@ +//////////////////////////////////////////////////////////////////// +// ImageDDS.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_IMAGEDDS_H__ +#define __SEACAVE_IMAGEDDS_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Image.h" + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class IO_API CImageDDS : public CImage +{ +public: + CImageDDS(); + virtual ~CImageDDS(); + + HRESULT ReadHeader(); + HRESULT ReadData(void*, PIXELFORMAT, Size nStride, Size lineWidth); + HRESULT WriteHeader(PIXELFORMAT, Size width, Size height, BYTE numLevels); + HRESULT WriteData(void*, PIXELFORMAT, Size nStride, Size lineWidth); +}; // class CImageDDS +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_IMAGEDDS_H__ diff --git a/libs/IO/ImageJPG.cpp b/libs/IO/ImageJPG.cpp new file mode 100644 index 0000000..7ed958e --- /dev/null +++ b/libs/IO/ImageJPG.cpp @@ -0,0 +1,281 @@ +//////////////////////////////////////////////////////////////////// +// ImageJPG.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" + +#ifdef _IMAGE_JPG +#include "ImageJPG.h" + +#ifdef _MSC_VER +#define XMD_H // prevent redefinition of INT32 +#undef FAR // prevent FAR redefinition +#endif + +extern "C" { +#include +} +#include + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define JPG_BUFFER_SIZE (16*1024) + +struct JpegErrorMgr +{ + struct jpeg_error_mgr pub; + jmp_buf setjmp_buffer; +}; + +struct JpegSource +{ + struct jpeg_source_mgr pub; + ISTREAM* pStream; + JOCTET* buffer; +}; + +struct JpegState +{ + jpeg_decompress_struct cinfo; // IJG JPEG codec structure + JpegErrorMgr jerr;// error processing manager state + JpegSource source;// memory buffer source +}; + + +// F U N C T I O N S /////////////////////////////////////////////// + +METHODDEF(void) +stub(j_decompress_ptr cinfo) +{ + JpegSource* source = (JpegSource*)cinfo->src; + source->pStream->setPos(0); +} + +METHODDEF(boolean) +fill_input_buffer(j_decompress_ptr cinfo) +{ + JpegSource* source = (JpegSource*)cinfo->src; + const size_t size = source->pStream->read(source->buffer, JPG_BUFFER_SIZE); + if (size == STREAM_ERROR || size == 0) + return FALSE; + source->pub.next_input_byte = source->buffer; + source->pub.bytes_in_buffer = size; + return TRUE; +} + +METHODDEF(void) +skip_input_data(j_decompress_ptr cinfo, long num_bytes) +{ + JpegSource* source = (JpegSource*)cinfo->src; + + if (num_bytes > (long)source->pub.bytes_in_buffer) + { + // We need to skip more data than we have in the buffer. + source->pStream->setPos(source->pStream->getPos() + (num_bytes - source->pub.bytes_in_buffer)); + source->pub.next_input_byte += source->pub.bytes_in_buffer; + source->pub.bytes_in_buffer = 0; + } + else + { + // Skip portion of the buffer + source->pub.bytes_in_buffer -= num_bytes; + source->pub.next_input_byte += num_bytes; + } +} + +METHODDEF(void) +error_exit(j_common_ptr cinfo) +{ + JpegErrorMgr* err_mgr = (JpegErrorMgr*)(cinfo->err); + + /* Return control to the setjmp point */ + longjmp( err_mgr->setjmp_buffer, 1 ); +} + + +// S T R U C T S /////////////////////////////////////////////////// + +CImageJPG::CImageJPG() : m_state(NULL) +{ +} // Constructor + +CImageJPG::~CImageJPG() +{ + //clean up + Close(); +} // Destructor +/*----------------------------------------------------------------*/ + +void CImageJPG::Close() +{ + if (m_state) + { + JpegState* state = (JpegState*)m_state; + jpeg_destroy_decompress( &state->cinfo ); + delete state; + m_state = NULL; + } + m_width = m_height = 0; + CImage::Close(); +} +/*----------------------------------------------------------------*/ + +HRESULT CImageJPG::ReadHeader() +{ + JpegState* state = new JpegState; + m_state = state; + state->cinfo.err = jpeg_std_error(&state->jerr.pub); + state->jerr.pub.error_exit = error_exit; + + if (setjmp(state->jerr.setjmp_buffer ) == 0) + { + jpeg_create_decompress( &state->cinfo ); + + // Prepare for suspending reader + state->source.pub.init_source = stub; + state->source.pub.fill_input_buffer = fill_input_buffer; + state->source.pub.skip_input_data = skip_input_data; + state->source.pub.resync_to_restart = jpeg_resync_to_restart; + state->source.pub.term_source = stub; + state->source.pub.bytes_in_buffer = 0;// forces fill_input_buffer on first read + state->source.pub.next_input_byte = NULL; + state->source.pStream = (IOSTREAM*)m_pStream; + state->source.buffer = (JOCTET*)(*state->cinfo.mem->alloc_small)((j_common_ptr)&state->cinfo, JPOOL_PERMANENT, JPG_BUFFER_SIZE * sizeof(JOCTET)); + state->cinfo.src = &state->source.pub; + + jpeg_read_header(&state->cinfo, TRUE); + + m_dataWidth = m_width = state->cinfo.image_width; + m_dataHeight= m_height = state->cinfo.image_height; + m_numLevels = 0; + m_level = 0; + m_stride = state->cinfo.num_components; + m_lineWidth = m_width * m_stride; + switch (m_stride) + { + case 1: + m_format = PF_GRAY8; + state->cinfo.out_color_space = JCS_GRAYSCALE; + state->cinfo.out_color_components = 1; + break; + case 3: + m_format = PF_B8G8R8; + state->cinfo.out_color_space = JCS_RGB; + state->cinfo.out_color_components = 3; + break; + default: + LOG(LT_IMAGE, "error: unsupported JPG image"); + return _INVALIDFILE; + } + return _OK; + } + + Close(); + return _FAIL; +} // ReadHeader +/*----------------------------------------------------------------*/ + +HRESULT CImageJPG::ReadData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + JpegState* state = (JpegState*)m_state; + + if (state && m_width && m_height) + { + jpeg_decompress_struct* cinfo = &state->cinfo; + JpegErrorMgr* jerr = &state->jerr; + + if (setjmp(jerr->setjmp_buffer) == 0) + { + jpeg_start_decompress(cinfo); + + // read data + if (dataFormat == m_format && nStride == m_stride) { + // read image directly to the data buffer + JSAMPLE* buffer[1] = {(JSAMPLE*)pData}; + uint8_t*& data = (uint8_t*&)buffer[0]; + for (Size j=0; jmem->alloc_sarray)((j_common_ptr)cinfo, JPOOL_IMAGE, m_lineWidth, 1); + uint8_t* dst = (uint8_t*)pData; + uint8_t* src = (uint8_t*)buffer[0]; + for (Size j=0; j +#include + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + + +// F U N C T I O N S /////////////////////////////////////////////// + +void custom_png_read_file(png_structp png_ptr, png_bytep buffer, png_size_t size) +{ + png_voidp const ptrIO = png_get_io_ptr(png_ptr); + ((InputStream*)ptrIO)->read(buffer, size); +} + +void custom_png_write_file(png_structp png_ptr, png_bytep buffer, png_size_t size) +{ + png_voidp const ptrIO = png_get_io_ptr(png_ptr); + ((IOStream*)ptrIO)->write(buffer, size); +} + +void custom_png_flush_file(png_structp png_ptr) +{ + png_voidp const ptrIO = png_get_io_ptr(png_ptr); + ((IOStream*)ptrIO)->flush(); +} + + + +// S T R U C T S /////////////////////////////////////////////////// + +CImagePNG::CImagePNG() : m_png_ptr(NULL), m_info_ptr(NULL) +{ +} // Constructor + +CImagePNG::~CImagePNG() +{ + Close(); +} // Destructor +/*----------------------------------------------------------------*/ + +void CImagePNG::Close() +{ + if (m_png_ptr) { + png_structp png_ptr = (png_structp)m_png_ptr; + png_infop info_ptr = (png_infop)m_info_ptr; + if (bRead) + png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL); + else + png_destroy_write_struct(&png_ptr, &info_ptr); + m_png_ptr = m_info_ptr = NULL; + } + CImage::Close(); +} +/*----------------------------------------------------------------*/ + + +HRESULT CImagePNG::ReadHeader() +{ + // initialize stuff + ASSERT(m_png_ptr == NULL); + m_png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!m_png_ptr) { + LOG(LT_IMAGE, "error: invalid PNG image (png_create_read_struct)"); + return _INVALIDFILE; + } + bRead = true; + png_structp png_ptr = (png_structp)m_png_ptr; + + m_info_ptr = png_create_info_struct(png_ptr); + if (!m_info_ptr) { + LOG(LT_IMAGE, "error: invalid PNG image (png_create_info_struct)"); + return _INVALIDFILE; + } + png_infop info_ptr = (png_infop)m_info_ptr; + + if (setjmp(png_jmpbuf(png_ptr))) + return _INVALIDFILE; + + ((ISTREAM*)m_pStream)->setPos(0); + png_set_read_fn(png_ptr, m_pStream, custom_png_read_file); + + png_read_info(png_ptr, info_ptr); + + png_uint_32 width, height; int bitdepth, colortype; + png_get_IHDR(png_ptr, info_ptr, &width, &height, &bitdepth, &colortype, NULL, NULL, NULL); + + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { + png_set_tRNS_to_alpha(png_ptr); + colortype = PNG_COLOR_TYPE_RGB_ALPHA; + } + + #if 0 + // if it doesn't have a file gamma, don't do any correction ("do no harm") + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_gAMA)) { + double gamma = 0; + const double screen_gamma = 2.2; + if (png_get_gAMA(png_ptr, info_ptr, &gamma)) + png_set_gamma(png_ptr, screen_gamma, gamma); + } + #endif + + switch (colortype) + { + case PNG_COLOR_TYPE_GRAY: + if (bitdepth < 8) + png_set_expand_gray_1_2_4_to_8(png_ptr); + m_format = PF_GRAY8; + m_stride = 1; + break; + case PNG_COLOR_TYPE_PALETTE: + png_set_palette_to_rgb(png_ptr); + m_format = PF_B8G8R8; + m_stride = 3; + break; + case PNG_COLOR_TYPE_GRAY_ALPHA: + png_set_gray_to_rgb(png_ptr); + m_format = PF_B8G8R8; + m_stride = 3; + break; + case PNG_COLOR_TYPE_RGB: + m_format = PF_B8G8R8; + m_stride = 3; + break; + case PNG_COLOR_TYPE_RGB_ALPHA: + m_format = PF_B8G8R8A8; + m_stride = 4; + break; + default: + LOG(LT_IMAGE, "error: unsupported PNG image"); + return _INVALIDFILE; + } + + if (bitdepth == 16) + png_set_strip_16(png_ptr); + + png_set_interlace_handling(png_ptr); + png_read_update_info(png_ptr, info_ptr); + + m_dataWidth = m_width = (Size)width; + m_dataHeight= m_height = (Size)height; + m_numLevels = 0; + m_level = 0; + m_lineWidth = (Size)png_get_rowbytes(png_ptr, info_ptr); + + return _OK; +} // ReadHeader +/*----------------------------------------------------------------*/ + + +HRESULT CImagePNG::ReadData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + png_structp png_ptr = (png_structp)m_png_ptr; + + // read data + if (nStride == m_stride && (m_format != PF_B8G8R8A8 || (dataFormat != PF_A8R8G8B8 && dataFormat != PF_A8B8G8R8))) { + if ((m_format == PF_B8G8R8 && dataFormat == PF_R8G8B8) || + (m_format == PF_B8G8R8A8 && dataFormat == PF_R8G8B8A8)) { + // flip R and B from the PNG library + png_set_bgr(png_ptr); + } else { + ASSERT(m_format == dataFormat); + } + // read image directly to the data buffer + for (Size j=0; j const buffer(new png_byte[m_lineWidth]); + for (Size j=0; j 0) { + png_set_compression_mem_level(png_ptr, compression_level); + } + else { + // tune parameters for speed + // (see http://wiki.linuxquestions.org/wiki/Libpng) + png_set_filter(png_ptr, PNG_FILTER_TYPE_BASE, PNG_FILTER_SUB); + png_set_compression_level(png_ptr, Z_BEST_SPEED); + } + png_set_compression_strategy(png_ptr, compression_strategy); + + png_set_IHDR(png_ptr, info_ptr, m_width, m_height, 8/*depth*/, + m_stride == 1 ? PNG_COLOR_TYPE_GRAY : + m_stride == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGBA, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT); + + png_write_info(png_ptr, info_ptr); + + return _OK; +} // WriteHeader +/*----------------------------------------------------------------*/ + + +HRESULT CImagePNG::WriteData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + png_structp png_ptr = (png_structp)m_png_ptr; + + // write data + if (nStride == m_stride && (m_format != PF_B8G8R8A8 || (dataFormat != PF_A8R8G8B8 && dataFormat != PF_A8B8G8R8))) { + if ((m_format == PF_B8G8R8 && dataFormat == PF_R8G8B8) || + (m_format == PF_B8G8R8A8 && dataFormat == PF_R8G8B8A8)) { + // flip R and B from the PNG library + png_set_bgr(png_ptr); + } else { + ASSERT(m_format == dataFormat); + } + // write image directly to the data buffer + for (Size j=0; j const buffer(new png_byte[m_lineWidth]); + for (Size j=0; jsetPos(0); + SCIINFOHEADER sciInfo; + m_pStream->read(&sciInfo, sizeof(SCIINFOHEADER)); + if (sciInfo.dwHeader != IMAGE_SCI_HEADER) { + LOG(LT_IMAGE, "error: invalid SCI image"); + return _INVALIDFILE; + } + m_width = sciInfo.shWidth; + m_height = sciInfo.shHeight; + m_numLevels = sciInfo.byLevels; + m_level = 0; + m_format = (PIXELFORMAT)sciInfo.byFormat; + m_stride = GetStride(m_format); + m_lineWidth = GetDataSizes(0, m_dataWidth, m_dataHeight); + return _OK; +} // ReadHeader +/*----------------------------------------------------------------*/ + + +HRESULT CImageSCI::WriteHeader(PIXELFORMAT imageFormat, Size width, Size height, BYTE numLevels) +{ + // write header + CImage::WriteHeader(imageFormat, width, height, numLevels); + ((OSTREAM*)m_pStream)->setPos(0); + const SCIINFOHEADER sciInfo = {IMAGE_SCI_HEADER, (uint16_t)m_width, (uint16_t)m_height, (uint8_t)m_format, m_numLevels, 0, 0}; + if (sizeof(SCIINFOHEADER) != m_pStream->write(&sciInfo, sizeof(SCIINFOHEADER))) { + LOG(LT_IMAGE, "error: failed writing the SCI image"); + return _INVALIDFILE; + } + return _OK; +} // WriteHeader +/*----------------------------------------------------------------*/ diff --git a/libs/IO/ImageSCI.h b/libs/IO/ImageSCI.h new file mode 100644 index 0000000..fb9bb28 --- /dev/null +++ b/libs/IO/ImageSCI.h @@ -0,0 +1,34 @@ +//////////////////////////////////////////////////////////////////// +// ImageSCI.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_IMAGESCI_H__ +#define __SEACAVE_IMAGESCI_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Image.h" + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class IO_API CImageSCI : public CImage +{ +public: + CImageSCI(); + virtual ~CImageSCI(); + + HRESULT ReadHeader(); + HRESULT WriteHeader(PIXELFORMAT, Size width, Size height, BYTE numLevels); +}; // class CImageSCI +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_IMAGESCI_H__ diff --git a/libs/IO/ImageTGA.cpp b/libs/IO/ImageTGA.cpp new file mode 100644 index 0000000..deefa12 --- /dev/null +++ b/libs/IO/ImageTGA.cpp @@ -0,0 +1,146 @@ +//////////////////////////////////////////////////////////////////// +// ImageTGA.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" + +#ifdef _IMAGE_TGA +#include "ImageTGA.h" + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + +#pragma pack(push, 1) +typedef struct TGAINFOHEADER_TYPE { + BYTE byIDLength; // ID length + BYTE byCMType; // Color map type + BYTE byType; // Image type + struct { // Color map specifications + uint16_t shCMIdx; // First entry index, offset into the color map table + uint16_t shCMLength; // Color map length, number of entries + BYTE byCMBPP; // Color map entry size, number of bits per pixel + }; + struct { // Image specifications + uint16_t shXOrigin; // X-origin, absolute coordinate of lower-left corner for displays where origin is at the lower left + uint16_t shYOrigin; // Y-origin, as for X-origin + uint16_t shWidth; // Image width, width in pixels + uint16_t shHeight; // Image height, width in pixels + BYTE byBPP; // Pixel depth, bits per pixel + union { + BYTE byAlphaInfo; // Image descriptor, bits 3-0 give the alpha channel depth, bits 4-5 give direction + struct { + BYTE byAlphaBPP : 3;// alpha channel depth + BYTE bMirrored : 1;// alpha channel depth + BYTE bFlipped : 1;// alpha channel depth + BYTE : 0;// Force alignment to next boundary. + }; + }; + }; +} TGAINFOHEADER; +#pragma pack(pop) + + + +// S T R U C T S /////////////////////////////////////////////////// + +CImageTGA::CImageTGA() +{ +} // Constructor + +CImageTGA::~CImageTGA() +{ +} // Destructor +/*----------------------------------------------------------------*/ + + +HRESULT CImageTGA::ReadHeader() +{ + // read header + ((ISTREAM*)m_pStream)->setPos(0); + TGAINFOHEADER tgaInfo; + m_pStream->read(&tgaInfo, sizeof(TGAINFOHEADER)); + if (tgaInfo.byCMType != 0) { // paletted images not supported + LOG(LT_IMAGE, "error: invalid TGA image"); + return _INVALIDFILE; + } + + m_dataWidth = m_width = tgaInfo.shWidth; + m_dataHeight = m_height = tgaInfo.shHeight; + m_numLevels = 0; + m_level = 0; + + // get the format + m_stride = (tgaInfo.byBPP>>3); + switch (tgaInfo.byType) + { + case 2: //uncompressed, true-color image + case 3: //uncompressed, black-and-white image + m_format = (m_stride == 3 ? PF_R8G8B8 : PF_R8G8B8A8); + m_bRLE = false; + break; + //case 10://run-length encoded, true-color image and + // m_format = IF_3DC; + // m_bRLE = true; + // break; + default: + ASSERT(0); + LOG(LT_IMAGE, "error: unsupported TGA image"); + return _INVALIDFILE; + } + + m_lineWidth = m_width * m_stride; + + // read the id stuff, not using seeking to be able to use non seekable sources + if (tgaInfo.byIDLength) { + CAutoPtrArr const buffer(new uint8_t[tgaInfo.byIDLength]); + m_pStream->read(buffer, tgaInfo.byIDLength); + } + + return _OK; +} // ReadHeader +/*----------------------------------------------------------------*/ + + +HRESULT CImageTGA::ReadData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + // read data + if (dataFormat == m_format && nStride == m_stride) { + // read image directly to the data buffer + (BYTE*&)pData += (m_height-1)*lineWidth; + for (Size j=0; jread(pData, m_lineWidth)) + return _INVALIDFILE; + } else { + // read image to a buffer and convert it + CAutoPtrArr const buffer(new uint8_t[m_lineWidth]); + for (Size j=0; jread(buffer, m_lineWidth)) + return _INVALIDFILE; + if (!FilterFormat((BYTE*)pData+(m_height-j-1)*lineWidth, dataFormat, nStride, buffer, m_format, m_stride, m_width)) + return _FAIL; + } + } + return _OK; +} // ReadData +/*----------------------------------------------------------------*/ + + +HRESULT CImageTGA::WriteHeader(PIXELFORMAT imageFormat, Size width, Size height, BYTE numLevels) +{ + return _FAIL; +} // WriteHeader +/*----------------------------------------------------------------*/ + + +HRESULT CImageTGA::WriteData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + return _FAIL; +} // WriteData +/*----------------------------------------------------------------*/ + +#endif // _IMAGE_TGA diff --git a/libs/IO/ImageTGA.h b/libs/IO/ImageTGA.h new file mode 100644 index 0000000..517814e --- /dev/null +++ b/libs/IO/ImageTGA.h @@ -0,0 +1,39 @@ +//////////////////////////////////////////////////////////////////// +// ImageTGA.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_IMAGETGA_H__ +#define __SEACAVE_IMAGETGA_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Image.h" + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class IO_API CImageTGA : public CImage +{ +public: + CImageTGA(); + virtual ~CImageTGA(); + + HRESULT ReadHeader(); + HRESULT ReadData(void*, PIXELFORMAT, Size nStride, Size lineWidth); + HRESULT WriteHeader(PIXELFORMAT, Size width, Size height, BYTE numLevels); + HRESULT WriteData(void*, PIXELFORMAT, Size nStride, Size lineWidth); + +protected: + bool m_bRLE; +}; // class CImageTGA +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_IMAGETGA_H__ diff --git a/libs/IO/ImageTIFF.cpp b/libs/IO/ImageTIFF.cpp new file mode 100644 index 0000000..bfdd7e1 --- /dev/null +++ b/libs/IO/ImageTIFF.cpp @@ -0,0 +1,567 @@ +//////////////////////////////////////////////////////////////////// +// ImageTIFF.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" + +#ifdef _IMAGE_TIFF +#include "ImageTIFF.h" + +extern "C" { +#if !defined(_MSC_VER) && !defined(__BORLANDC__) +#include +#undef TIFF_INT64_T +#define TIFF_INT64_T int64_t +#undef TIFF_UINT64_T +#define TIFF_UINT64_T uint64_t +#endif +#include +} + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + +/* + ISO C++ uses a 'std::streamsize' type to define counts. This makes + it similar to, (but perhaps not the same as) size_t. + + The std::ios::pos_type is used to represent stream positions as used + by tellg(), tellp(), seekg(), and seekp(). This makes it similar to + (but perhaps not the same as) 'off_t'. The std::ios::streampos type + is used for character streams, but is documented to not be an + integral type anymore, so it should *not* be assigned to an integral + type. + + The std::ios::off_type is used to specify relative offsets needed by + the variants of seekg() and seekp() which accept a relative offset + argument. + + Useful prototype knowledge: + + Obtain read position + ios::pos_type basic_istream::tellg() + + Set read position + basic_istream& basic_istream::seekg(ios::pos_type) + basic_istream& basic_istream::seekg(ios::off_type, ios_base::seekdir) + + Read data + basic_istream& istream::read(char *str, streamsize count) + + Number of characters read in last unformatted read + streamsize istream::gcount(); + + Obtain write position + ios::pos_type basic_ostream::tellp() + + Set write position + basic_ostream& basic_ostream::seekp(ios::pos_type) + basic_ostream& basic_ostream::seekp(ios::off_type, ios_base::seekdir) + + Write data + basic_ostream& ostream::write(const char *str, streamsize count) +*/ + +struct tiffis_data; +struct tiffos_data; + +extern "C" { + + static tmsize_t _tiffosReadProc(thandle_t, void*, tmsize_t); + static tmsize_t _tiffisReadProc(thandle_t fd, void* buf, tmsize_t size); + static tmsize_t _tiffosWriteProc(thandle_t fd, void* buf, tmsize_t size); + static tmsize_t _tiffisWriteProc(thandle_t, void*, tmsize_t); + static uint64_t _tiffosSeekProc(thandle_t fd, uint64_t off, int whence); + static uint64_t _tiffisSeekProc(thandle_t fd, uint64_t off, int whence); + static uint64_t _tiffosSizeProc(thandle_t fd); + static uint64_t _tiffisSizeProc(thandle_t fd); + static int _tiffosCloseProc(thandle_t fd); + static int _tiffisCloseProc(thandle_t fd); + static int _tiffDummyMapProc(thandle_t, void** base, toff_t* size); + static void _tiffDummyUnmapProc(thandle_t, void* base, toff_t size); + static TIFF* _tiffStreamOpen(const char* name, const char* mode, void *fd); + + struct tiffis_data + { + ISTREAM* stream; + size_f_t start_pos; + }; + + struct tiffos_data + { + OSTREAM* stream; + size_f_t start_pos; + }; + + static tmsize_t _tiffosReadProc(thandle_t, void*, tmsize_t) + { + return 0; + } + + static tmsize_t _tiffisReadProc(thandle_t fd, void* buf, tmsize_t size) + { + tiffis_data *data = reinterpret_cast(fd); + + // Verify that type does not overflow. + size_t request_size = size; + if (static_cast(request_size) != size) + return static_cast(-1); + + return static_cast(data->stream->read(buf, request_size)); + } + + static tmsize_t _tiffosWriteProc(thandle_t fd, void* buf, tmsize_t size) + { + tiffos_data *data = reinterpret_cast(fd); + + // Verify that type does not overflow. + size_t request_size = size; + if (static_cast(request_size) != size) + return static_cast(-1); + + return static_cast(data->stream->write(buf, request_size)); + } + + static tmsize_t _tiffisWriteProc(thandle_t, void*, tmsize_t) + { + return 0; + } + + static uint64_t _tiffosSeekProc(thandle_t fd, uint64_t off, int whence) + { + tiffos_data *data = reinterpret_cast(fd); + OSTREAM* os = data->stream; + + // if the stream has already failed, don't do anything + if (os == NULL) + return static_cast(-1); + + bool bSucceeded(true); + switch (whence) { + case SEEK_SET: + { + // Compute 64-bit offset + uint64_t new_offset = static_cast(data->start_pos) + off; + + // Verify that value does not overflow + size_f_t offset = static_cast(new_offset); + if (static_cast(offset) != new_offset) + return static_cast(-1); + + bSucceeded = os->setPos(offset); + break; + } + case SEEK_CUR: + { + // Verify that value does not overflow + size_f_t offset = static_cast(off); + if (static_cast(offset) != off) + return static_cast(-1); + + bSucceeded = os->setPos(os->getPos()+offset); + break; + } + case SEEK_END: + { + // Verify that value does not overflow + size_f_t offset = static_cast(off); + if (static_cast(offset) != off) + return static_cast(-1); + + bSucceeded = os->setPos(os->getSize()-offset); + break; + } + } + + // Attempt to workaround problems with seeking past the end of the + // stream. ofstream doesn't have a problem with this but + // ostrstream/ostringstream does. In that situation, add intermediate + // '\0' characters. + if (!bSucceeded) { + size_f_t origin; + switch (whence) { + case SEEK_SET: + default: + origin = data->start_pos; + break; + case SEEK_CUR: + origin = os->getPos(); + break; + case SEEK_END: + os->setPos(os->getSize()); + origin = os->getPos(); + break; + } + + // only do something if desired seek position is valid + if ((static_cast(origin) + off) > static_cast(data->start_pos)) { + uint64_t num_fill; + // extend the stream to the expected size + os->setPos(os->getSize()); + num_fill = (static_cast(origin)) + off - os->getPos(); + const char dummy = '\0'; + for (uint64_t i = 0; i < num_fill; i++) + os->write(&dummy, 1); + // retry the seek + os->setPos(static_cast(static_cast(origin) + off)); + } + } + + return static_cast(os->getPos()); + } + + static uint64_t _tiffisSeekProc(thandle_t fd, uint64_t off, int whence) + { + tiffis_data *data = reinterpret_cast(fd); + ISTREAM* is = data->stream; + + switch (whence) { + case SEEK_SET: + { + // Compute 64-bit offset + uint64_t new_offset = static_cast(data->start_pos) + off; + + // Verify that value does not overflow + size_f_t offset = static_cast(new_offset); + if (static_cast(offset) != new_offset) + return static_cast(-1); + + is->setPos(offset); + break; + } + case SEEK_CUR: + { + // Verify that value does not overflow + size_f_t offset = static_cast(off); + if (static_cast(offset) != off) + return static_cast(-1); + + is->setPos(is->getPos()+offset); + break; + } + case SEEK_END: + { + // Verify that value does not overflow + size_f_t offset = static_cast(off); + if (static_cast(offset) != off) + return static_cast(-1); + + is->setPos(is->getSize()-offset); + break; + } + } + + return (uint64_t)(is->getPos() - data->start_pos); + } + + static uint64_t _tiffosSizeProc(thandle_t fd) + { + tiffos_data *data = reinterpret_cast(fd); + return (uint64_t)data->stream->getSize(); + } + + static uint64_t _tiffisSizeProc(thandle_t fd) + { + tiffis_data *data = reinterpret_cast(fd); + return (uint64_t)data->stream->getSize(); + } + + static int _tiffosCloseProc(thandle_t fd) + { + // Our stream was not allocated by us, so it shouldn't be closed by us. + delete reinterpret_cast(fd); + return 0; + } + + static int _tiffisCloseProc(thandle_t fd) + { + // Our stream was not allocated by us, so it shouldn't be closed by us. + delete reinterpret_cast(fd); + return 0; + } + + static int _tiffDummyMapProc(thandle_t, void** /*base*/, toff_t* /*size*/) + { + return (0); + } + + static void _tiffDummyUnmapProc(thandle_t, void* /*base*/, toff_t /*size*/) + { + } + + /* + * Open a TIFF file descriptor for read/writing. + */ + static TIFF* _tiffStreamOpen(const char* name, const char* mode, void *fd) + { + TIFF* tif; + + if (strchr(mode, 'w')) { + tiffos_data *data = new tiffos_data; + data->stream = reinterpret_cast(fd); + data->start_pos = data->stream->getPos(); + + // Open for writing. + tif = TIFFClientOpen(name, mode, + reinterpret_cast(data), + _tiffosReadProc, + _tiffosWriteProc, + _tiffosSeekProc, + _tiffosCloseProc, + _tiffosSizeProc, + _tiffDummyMapProc, + _tiffDummyUnmapProc); + } else { + tiffis_data *data = new tiffis_data; + data->stream = reinterpret_cast(fd); + data->start_pos = data->stream->getPos(); + // Open for reading. + tif = TIFFClientOpen(name, mode, + reinterpret_cast(data), + _tiffisReadProc, + _tiffisWriteProc, + _tiffisSeekProc, + _tiffisCloseProc, + _tiffisSizeProc, + _tiffDummyMapProc, + _tiffDummyUnmapProc); + } + + return (tif); + } + +} /* extern "C" */ + +// TIFFOpen() mode flags are different to fopen(). A 'b' in mode "rb" has no effect when reading. +// http://www.remotesensing.org/libtiff/man/TIFFOpen.3tiff.html +// NB: We don't support mapped files with streams so add 'm' +TIFF* TIFFStreamOpen(const char* name, OSTREAM* os) +{ + return _tiffStreamOpen(name, "wm", os); +} +TIFF* TIFFStreamOpen(const char* name, ISTREAM* is) +{ + return _tiffStreamOpen(name, "rm", is); +} + + +// S T R U C T S /////////////////////////////////////////////////// + +CImageTIFF::CImageTIFF() : m_state(NULL) +{ +} // Constructor + +CImageTIFF::~CImageTIFF() +{ + //clean up + Close(); +} // Destructor +/*----------------------------------------------------------------*/ + +void CImageTIFF::Close() +{ + if (m_state) + { + TIFF* tif = static_cast(m_state); + TIFFClose(tif); + m_state = NULL; + } + m_width = m_height = 0; + CImage::Close(); +} +/*----------------------------------------------------------------*/ + +HRESULT CImageTIFF::ReadHeader() +{ + TIFF* tif = static_cast(m_state); + if (!tif) { + tif = TIFFStreamOpen("ReadTIFF", (ISTREAM*)m_pStream); + if (!tif) { + LOG(LT_IMAGE, "error: unsupported TIFF image"); + return _INVALIDFILE; + } + } + m_state = tif; + + uint16 photometric = 0; + if (TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &m_width) && + TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &m_height) && + TIFFGetField(tif, TIFFTAG_PHOTOMETRIC, &photometric)) + { + uint16 bpp=8, ncn = photometric > 1 ? 3 : 1; + TIFFGetField(tif, TIFFTAG_BITSPERSAMPLE, &bpp); + TIFFGetField(tif, TIFFTAG_SAMPLESPERPIXEL, &ncn); + + m_dataWidth = m_width; + m_dataHeight= m_height; + m_numLevels = 0; + m_level = 0; + + if ((bpp == 32 && ncn == 3) || photometric == PHOTOMETRIC_LOGLUV) { + // this is HDR format with 3 floats per pixel + //TODO: implement + ASSERT("error: not implemented" == NULL); + Close(); + return _FAIL; + } + if (bpp > 8 && + ((photometric != 2 && photometric != 1) || + (ncn != 1 && ncn != 3 && ncn != 4))) + bpp = 8; + switch (bpp) { + case 8: + m_stride = 4; + m_format = PF_B8G8R8A8; + break; + //case 16: + // m_type = CV_MAKETYPE(CV_16U, photometric > 1 ? 3 : 1); + // break; + //case 32: + // m_type = CV_MAKETYPE(CV_32F, photometric > 1 ? 3 : 1); + // break; + //case 64: + // m_type = CV_MAKETYPE(CV_64F, photometric > 1 ? 3 : 1); + // break; + default: + //TODO: implement + ASSERT("error: not implemented" == NULL); + LOG(LT_IMAGE, "error: unsupported TIFF image"); + Close(); + return _INVALIDFILE; + } + m_lineWidth = m_width * m_stride; + + return _OK; + } + + Close(); + return _FAIL; +} // ReadHeader +/*----------------------------------------------------------------*/ + +HRESULT CImageTIFF::ReadData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + if (m_state && m_width && m_height) { + TIFF* tif = (TIFF*)m_state; + uint32_t tile_width0 = m_width, tile_height0 = 0; + int is_tiled = TIFFIsTiled(tif); + uint16 photometric; + TIFFGetField(tif, TIFFTAG_PHOTOMETRIC, &photometric); + uint16 bpp = 8, ncn = photometric > 1 ? 3 : 1; + TIFFGetField(tif, TIFFTAG_BITSPERSAMPLE, &bpp); + TIFFGetField(tif, TIFFTAG_SAMPLESPERPIXEL, &ncn); + const int bitsPerByte = 8; + int dst_bpp = (int)(1 * bitsPerByte); + if (dst_bpp == 8) { + char errmsg[1024]; + if (!TIFFRGBAImageOK(tif, errmsg)) { + Close(); + return _INVALIDFILE; + } + } + + if ((!is_tiled) || + (is_tiled && + TIFFGetField(tif, TIFFTAG_TILEWIDTH, &tile_width0) && + TIFFGetField(tif, TIFFTAG_TILELENGTH, &tile_height0))) + { + if (!is_tiled) + TIFFGetField(tif, TIFFTAG_ROWSPERSTRIP, &tile_height0); + + if (tile_width0 <= 0) + tile_width0 = m_width; + + if (tile_height0 <= 0 || + (!is_tiled && tile_height0 == std::numeric_limits::max())) + tile_height0 = m_height; + + uint8_t* data = (uint8_t*)pData; + if (!is_tiled && tile_height0 == 1 && dataFormat == m_format && nStride == m_stride) { + // read image directly to the data buffer + for (Size j=0; j m_height) + tile_height = m_height - y; + + for (uint32_t x = 0; x < m_width; x += tile_width0) { + uint32_t tile_width = tile_width0; + if (x + tile_width > m_width) + tile_width = m_width - x; + + int ok; + switch (dst_bpp) { + case 8: + { + uint8_t* bstart = buffer; + if (!is_tiled) + ok = TIFFReadRGBAStrip(tif, y, (uint32_t*)buffer); + else { + ok = TIFFReadRGBATile(tif, x, y, (uint32_t*)buffer); + //Tiles fill the buffer from the bottom up + bstart += (tile_height0 - tile_height) * tile_width0 * 4; + } + if (!ok) { + Close(); + return _INVALIDFILE; + } + + for (uint32_t i = 0; i < tile_height; ++i) { + uint8_t* dst = data + x*3 + lineWidth*(tile_height - i - 1); + uint8_t* src = bstart + i*tile_width0*4; + if (!FilterFormat(dst, dataFormat, nStride, src, m_format, m_stride, tile_width)) { + Close(); + return _FAIL; + } + } + break; + } + default: + { + Close(); + return _INVALIDFILE; + } + } + } + } + } + + return _OK; + } + } + + Close(); + return _FAIL; +} // Read +/*----------------------------------------------------------------*/ + +HRESULT CImageTIFF::WriteHeader(PIXELFORMAT imageFormat, Size width, Size height, BYTE numLevels) +{ + //TODO: to implement the TIFF encoder + return _OK; +} // WriteHeader +/*----------------------------------------------------------------*/ + +HRESULT CImageTIFF::WriteData(void* pData, PIXELFORMAT dataFormat, Size nStride, Size lineWidth) +{ + //TODO: to implement the TIFF encoder + return _OK; +} // WriteData +/*----------------------------------------------------------------*/ + +#endif // _IMAGE_TIFF diff --git a/libs/IO/ImageTIFF.h b/libs/IO/ImageTIFF.h new file mode 100644 index 0000000..f3eb743 --- /dev/null +++ b/libs/IO/ImageTIFF.h @@ -0,0 +1,44 @@ +//////////////////////////////////////////////////////////////////// +// ImageTIFF.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_IMAGETIFF_H__ +#define __SEACAVE_IMAGETIFF_H__ + + +// D E F I N E S /////////////////////////////////////////////////// + + +// I N C L U D E S ///////////////////////////////////////////////// + +#include "Image.h" + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +class IO_API CImageTIFF : public CImage +{ +public: + CImageTIFF(); + virtual ~CImageTIFF(); + + void Close(); + + HRESULT ReadHeader(); + HRESULT ReadData(void*, PIXELFORMAT, Size nStride, Size lineWidth); + HRESULT WriteHeader(PIXELFORMAT, Size width, Size height, BYTE numLevels); + HRESULT WriteData(void*, PIXELFORMAT, Size nStride, Size lineWidth); + +protected: + void* m_state; +}; // class CImageTIFF +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_IMAGETIFF_H__ diff --git a/libs/IO/OBJ.cpp b/libs/IO/OBJ.cpp new file mode 100644 index 0000000..0c1f530 --- /dev/null +++ b/libs/IO/OBJ.cpp @@ -0,0 +1,290 @@ +//////////////////////////////////////////////////////////////////// +// OBJ.cpp +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" +#include "OBJ.h" + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + +// uncomment to enable multi-threading based on OpenMP +#ifdef _USE_OPENMP +#define OBJ_USE_OPENMP +#endif + +#define OBJ_INDEX_OFFSET 1 + + +// S T R U C T S /////////////////////////////////////////////////// + +ObjModel::MaterialLib::Material::Material(const Image8U3& _diffuse_map, const Color& _Kd) + : + diffuse_map(_diffuse_map), + Kd(_Kd) +{ +} + +bool ObjModel::MaterialLib::Material::LoadDiffuseMap() +{ + if (diffuse_map.empty()) + return diffuse_map.Load(diffuse_name); + return true; +} +/*----------------------------------------------------------------*/ + + + +// S T R U C T S /////////////////////////////////////////////////// + +ObjModel::MaterialLib::MaterialLib() +{ +} + +bool ObjModel::MaterialLib::Save(const String& prefix, bool texLossless) const +{ + std::ofstream out((prefix+".mtl").c_str()); + if (!out.good()) + return false; + + const String pathName(Util::getFilePath(prefix)); + const String name(Util::getFileNameExt(prefix)); + #ifdef OBJ_USE_OPENMP + bool bSuccess(true); + #pragma omp parallel for + #endif + for (int_t i = 0; i < (int_t)materials.size(); ++i) { + const Material& mat = materials[i]; + // save material description + std::stringstream ss; + ss << "newmtl " << mat.name << "\n" + << "Ka 1.000000 1.000000 1.000000" << "\n" + << "Kd " << mat.Kd.r << " " << mat.Kd.g << " " << mat.Kd.b << "\n" + << "Ks 0.000000 0.000000 0.000000" << "\n" + << "Tr 1.000000" << "\n" + << "illum 1" << "\n" + << "Ns 1.000000" << "\n"; + // save material maps + if (mat.diffuse_map.empty()) { + #ifdef OBJ_USE_OPENMP + #pragma omp critical + #endif + out << ss.str(); + continue; + } + if (mat.diffuse_name.IsEmpty()) + const_cast(mat.diffuse_name) = name+"_"+mat.name+"_map_Kd."+(texLossless?"png":"jpg"); + ss << "map_Kd " << mat.diffuse_name << "\n"; + #ifdef OBJ_USE_OPENMP + #pragma omp critical + #endif + out << ss.str(); + const bool bRet(mat.diffuse_map.Save(pathName+mat.diffuse_name)); + #ifdef OBJ_USE_OPENMP + #pragma omp critical + if (!bRet) + bSuccess = false; + #else + if (!bRet) + return false; + #endif + } + #ifdef OBJ_USE_OPENMP + return bSuccess; + #else + return true; + #endif +} + +bool ObjModel::MaterialLib::Load(const String& fileName) +{ + const size_t numMaterials(materials.size()); + std::ifstream in(fileName.c_str()); + String keyword; + while (in.good() && in >> keyword) { + if (keyword == "newmtl") { + in >> keyword; + materials.push_back(Material(keyword)); + } else if (keyword == "Kd") { + ASSERT(numMaterials < materials.size()); + Color c; + in >> c.r >> c.g >> c.b; + materials.back().Kd = c; + } else if (keyword == "map_Kd") { + ASSERT(numMaterials < materials.size()); + String& diffuse_name = materials.back().diffuse_name; + in >> diffuse_name; + diffuse_name = Util::getFilePath(fileName) + diffuse_name; + } + } + return numMaterials < materials.size(); +} +/*----------------------------------------------------------------*/ + + + +// S T R U C T S /////////////////////////////////////////////////// + +bool ObjModel::Save(const String& fileName, unsigned precision, bool texLossless) const +{ + if (vertices.empty()) + return false; + const String prefix(Util::getFileFullName(fileName)); + const String name(Util::getFileNameExt(prefix)); + + if (!material_lib.Save(prefix, texLossless)) + return false; + + std::ofstream out((prefix + ".obj").c_str()); + if (!out.good()) + return false; + + out << "mtllib " << name << ".mtl" << "\n"; + + out << std::fixed << std::setprecision(precision); + for (size_t i = 0; i < vertices.size(); ++i) { + out << "v " + << vertices[i][0] << " " + << vertices[i][1] << " " + << vertices[i][2] << "\n"; + } + + for (size_t i = 0; i < texcoords.size(); ++i) { + out << "vt " + << texcoords[i][0] << " " + << texcoords[i][1] << "\n"; + } + + for (size_t i = 0; i < normals.size(); ++i) { + out << "vn " + << normals[i][0] << " " + << normals[i][1] << " " + << normals[i][2] << "\n"; + } + + for (size_t i = 0; i < groups.size(); ++i) { + out << "usemtl " << groups[i].material_name << "\n"; + for (size_t j = 0; j < groups[i].faces.size(); ++j) { + const Face& face = groups[i].faces[j]; + out << "f"; + for (size_t k = 0; k < 3; ++k) { + out << " " << face.vertices[k] + OBJ_INDEX_OFFSET; + if (!texcoords.empty()) { + out << "/" << face.texcoords[k] + OBJ_INDEX_OFFSET; + if (!normals.empty()) + out << "/" << face.normals[k] + OBJ_INDEX_OFFSET; + } else + if (!normals.empty()) + out << "//" << face.normals[k] + OBJ_INDEX_OFFSET; + } + out << "\n"; + } + } + return true; +} + +bool ObjModel::Load(const String& fileName) +{ + ASSERT(vertices.empty() && groups.empty() && material_lib.materials.empty()); + std::ifstream fin(fileName.c_str()); + String line, keyword; + std::istringstream in; + while (fin.good()) { + std::getline(fin, line); + if (line.empty() || line[0u] == '#') + continue; + in.str(line); + in >> keyword; + if (keyword == "v") { + Vertex v; + in >> v[0] >> v[1] >> v[2]; + if (in.fail()) + return false; + vertices.push_back(v); + } else + if (keyword == "vt") { + TexCoord vt; + in >> vt[0] >> vt[1]; + if (in.fail()) + return false; + texcoords.push_back(vt); + } else + if (keyword == "vn") { + Normal vn; + in >> vn[0] >> vn[1] >> vn[2]; + if (in.fail()) + return false; + normals.push_back(vn); + } else + if (keyword == "f") { + Face f; + memset(&f, 0xFF, sizeof(Face)); + for (size_t k = 0; k < 3; ++k) { + in >> keyword; + switch (sscanf(keyword, "%u/%u/%u", f.vertices+k, f.texcoords+k, f.normals+k)) { + case 1: + f.vertices[k] -= OBJ_INDEX_OFFSET; + break; + case 2: + f.vertices[k] -= OBJ_INDEX_OFFSET; + if (f.texcoords[k] != NO_ID) + f.texcoords[k] -= OBJ_INDEX_OFFSET; + if (f.normals[k] != NO_ID) + f.normals[k] -= OBJ_INDEX_OFFSET; + break; + case 3: + f.vertices[k] -= OBJ_INDEX_OFFSET; + f.texcoords[k] -= OBJ_INDEX_OFFSET; + f.normals[k] -= OBJ_INDEX_OFFSET; + break; + default: + return false; + } + } + if (in.fail()) + return false; + if (groups.empty()) + AddGroup(""); + groups.back().faces.push_back(f); + } else + if (keyword == "mtllib") { + in >> keyword; + if (!material_lib.Load(keyword)) + return false; + } else + if (keyword == "usemtl") { + Group group; + in >> group.material_name; + if (in.fail()) + return false; + groups.push_back(group); + } + in.clear(); + } + return !vertices.empty(); +} + + +ObjModel::Group& ObjModel::AddGroup(const String& material_name) +{ + groups.push_back(Group()); + Group& group = groups.back(); + group.material_name = material_name; + if (!GetMaterial(material_name)) + material_lib.materials.push_back(MaterialLib::Material(material_name)); + return group; +} + +ObjModel::MaterialLib::Material* ObjModel::GetMaterial(const String& name) +{ + MaterialLib::Materials::iterator it(std::find_if(material_lib.materials.begin(), material_lib.materials.end(), [&name](const MaterialLib::Material& mat) { return mat.name == name; })); + if (it == material_lib.materials.end()) + return NULL; + return &(*it); +} +/*----------------------------------------------------------------*/ diff --git a/libs/IO/OBJ.h b/libs/IO/OBJ.h new file mode 100644 index 0000000..d32c356 --- /dev/null +++ b/libs/IO/OBJ.h @@ -0,0 +1,115 @@ +//////////////////////////////////////////////////////////////////// +// OBJ.h +// +// Copyright 2007 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#ifndef __SEACAVE_OBJ_H__ +#define __SEACAVE_OBJ_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + + +namespace SEACAVE { + +// S T R U C T S /////////////////////////////////////////////////// + +// OBJ model files parser. +// +// The OBJ file format is a simple data-format that represents 3D geometry alone — +// namely, the position of each vertex, the UV position of each texture coordinate +// vertex, vertex normals, and the faces that make each polygon defined as a list of +// vertices, and texture vertices. Vertices are stored in a counter-clockwise order +// by default, making explicit declaration of face normals unnecessary. + +class IO_API ObjModel { +public: + typedef Pixel32F Color; + + // represents a material lib of an OBJ model + struct MaterialLib { + // represents a Lambertian material + struct Material { + String name; + String diffuse_name; + Image8U3 diffuse_map; + Color Kd; + + Material() : Kd(Color::WHITE) {} + Material(const String& _name) : name(_name), Kd(Color::WHITE) {} + Material(const Image8U3& _diffuse_map, const Color& _Kd=Color::WHITE); + + // Makes sure the image is loaded for the diffuse map + bool LoadDiffuseMap(); + }; + + typedef std::vector Materials; + + Materials materials; + + MaterialLib(); + + // Saves the material lib to a .mtl file and all textures of its materials with the given prefix name + bool Save(const String& prefix, bool texLossless=false) const; + // Loads the material lib from a .mtl file and all textures of its materials with the given file name + bool Load(const String& fileName); + }; + + typedef Point3f Vertex; + typedef Point2f TexCoord; + typedef Point3f Normal; + + typedef uint32_t Index; + + struct Face { + Index vertices[3]; + Index texcoords[3]; + Index normals[3]; + }; + + struct Group { + String material_name; + std::vector faces; + }; + + typedef std::vector Vertices; + typedef std::vector TexCoords; + typedef std::vector Normals; + typedef std::vector Groups; + +protected: + Vertices vertices; + TexCoords texcoords; + Normals normals; + Groups groups; + MaterialLib material_lib; + +public: + ObjModel() {} + + // Saves the obj model to an .obj file, its material lib and the materials with the given file name + bool Save(const String& fileName, unsigned precision=6, bool texLossless=false) const; + // Loads the obj model from an .obj file, its material lib and the materials with the given file name + bool Load(const String& fileName); + + // Creates a new group with the given material name + Group& AddGroup(const String& material_name); + // Retrieves a material from the library based on its name + MaterialLib::Material* GetMaterial(const String& name); + + MaterialLib& get_material_lib() { return material_lib; } + Vertices& get_vertices() { return vertices; } + TexCoords& get_texcoords() { return texcoords; } + Normals& get_normals() { return normals; } + Groups& get_groups() { return groups; } +}; +/*----------------------------------------------------------------*/ + +} // namespace SEACAVE + +#endif // __SEACAVE_OBJ_H__ diff --git a/libs/IO/PLY.cpp b/libs/IO/PLY.cpp new file mode 100644 index 0000000..4dbfa5e --- /dev/null +++ b/libs/IO/PLY.cpp @@ -0,0 +1,2386 @@ +//////////////////////////////////////////////////////////////////// +// PLY.cpp +// +// Copyright 2023 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) + +#include "Common.h" +#include "PLY.h" + +using namespace SEACAVE; + + +// D E F I N E S /////////////////////////////////////////////////// + +#define NO_OTHER_PROPS -1 + +#define DONT_STORE_PROP 0 +#define STORE_PROP 1 + +#define OTHER_PROP 0 +#define NAMED_PROP 1 + +#define abort_ply(...) { VERBOSE(__VA_ARGS__); exit(-1); } + + +// S T R U C T S /////////////////////////////////////////////////// + +const char* const PLY::type_names[] = { + "invalid", "int8", "int16", "int32", "uint8", "uint16", "uint32", "float32", "float64", +}; + +const char* const PLY::old_type_names[] = { + "invalid", "char", "short", "int", "uchar", "ushort", "uint", "float", "double", +}; + +const int PLY::ply_type_size[] = { + 0, 1, 2, 4, 1, 2, 4, 4, 8 +}; + +const PLY::RuleName PLY::rule_name_list[] = { + {AVERAGE_RULE, "avg"}, + {RANDOM_RULE, "rnd"}, + {MINIMUM_RULE, "max"}, + {MAXIMUM_RULE, "min"}, + {MAJORITY_RULE, "major"}, + {SAME_RULE, "same"}, + {-1, "end_marker"} +}; + + + +/******************/ +/* Construction */ +/******************/ + + +/****************************************************************************** +Init PLY data as empty. + +Entry: + +Exit: +******************************************************************************/ + +PLY::PLY() + : + which_elem(NULL), other_elems(NULL), current_rules(NULL), rule_list(NULL), + istream(NULL), mfp(NULL), write_type_names(type_names) +{ +} + +PLY::~PLY() +{ + release(); +} + + +/****************************************************************************** +Free the memory used by a PLY file. +******************************************************************************/ + +void PLY::release() +{ + if (mfp) { + delete mfp; + mfp = NULL; + ostream = NULL; + } else + if (istream) { + if (static_cast(istream)->getInputStream(ISTREAM::LAYER_ID_IN)) { + IOSTREAM* const iostream(static_cast(istream)->getIOStream(ISTREAM::LAYER_ID_IN, OSTREAM::LAYER_ID_OUT)); + if (iostream) + delete iostream; + else + delete istream; + } else { + ASSERT(static_cast(ostream)->getOutputStream(OSTREAM::LAYER_ID_OUT)); + IOSTREAM* const iostream(static_cast(ostream)->getIOStream(ISTREAM::LAYER_ID_IN, OSTREAM::LAYER_ID_OUT)); + if (iostream) + delete iostream; + else + delete ostream; + } + istream = NULL; + } + if (!elems.empty()) { + for (size_t i=0; iprops.empty()) { + for (size_t j=0; jprops.size(); ++j) + delete elem->props[j]; + elem->props.clear(); + elem->store_prop.clear(); + } + delete elem; + } + elems.clear(); + } + if (other_elems) { + for (size_t i=0; iother_list.size(); ++i) { + OtherElem& elem = other_elems->other_list[i]; + delete[] elem.other_data; + delete elem.other_props; + } + delete other_elems; other_elems = NULL; + } + comments.clear(); + obj_info.clear(); + vals.clear(); +} + + + +/*************/ +/* Writing */ +/*************/ + + +/****************************************************************************** +Given a file pointer, get ready to write PLY data to the file. + +Entry: +fp - the given file pointer +nelems - number of elements in object +elem_names - list of element names +file_type - file type, either ascii or binary +memBufferSize - memory file initial size (useful if the ply size is unknown) + +Exit: +returns a pointer to a PlyFile, used to refer to this file, or NULL if error +******************************************************************************/ + +bool PLY::write(LPCSTR _filename, int nelems, LPCSTR* elem_names, int _file_type, size_t memBufferSize) +{ + filename = _filename; + if (memBufferSize == 0) { + // create output file now + File* const pf(new File(filename.c_str(), File::WRITE, File::CREATE | File::TRUNCATE)); + if (!pf->isOpen()) + return false; + return write(new BufferedOutputStream(pf, 64*1024), nelems, elem_names, _file_type, memBufferSize); + } + return write((OSTREAM*)NULL, nelems, elem_names, _file_type, memBufferSize); +} + +bool PLY::write(OSTREAM* fp, int nelems, LPCSTR* elem_names, int _file_type, size_t memBufferSize) +{ + // create a record for this object + file_type = _file_type; + version = 1.0; + other_elems = NULL; + + if (memBufferSize > 0) { + // write ply into a memory buffer, and save it to disk at the end; + // useful in case the number of ply elements is not know from the start + // in order to avoid an additional file copy operation + ASSERT(fp == NULL && !filename.empty()); + mfp = new MemFile(memBufferSize); + ostream = mfp; + } else { + // directly write ply data to disk; + // in case the number of ply elements is unknown from the start, + // they are written into a temporary file and the header is written + // at the end using a file copy operation + ASSERT(fp != NULL); + ostream = fp; + mfp = NULL; + } + + // tuck aside the names of the elements + elems.resize(nelems); + for (int i = 0; i < nelems; ++i) { + PlyElement* elem = new PlyElement; + elem->name = elem_names[i]; + elem->num = 0; + elems[i] = elem; + } + return true; +} + + +/****************************************************************************** +Describe an element, including its properties and how many will be written +to the file. + +Entry: +elem_name - name of element that information is being specified about +nelems - number of elements of this type to be written +nprops - number of properties contained in the element +prop_list - list of properties +******************************************************************************/ + +void PLY::element_layout( + const char* elem_name, + int nelems, + int nprops, + PlyProperty* prop_list + ) +{ + // look for appropriate element + PlyElement *elem = find_element(elem_name); + if (elem == NULL) + abort_ply("error: element_layout: can't find element '%s'", elem_name); + elem->num = nelems; + + // copy the list of properties + elem->props.resize(nprops); + elem->store_prop.resize(nprops); + + for (int i = 0; i < nprops; ++i) { + PlyProperty *prop = new PlyProperty; + elem->props[i] = prop; + elem->store_prop[i] = NAMED_PROP; + copy_property(*prop, prop_list[i]); + } +} + + +/****************************************************************************** +Describe a property of an element. + +Entry: +elem_name - name of element that information is being specified about +prop - the new property +******************************************************************************/ + +void PLY::describe_property(const char* elem_name, const PlyProperty& prop) +{ + // look for appropriate element + put_element_setup(elem_name); + + // describe property + describe_property(prop); +} + +void PLY::describe_property(const char* elem_name, int nprops, const PlyProperty* props) +{ + // look for appropriate element + put_element_setup(elem_name); + + // describe properties + for (int i=0; inum = nelems; +} + + +/****************************************************************************** +Signal that we've described everything a PLY file's header and that the +header should be written to the file. + +This function can be also called at the end, in which case the file is close, +the header is written in a new file and the already written content is copied +from the temporary file. +******************************************************************************/ + +bool PLY::header_complete() +{ + std::string filenameTmp; + if (mfp != NULL) { + // ply data was written into a memory buffer, + // now write the header to disk and append the data in memory + ostream = NULL; + } else if (!filename.empty() && ostream->getPos() > 0) { + // close this file, rename it, and open a new file to write the header + delete ostream; ostream = NULL; + filenameTmp = filename+".tmp"; + if (!File::renameFile(filename.c_str(), filenameTmp.c_str())) + return false; + } + if (ostream == NULL) { + File* const pf(new File(filename.c_str(), File::WRITE, File::CREATE | File::TRUNCATE)); + if (!pf->isOpen()) + return false; + ostream = new BufferedOutputStream(pf, 64*1024); + } + + // write header + ostream->print("ply\n"); + + switch (file_type) { + case ASCII: + ostream->print("format ascii 1.0\n"); + break; + case BINARY_BE: + ostream->print("format binary_big_endian 1.0\n"); + break; + case BINARY_LE: + ostream->print("format binary_little_endian 1.0\n"); + break; + default: + abort_ply("error: ply_header_complete: bad file type = %d\n", file_type); + } + + // write out the comments + for (size_t i = 0; i < comments.size(); ++i) + ostream->print("comment %s\n", comments[i].c_str()); + + // write out object information + for (size_t i = 0; i < obj_info.size(); ++i) + ostream->print("obj_info %s\n", obj_info[i].c_str()); + + // write out information about each element + for (size_t i = 0; i < elems.size(); ++i) { + PlyElement *elem = elems[i]; + ASSERT(elem->num > 0); + ostream->print("element %s %d\n", elem->name.c_str(), elem->num); + + // write out each property + for (size_t j = 0; j < elem->props.size(); ++j) { + PlyProperty *prop = elem->props[j]; + if (prop->is_list == LIST) { + ostream->print("property list "); + write_scalar_type(prop->count_external); + ostream->print(" "); + write_scalar_type(prop->external_type); + ostream->print(" %s\n", prop->name.c_str()); + } else if (prop->is_list == STRING) { + ostream->print("property string"); + ostream->print(" %s\n", prop->name.c_str()); + } else { + ostream->print("property "); + write_scalar_type(prop->external_type); + ostream->print(" %s\n", prop->name.c_str()); + } + } + } + + ostream->print("end_header\n"); + + if (mfp != NULL) { + // now write the ply data from memory to disk + ostream->write(mfp->getBuffer(), mfp->getSize()); + delete mfp; mfp = NULL; + } else if (!filenameTmp.empty()) { + // append the body of the ply from the temp file, and delete it + File ftmp(filenameTmp.c_str(), File::READ, File::OPEN); + if (!ftmp.isOpen()) + return false; + Unsigned8Arr buffer(256 * 1024); + size_t len; + while ((len = ftmp.read(buffer.data(), buffer.size())) > 0) + ostream->write(buffer.data(), len); + ftmp.close(); + File::deleteFile(filenameTmp.c_str()); + } else { + // element writing is starting next, reset counters + for (size_t i = 0; i < elems.size(); ++i) + elems[i]->num = 0; + } + return true; +} + + +/****************************************************************************** +Specify which elements are going to be written. This should be called +before a call to the routine ply_put_element(). + +Entry: +elem_name - name of element we're talking about +******************************************************************************/ + +void PLY::put_element_setup(const char* elem_name) +{ + PlyElement *elem = find_element(elem_name); + if (elem == NULL) + abort_ply("error: put_element_setup: can't find element '%s'", elem_name); + which_elem = elem; +} + + +/****************************************************************************** +Write an element to the file. This routine assumes that we're +writing the type of element specified in the last call to the routine +put_element_setup(). + +Entry: +elem_ptr - pointer to the element +******************************************************************************/ + +void PLY::put_element(const void* elem_ptr) +{ + char *item; + char *elem_data; + char **item_ptr; + ValueType val; + char **other_ptr; + + PlyElement *elem = which_elem; + elem_data = (char*)elem_ptr; + other_ptr = (char**)(elem_data + elem->other_offset); + + // write out either to an ascii or binary file + if (file_type == ASCII) { + + // write an ascii file + + // write out each property of the element + for (size_t j = 0; j < elem->props.size(); ++j) { + + PlyProperty *prop = elem->props[j]; + + if (elem->store_prop[j] == OTHER_PROP) + elem_data = *other_ptr; + else + elem_data = (char*)elem_ptr; + + switch (prop->is_list) { + case SCALAR: { // scalar + item = elem_data + prop->offset; + get_stored_item((void*)item, prop->internal_type, val); + write_ascii_item(val, prop->internal_type, prop->external_type); + break; + } + case LIST: { // list + item = elem_data + prop->count_offset; + get_stored_item((void*)item, prop->count_internal, val); + write_ascii_item(val, prop->count_internal, prop->count_external); + const int list_count(ValueType2Type(val, prop->count_external)); + item_ptr = (char**)(elem_data + prop->offset); + item = item_ptr[0]; + const int item_size = ply_type_size[prop->internal_type]; + for (int k = 0; k < list_count; k++) { + get_stored_item((void*)item, prop->internal_type, val); + write_ascii_item(val, prop->internal_type, prop->external_type); + item += item_size; + } + break; + } + case STRING: { // string + item = elem_data + prop->offset; + char** str = (char**)item; + ostream->print("\"%s\"", *str); + break; + } + default: + abort_ply("error: put_element: bad type = %d", prop->is_list); + } + } + + ostream->print("\n"); + } else { + + // write a binary file + + // write out each property of the element + for (size_t j = 0; j < elem->props.size(); ++j) { + PlyProperty *prop = elem->props[j]; + if (elem->store_prop[j] == OTHER_PROP) + elem_data = *other_ptr; + else + elem_data = (char*)elem_ptr; + switch (prop->is_list) { + case SCALAR: { // scalar + item = elem_data + prop->offset; + get_stored_item((void*)item, prop->internal_type, val); + write_binary_item(val, prop->internal_type, prop->external_type); + break; + } + case LIST: { // list + item = elem_data + prop->count_offset; + int item_size = ply_type_size[prop->count_internal]; + get_stored_item((void*)item, prop->count_internal, val); + write_binary_item(val, prop->count_internal, prop->count_external); + const int list_count(ValueType2Type(val, prop->count_external)); + item_ptr = (char**)(elem_data + prop->offset); + item = item_ptr[0]; + item_size = ply_type_size[prop->internal_type]; + for (int k = 0; k < list_count; k++) { + get_stored_item((void*)item, prop->internal_type, val); + write_binary_item(val, prop->internal_type, prop->external_type); + item += item_size; + } + break; + } + case STRING: { // string + item = elem_data + prop->offset; + char** str = (char**)item; + + // write the length + const int len = (int)_tcslen(*str) + 1; + ostream->write(&len, sizeof(int)); + + // write the string, including the null character + ostream->write(*str, len); + break; + } + default: + abort_ply("error: put_element: bad type = %d", prop->is_list); + } + } + } + + // count element items + elem->num++; +} + + + +/*************/ +/* Reading */ +/*************/ + + +/****************************************************************************** +Given a file pointer, get ready to read PLY data from the file. + +Entry: +fp - the given file pointer + +Exit: +nelems - number of elements in object +elem_names - list of element names +returns a pointer to a PlyFile, used to refer to this file, or NULL if error +******************************************************************************/ + +bool PLY::read(LPCSTR _filename) +{ + filename = _filename; + File* const pf(new File(_filename, File::READ, File::OPEN)); + if (!pf->isOpen()) + return false; + return read(new BufferedInputStream(pf, 64*1024)); +} + +bool PLY::read(ISTREAM* fp) +{ + // create record for this object + ASSERT(elems.empty()); + other_elems = NULL; + rule_list = NULL; + istream = fp; + + // read and parse the file's header + int nwords; + char *orig_line; + STRISTREAM sfp(istream); + char **words = get_words(sfp, &nwords, &orig_line); + if (words == NULL) + return false; + if (!equal_strings(words[0], "ply")) { + free(words); + return false; + } + free(words); + + // parse words + while ((words = get_words(sfp, &nwords, &orig_line)) != NULL) { + if (equal_strings(words[0], "format")) { + if (nwords != 3) + return false; + if (equal_strings(words[1], "ascii")) + file_type = ASCII; + else if (equal_strings(words[1], "binary_big_endian")) + file_type = BINARY_BE; + else if (equal_strings(words[1], "binary_little_endian")) + file_type = BINARY_LE; + else + return false; + version = (float)atof(words[2]); + } else if (equal_strings(words[0], "element")) + add_element((const char**)words, nwords); + else if (equal_strings(words[0], "property")) + add_property((const char**)words, nwords); + else if (equal_strings(words[0], "comment")) + add_comment(orig_line); + else if (equal_strings(words[0], "obj_info")) + add_obj_info(orig_line); + else if (equal_strings(words[0], "end_header")) { + free(words); + break; + } + free(words); + } + sfp.emptyBuffer(); + + // create tags for each property of each element, to be used + // later to say whether or not to store each property for the user + for (size_t i = 0; i < elems.size(); ++i) { + PlyElement *elem = elems[i]; + elem->store_prop.resize(elem->props.size()); + for (size_t j = 0; j < elem->props.size(); ++j) + elem->store_prop[j] = DONT_STORE_PROP; + elem->other_offset = NO_OTHER_PROPS; // no "other" props by default + } + return true; +} + + +/****************************************************************************** +Get information about a particular element. + +Entry: +elem_name - name of element to get information about + +Exit: +props - the list of properties returned +returns number of elements of this type in the file +******************************************************************************/ + +int PLY::get_element_description(const char* elem_name, std::vector& prop_list) const +{ + // find information about the element + PlyElement *elem = find_element(elem_name); + if (elem == NULL) + return 0; + + // make a copy of the element's property list + prop_list.resize(elem->props.size()); + for (size_t i = 0; i < elem->props.size(); ++i) { + PlyProperty *prop = new PlyProperty; + copy_property(*prop, *elem->props[i]); + prop_list[i] = prop; + } + + return elem->num; +} + + +/****************************************************************************** +Specify which properties of an element are to be returned. This should be +called before a call to the routine get_element(). + +Entry: +elem_name - which element we're talking about +nprops - number of properties +prop_list - list of properties +******************************************************************************/ + +void PLY::get_element_setup( + const char* elem_name, + int nprops, + PlyProperty* prop_list + ) +{ + // find information about the element + PlyElement *elem = find_element(elem_name); + which_elem = elem; + + // deposit the property information into the element's description + for (int i = 0; i < nprops; ++i) { + // look for actual property + int index = find_property(elem, prop_list[i].name.c_str()); + if (index == -1) { + DEBUG("warning: Can't find property '%s' in element '%s'", prop_list[i].name.c_str(), elem_name); + continue; + } + + // store its description + PlyProperty *prop = elem->props[index]; + prop->internal_type = prop_list[i].internal_type; + prop->offset = prop_list[i].offset; + prop->count_internal = prop_list[i].count_internal; + prop->count_offset = prop_list[i].count_offset; + + // specify that the user wants this property + elem->store_prop[index] = STORE_PROP; + } +} + + +/****************************************************************************** +Specify a property of an element that is to be returned. This should be +called (usually multiple times) before a call to the routine get_element(). +This routine should be used in preference to the less flexible old routine +called ply_get_element_setup(). + +Entry: +elem_name - which element we're talking about +prop - property to add to those that will be returned +******************************************************************************/ + +void PLY::get_property(const char* elem_name, PlyProperty* prop) +{ + // find information about the element + PlyElement *elem = find_element(elem_name); + which_elem = elem; + + // deposit the property information into the element's description + int index = find_property(elem, prop->name.c_str()); + if (index == -1) { + DEBUG("warning: Can't find property '%s' in element '%s'", prop->name.c_str(), elem_name); + return; + } + PlyProperty *prop_ptr = elem->props[index]; + prop_ptr->internal_type = prop->internal_type; + prop_ptr->offset = prop->offset; + prop_ptr->count_internal = prop->count_internal; + prop_ptr->count_offset = prop->count_offset; + + // specify that the user wants this property + elem->store_prop[index] = STORE_PROP; +} + + +/****************************************************************************** +Read one element from the file. This routine assumes that we're reading +the type of element specified in the last call to the routine +ply_get_element_setup(). + +Entry: +elem_ptr - pointer to location where the element information should be put +******************************************************************************/ + +void PLY::get_element(void* elem_ptr) +{ + if (file_type == ASCII) + ascii_get_element((uint8_t*)elem_ptr); + else + binary_get_element((uint8_t*)elem_ptr); +} + + +/****************************************************************************** +Extract the comments from the header information of a PLY file. + +Entry: + +Exit: +returns the list of comments +******************************************************************************/ + +std::vector& PLY::get_comments() +{ + return comments; +} + + +/****************************************************************************** +Extract the object information (arbitrary text) from the header information +of a PLY file. + +Entry: + +Exit: +returns the list of object info lines +******************************************************************************/ + +std::vector& PLY::get_obj_info() +{ + return obj_info; +} + + +/****************************************************************************** +Make ready for "other" properties of an element-- those properties that +the user has not explicitly asked for, but that are to be stashed away +in a special structure to be carried along with the element's other +information. + +Entry: +elem - element for which we want to save away other properties +******************************************************************************/ + +void PLY::setup_other_props(PlyElement* elem) +{ + int size = 0; + + // Examine each property in decreasing order of size. + // We do this so that all data types will be aligned by + // word, half-word, or whatever within the structure. + for (int type_size = 8; type_size > 0; type_size /= 2) { + + // add up the space taken by each property, and save this information + // away in the property descriptor + for (size_t i = 0; i < elem->props.size(); ++i) { + + // don't bother with properties we've been asked to store explicitly + if (elem->store_prop[i]) + continue; + + PlyProperty *prop = elem->props[i]; + + // internal types will be same as external + prop->internal_type = prop->external_type; + prop->count_internal = prop->count_external; + + // list case + if (prop->is_list == LIST) { + + // pointer to list + if (type_size == sizeof(void *)) { + prop->offset = size; + size += sizeof(void *); // always use size of a pointer here + } + + // count of number of list elements + if (type_size == ply_type_size[prop->count_external]) { + prop->count_offset = size; + size += ply_type_size[prop->count_external]; + } + } + // string + else if (prop->is_list == STRING) { + // pointer to string + if (type_size == sizeof(char*)) { + prop->offset = size; + size += sizeof(char*); + } + } + // scalar + else if (type_size == ply_type_size[prop->external_type]) { + prop->offset = size; + size += ply_type_size[prop->external_type]; + } + } + + } + + // save the size for the other_props structure + elem->other_size = size; +} + + +/****************************************************************************** +Specify that we want the "other" properties of an element to be tucked +away within the user's structure. + +Entry: +elem - the element that we want to store other_props in +offset - offset to where other_props will be stored inside user's structure + +Exit: +returns pointer to structure containing description of other_props +******************************************************************************/ + +PLY::PlyOtherProp* PLY::get_other_properties(PlyElement* elem, int offset) +{ + // remember that this is the "current" element + which_elem = elem; + + // save the offset to where to store the other_props + elem->other_offset = offset; + + // place the appropriate pointers, etc. in the element's property list + setup_other_props(elem); + + // create structure for describing other_props + PlyOtherProp *other = new PlyOtherProp; + other->name = elem->name; + #if 0 + if (elem->other_offset == NO_OTHER_PROPS) { + other->size = 0; + other->props = NULL; + other->nprops = 0; + return other; + } + #endif + other->size = elem->other_size; + other->props.reserve(elem->props.size()); + + // save descriptions of each "other" property + for (size_t i = 0; i < elem->props.size(); ++i) { + if (elem->store_prop[i]) + continue; + PlyProperty *prop = new PlyProperty; + copy_property(*prop, *elem->props[i]); + other->props.push_back(prop); + } + + // set other_offset pointer appropriately if there are NO other properties + if (other->props.empty()) + elem->other_offset = NO_OTHER_PROPS; + + // return structure + return other; +} + + +/****************************************************************************** +Specify that we want the "other" properties of an element to be tucked +away within the user's structure. The user needn't be concerned for how +these properties are stored. + +Entry: +elem_name - name of element that we want to store other_props in +offset - offset to where other_props will be stored inside user's structure + +Exit: +returns pointer to structure containing description of other_props +******************************************************************************/ + +PLY::PlyOtherProp* PLY::get_other_properties(const char* elem_name, int offset) +{ + // find information about the element + PlyElement *elem = find_element(elem_name); + if (elem == NULL) { + DEBUG("warning: get_other_properties: Can't find element '%s'", elem_name); + return NULL; + } + + PlyOtherProp *other = get_other_properties(elem, offset); + return other; +} + + + + +/*************************/ +/* Other Element Stuff */ +/*************************/ + + + + + +/****************************************************************************** +Grab all the data for the current element that a user does not want to +explicitly read in. Stores this in the PLY object's data structure. + +Entry: + +Exit: +returns pointer to ALL the "other" element data for this PLY file +******************************************************************************/ + +PLY::PlyOtherElems* PLY::get_other_element() +{ + PlyElement *elem = which_elem; + + // create room for the new "other" element, initializing the + // other data structure if necessary + OtherElem other; + + // count of element instances in file + other.elem_count = elem->num; + + // save name of element + other.elem_name = elem->name; + + // create a list to hold all the current elements + other.other_data = new OtherData*[other.elem_count]; + + // set up for getting elements + other.other_props = get_other_properties(elem->name.c_str(), offsetof(OtherData,other_props)); + + // grab all these elements + for (int i = 0; i < other.elem_count; ++i) { + // grab and element from the file + other.other_data[i] = new OtherData; + get_element((uint8_t*)other.other_data[i]); + } + + // return pointer to the other elements data + if (other_elems == NULL) + other_elems = new PlyOtherElems; + other_elems->other_list.push_back(other); + return other_elems; +} + + +/****************************************************************************** +Write out the "other" elements specified for this PLY file. + +Entry: +******************************************************************************/ + +void PLY::put_other_elements() +{ + // make sure we have other elements to write + if (other_elems == NULL) + return; + + // write out the data for each "other" element + for (size_t i = 0; i < other_elems->other_list.size(); ++i) { + OtherElem *other = &(other_elems->other_list[i]); + put_element_setup(other->elem_name.c_str()); + + // write out each instance of the current element + for (int j = 0; j < other->elem_count; ++j) + put_element(other->other_data[j]); + } +} + + + +/*******************/ +/* Miscellaneous */ +/*******************/ + + +/****************************************************************************** +Use old PLY type names during writing for backward compatibility. +******************************************************************************/ + +void PLY::set_legacy_type_names() +{ + write_type_names = old_type_names; +} + + +/****************************************************************************** +Get version number and file type of a PlyFile. + +Entry: + +Exit: +version - version of the file +file_type - PLY_ASCII, PLY_BINARY_BE, or PLY_BINARY_LE +******************************************************************************/ + +void PLY::get_info(float* _version, int* _file_type) +{ + *_version = version; + *_file_type = file_type; +} + + +/****************************************************************************** +Find an element from the element list of a given PLY object. + +Entry: +element - name of element we're looking for + +Exit: +returns the element, or NULL if not found +******************************************************************************/ + +PLY::PlyElement* PLY::find_element(const char* element) const +{ + for (size_t i=0; iname.c_str())) + return elems[i]; + return NULL; +} + + +/****************************************************************************** +Find a property in the list of properties of a given element. + +Entry: +elem - pointer to element in which we want to find the property +prop_name - name of property to find + +Exit: +returns the index to position in list +******************************************************************************/ + +int PLY::find_property(PlyElement* elem, const char* prop_name) const +{ + for (size_t i=0; iprops.size(); ++i) + if (equal_strings(prop_name, elem->props[i]->name.c_str())) + return (int)i; + return -1; +} + + +/****************************************************************************** +Read an element from an ascii file. + +Entry: +elem_ptr - pointer to element +******************************************************************************/ + +void PLY::ascii_get_element(uint8_t* elem_ptr) +{ + char *elem_data, *item; + char *item_ptr; + ValueType val; + char *orig_line; + char *other_data(NULL); + int other_flag(0); + + // the kind of element we're reading currently + PlyElement *elem = which_elem; + + // do we need to setup for other_props? + if (elem->other_offset != NO_OTHER_PROPS) { + other_flag = 1; + // make room for other_props + other_data = new char[elem->other_size]; + // store pointer in user's structure to the other_props + *((char**)(elem_ptr + elem->other_offset)) = other_data; + } + + // read in the element + int nwords; + char **words; + { + STRISTREAM sfp(istream); + words = get_words(sfp, &nwords, &orig_line); + if (words == NULL) + abort_ply("error: get_element: unexpected end of file"); + sfp.emptyBuffer(); + } + int which_word = 0; + + for (size_t j = 0; j < elem->props.size(); ++j) { + PlyProperty *prop = elem->props[j]; + const int store_it(elem->store_prop[j] | other_flag); + + // store either in the user's structure or in other_props + if (elem->store_prop[j]) + elem_data = (char*)elem_ptr; + else + elem_data = other_data; + + if (prop->is_list == LIST) { // a list + // get and store the number of items in the list + get_ascii_item(words[which_word++], prop->count_external, val); + if (store_it) { + item = elem_data + prop->count_offset; + store_item(item, prop->count_internal, val, prop->count_external); + } + + // allocate space for an array of items and store a ptr to the array + const int list_count(ValueType2Type(val, prop->count_external)); + char** store_array = (char**)(elem_data + prop->offset); + if (list_count == 0) { + if (store_it) + *store_array = NULL; + } else { + const int item_size(ply_type_size[prop->internal_type]); + + if (store_it) { + item_ptr = new char[item_size * list_count]; + item = item_ptr; + *store_array = item_ptr; + } + + // read items and store them into the array + for (int k = 0; k < list_count; k++) { + get_ascii_item(words[which_word++], prop->external_type, val); + if (store_it) { + store_item(item, prop->internal_type, val, prop->external_type); + item += item_size; + } + } + } + } else if (prop->is_list == STRING) { // a string + if (store_it) { + item = elem_data + prop->offset; + *((char**)item) = strdup(words[which_word++]); + } else { + which_word++; + } + } else { // a scalar + get_ascii_item(words[which_word++], prop->external_type, val); + if (store_it) { + item = elem_data + prop->offset; + store_item(item, prop->internal_type, val, prop->external_type); + } + } + } + + free(words); +} + + +/****************************************************************************** +Read an element from a binary file. + +Entry: +elem_ptr - pointer to an element +******************************************************************************/ + +void PLY::binary_get_element(uint8_t* elem_ptr) +{ + char *elem_data; + char *item; + char *item_ptr; + ValueType val; + char *other_data(NULL); + int other_flag(0); + + // the kind of element we're reading currently + PlyElement* elem = which_elem; + + // do we need to setup for other_props? + if (elem->other_offset != NO_OTHER_PROPS) { + other_flag = 1; + // make room for other_props + other_data = new char[elem->other_size]; + // store pointer in user's structure to the other_props + *((char**)(elem_ptr + elem->other_offset)) = other_data; + } + + // read in a number of elements + for (size_t j = 0; j < elem->props.size(); ++j) { + PlyProperty *prop = elem->props[j]; + const int store_it(elem->store_prop[j] | other_flag); + + // store either in the user's structure or in other_props + if (elem->store_prop[j]) + elem_data = (char*)elem_ptr; + else + elem_data = other_data; + + if (prop->is_list == LIST) { // list + // get and store the number of items in the list + get_binary_item(prop->count_external, val); + if (store_it) { + item = elem_data + prop->count_offset; + store_item(item, prop->count_internal, val, prop->count_external); + } + + // allocate space for an array of items and store a ptr to the array + const int list_count(ValueType2Type(val, prop->count_external)); + const int item_size(ply_type_size[prop->internal_type]); + char** store_array = (char**)(elem_data + prop->offset); + if (list_count == 0) { + if (store_it) + *store_array = NULL; + } else { + if (store_it) { + item_ptr = new char[item_size * list_count]; + item = item_ptr; + *store_array = item_ptr; + } + + // read items and store them into the array + for (int k = 0; k < list_count; k++) { + get_binary_item(prop->external_type, val); + if (store_it) { + store_item(item, prop->internal_type, val, prop->external_type); + item += item_size; + } + } + } + } else if (prop->is_list == STRING) { // string + int len; + istream->read(&len, sizeof(int)); + char *str = new char[len]; + istream->read(str, len); + if (store_it) { + item = elem_data + prop->offset; + *((char**)item) = str; + } + } else { // scalar + get_binary_item(prop->external_type, val); + if (store_it) { + item = elem_data + prop->offset; + store_item(item, prop->internal_type, val, prop->external_type); + } + } + } +} + + +/****************************************************************************** +Write to a file the word that represents a PLY data type. + +Entry: +code - code for type +******************************************************************************/ + +void PLY::write_scalar_type(int code) +{ + // make sure this is a valid code + if (code <= StartType || code >= EndType) + abort_ply("error: write_scalar_type: bad data code = %d", code); + + // write the code to a file + ostream->print("%s", write_type_names[code]); +} + + +/****************************************************************************** +Get a text line from a file and break it up into words. + +IMPORTANT: The calling routine should call "free" on the returned pointer once +finished with it. + +Entry: +sfp - string file to read from + +Exit: +nwords - number of words returned +orig_line - the original line of characters +returns a list of words from the line, or NULL if end-of-file +******************************************************************************/ + +char** PLY::get_words(STRISTREAM& sfp, int* nwords, char** orig_line) +{ + const int BIG_STRING = 4096; + char str[BIG_STRING]; + char str_copy[BIG_STRING]; + + int max_words = 10; + int num_words = 0; + char *ptr, *ptr2; + + char** words = (char**)malloc(sizeof(char*) * max_words); + + // read in a line + size_t len(sfp.readLine(str, BIG_STRING-2)); + if (len == 0 || len == STREAM_ERROR) { + *nwords = 0; + *orig_line = NULL; + free(words); + return NULL; + } + + // convert line-feed and tabs into spaces + // (this guarantees that there will be a space before the + // null character at the end of the string) + if (str[len-1] == '\r') + --len; + str[len] = '\n'; + + for (ptr = str, ptr2 = str_copy; ; ptr++, ptr2++) { + switch (*ptr) { + case '\t': + *ptr = ' '; + *ptr2 = ' '; + break; + case '\n': + *ptr = ' '; + *(ptr+1) = *ptr2 = '\0'; + goto EXIT_LOOP; + default: + *ptr2 = *ptr; + } + } + EXIT_LOOP: + + // find the words in the line + ptr = str; + while (*ptr != '\0') { + + // jump over leading spaces + while (*ptr == ' ') + ptr++; + + // break if we reach the end + if (*ptr == '\0') + break; + + // allocate more room for words if necessary + if (num_words >= max_words) { + max_words += 10; + words = (char**)realloc(words, sizeof(char*) * max_words); + } + + if (*ptr == '\"') { // a quote indicates that we have a string + // skip over leading quote + ptr++; + + // save pointer to beginning of word + words[num_words++] = ptr; + + // find trailing quote or end of line + while (*ptr != '\"' && *ptr != '\0') + ptr++; + + // replace quote with a null character to mark the end of the word + // if we are not already at the end of the line + if (*ptr != '\0') + *ptr++ = '\0'; + } else { // non-string + // save pointer to beginning of word + words[num_words++] = ptr; + + // jump over non-spaces + while (*ptr != ' ') + ptr++; + + // place a null character here to mark the end of the word + *ptr++ = '\0'; + } + } + + // return the list of words + *nwords = num_words; + *orig_line = str_copy; + return words; +} + + +/****************************************************************************** +Write out an item to a file as raw binary bytes. + +Entry: +val - item value to be written +double_val - value type +type - data type to write out +******************************************************************************/ + +void PLY::write_binary_item( + const ValueType& val, + int from_type, + int to_type + ) +{ + switch (to_type) { + case Int8: { + const int8_t v(ValueType2Type(val, from_type)); + ostream->write(&v, 1); + break; } + case Int16: { + const int16_t v(ValueType2Type(val, from_type)); + ostream->write(&v, 2); + break; } + case Int32: { + const int32_t v(ValueType2Type(val, from_type)); + ostream->write(&v, 4); + break; } + case Uint8: { + const uint8_t v(ValueType2Type(val, from_type)); + ostream->write(&v, 1); + break; } + case Uint16: { + const uint16_t v(ValueType2Type(val, from_type)); + ostream->write(&v, 2); + break; } + case Uint32: { + const uint32_t v(ValueType2Type(val, from_type)); + ostream->write(&v, 4); + break; } + case Float32: { + const float v(ValueType2Type(val, from_type)); + ostream->write(&v, 4); + break; } + case Float64: { + const double v(ValueType2Type(val, from_type)); + ostream->write(&v, 8); + break; } + default: + abort_ply("error: write_binary_item: bad type = %d", to_type); + } +} + + +/****************************************************************************** +Write out an item to a file as ascii characters. + +Entry: +val - item value to be written +double_val - value type +type - data type to write out +******************************************************************************/ + +void PLY::write_ascii_item( + const ValueType& val, + int from_type, + int to_type + ) +{ + switch (to_type) { + case Int8: + case Int16: + case Int32: + ostream->print("%d ", ValueType2Type(val, from_type)); + break; + case Uint8: + case Uint16: + case Uint32: + ostream->print("%u ", ValueType2Type(val, from_type)); + break; + case Float32: + case Float64: + ostream->print("%g ", ValueType2Type(val, from_type)); + break; + default: + abort_ply("error: write_ascii_item: bad type = %d", to_type); + } +} + + +/****************************************************************************** +Get the value of an item that is in memory, and place the result +into an integer, an unsigned integer and a double. + +Entry: +ptr - pointer to the item +type - data type supposedly in the item + +Exit: +val - extracted value +******************************************************************************/ + +void PLY::get_stored_item( + const void* ptr, + int type, + ValueType& val + ) +{ + switch (type) { + case Int8: + val.i8 = *((const int8_t*)ptr); + break; + case Uint8: + val.u8 = *((const uint8_t*)ptr); + break; + case Int16: + val.i16 = *((const int16_t*)ptr); + break; + case Uint16: + val.u16 = *((const uint16_t*)ptr); + break; + case Int32: + val.i32 = *((const int32_t*)ptr); + break; + case Uint32: + val.u32 = *((const uint32_t*)ptr); + break; + case Float32: + val.f = *((const float*)ptr); + break; + case Float64: + val.d = *((const double*)ptr); + break; + default: + abort_ply("error: get_stored_item: bad type = %d", type); + } +} + + +/****************************************************************************** +Get the value of an item from a binary file, and place the result +into an integer, an unsigned integer and a double. + +Entry: +type - data type supposedly in the word + +Exit: +val - store value +******************************************************************************/ + +void PLY::get_binary_item(int type, ValueType& val) +{ + switch (type) { + case Int8: + istream->read(&val.i8, 1); + break; + case Uint8: + istream->read(&val.u8, 1); + break; + case Int16: + istream->read(&val.i16, 2); + break; + case Uint16: + istream->read(&val.u16, 2); + break; + case Int32: + istream->read(&val.i32, 4); + break; + case Uint32: + istream->read(&val.u32, 4); + break; + case Float32: + istream->read(&val.f, 4); + break; + case Float64: + istream->read(&val.d, 8); + break; + default: + abort_ply("error: get_binary_item: bad type = %d", type); + } +} + + +/****************************************************************************** +Extract the value of an item from an ascii word, and place the result +into an integer, an unsigned integer and a double. + +Entry: +word - word to extract value from +type - data type supposedly in the word + +Exit: +val - store value +******************************************************************************/ + +void PLY::get_ascii_item(const char* word, int type, ValueType& val) +{ + switch (type) { + case Int8: + val.i8 = (int8_t)atoi(word); + break; + case Uint8: + val.u8 = (uint8_t)atoi(word); + break; + case Int16: + val.i16 = (int16_t)atoi(word); + break; + case Uint16: + val.u16 = (uint16_t)atoi(word); + break; + case Int32: + val.i32 = atoi(word); + break; + case Uint32: + val.u32 = strtoul(word, (char**)NULL, 10); + break; + case Float32: + val.f = (float)atof(word); + break; + case Float64: + val.d = atof(word); + break; + default: + abort_ply("error: get_ascii_item: bad type = %d", type); + } +} + + +/****************************************************************************** +Store a value into a place being pointed to, guided by a data type. + +Entry: +to_type - data type +val - value to be stored +from_type - value type + +Exit: +ptr - data pointer to stored value +******************************************************************************/ + +void PLY::store_item( + void* ptr, + int to_type, + const ValueType& val, + int from_type + ) +{ + switch (to_type) { + case Int8: + *((int8_t*)ptr) = ValueType2Type(val, from_type); + break; + case Uint8: + *((uint8_t*)ptr) = ValueType2Type(val, from_type); + break; + case Int16: + *((int16_t*)ptr) = ValueType2Type(val, from_type); + break; + case Uint16: + *((uint16_t*)ptr) = ValueType2Type(val, from_type); + break; + case Int32: + *((int32_t*)ptr) = ValueType2Type(val, from_type); + break; + case Uint32: + *((uint32_t*)ptr) = ValueType2Type(val, from_type); + break; + case Float32: + *((float*)ptr) = ValueType2Type(val, from_type); + break; + case Float64: + *((double*)ptr) = ValueType2Type(val, from_type); + break; + default: + abort_ply("error: store_item: bad type = %d", to_type); + } +} + + +/****************************************************************************** +Add an element to a PLY file descriptor. + +Entry: +words - list of words describing the element +nwords - number of words in the list +******************************************************************************/ + +void PLY::add_element(const char** words, int /*nwords*/) +{ + // create the new element + PlyElement *elem = new PlyElement; + elem->name = words[1]; + elem->num = atoi(words[2]); + + // add the new element to the object's list + elems.push_back(elem); +} + + +/****************************************************************************** +Return the type of a property, given the name of the property. + +Entry: +name - name of property type + +Exit: +returns integer code for property, or 0 if not found +******************************************************************************/ + +int PLY::get_prop_type(const char* type_name) +{ + // try to match the type name + for (int i = StartType + 1; i < EndType; ++i) + if (equal_strings (type_name, type_names[i])) + return i; + + // see if we can match an old type name + for (int i = StartType + 1; i < EndType; ++i) + if (equal_strings (type_name, old_type_names[i])) + return i; + + // if we get here, we didn't find the type + return 0; +} + + +/****************************************************************************** +Add a property to a PLY file descriptor. + +Entry: +words - list of words describing the property +nwords - number of words in the list +******************************************************************************/ + +void PLY::add_property(const char** words, int /*nwords*/) +{ + // create the new property + PlyProperty *prop = new PlyProperty; + + if (equal_strings(words[1], "list")) { // list + prop->count_external = get_prop_type (words[2]); + prop->external_type = get_prop_type (words[3]); + prop->name = words[4]; + prop->is_list = LIST; + } else if (equal_strings(words[1], "string")) { // string + prop->count_external = Int8; + prop->external_type = Int8; + prop->name = words[2]; + prop->is_list = STRING; + } else { // scalar + prop->external_type = get_prop_type (words[1]); + prop->name = words[2]; + prop->is_list = SCALAR; + } + + // internal types are the same as external by default + prop->internal_type = prop->external_type; + prop->count_internal = prop->count_external; + + // add this property to the list of properties of the current element + PlyElement *elem = elems.back(); + elem->props.push_back(prop); +} + + +/****************************************************************************** +Add a comment to a PLY file descriptor. + +Entry: +line - line containing comment +******************************************************************************/ + +void PLY::add_comment(const char* line) +{ + // skip over "comment" and leading spaces and tabs + int i = 7; + while (line[i] == ' ' || line[i] == '\t') + i++; + append_comment(&line[i]); +} + + +/****************************************************************************** +Add a some object information to a PLY file descriptor. + +Entry: +line - line containing text info +******************************************************************************/ + +void PLY::add_obj_info(const char* line) +{ + // skip over "obj_info" and leading spaces and tabs + int i = 8; + while (line[i] == ' ' || line[i] == '\t') + i++; + append_obj_info(&line[i]); +} + + +/****************************************************************************** +Copy a property. +******************************************************************************/ + +void PLY::copy_property(PlyProperty& dest, const PlyProperty& src) +{ + dest.name = src.name; + dest.external_type = src.external_type; + dest.internal_type = src.internal_type; + dest.offset = src.offset; + + dest.is_list = src.is_list; + dest.count_external = src.count_external; + dest.count_internal = src.count_internal; + dest.count_offset = src.count_offset; +} + + + +/****************************************************************************** +Return a list of the names of the elements in a particular PLY file. + +Entry: + +Exit: +elem_names - the list of element names +returns the number of elements +******************************************************************************/ + +int PLY::get_element_list(std::vector& elem_names) const +{ + // create the list of element names + elem_names.resize(elems.size()); + for (size_t i = 0; i < elems.size(); ++i) + elem_names[i] = elems[i]->name; + // return the number of elements and the list of element names + return (int)elems.size(); +} + + +/****************************************************************************** +Append a comment to a PLY file. + +Entry: +comment - the comment to append +******************************************************************************/ + +void PLY::append_comment(const char* comment) +{ + // add comment to list + comments.push_back(comment); +} + + +/****************************************************************************** +Copy the comments from one PLY file to another. + +Entry: +out_ply - destination file to copy comments to +in_ply - the source of the comments +******************************************************************************/ + +void PLY::copy_comments(const PLY& in_ply) +{ + for (size_t i = 0; i < in_ply.comments.size(); ++i) + append_comment(in_ply.comments[i].c_str()); +} + + +/****************************************************************************** +Append object information (arbitrary text) to a PLY file. + +Entry: +obj_info - the object info to append +******************************************************************************/ + +void PLY::append_obj_info(const char* _obj_info) +{ + // add info to list + obj_info.push_back(_obj_info); +} + + +/****************************************************************************** +Copy the object information from one PLY file to another. + +Entry: +out_ply - destination file to copy object information to +in_ply - the source of the object information +******************************************************************************/ + +void PLY::copy_obj_info(const PLY& in_ply) +{ + for (size_t i = 0; i < in_ply.obj_info.size(); ++i) + append_obj_info(in_ply.obj_info[i].c_str()); +} + + +/****************************************************************************** +Specify the index of the next element to be read in from a PLY file. + +Entry: +index - index of the element to be read + +Exit: +elem_count - the number of elements in the file +returns pointer to the name of this next element +******************************************************************************/ + +LPCSTR PLY::setup_element_read(int index, int* elem_count) +{ + if ((size_t)index > elems.size()) { + DEBUG("warning: No element with index %d", index); + return 0; + } + + PlyElement* elem = elems[index]; + + // set this to be the current element + which_elem = elem; + + // return the number of such elements in the file and the element's name + *elem_count = elem->num; + return elem->name.c_str(); +} + + +/****************************************************************************** +Specify one of several properties of the current element that is to be +read from a file. This should be called (usually multiple times) before a +call to the routine get_element(). + +Entry: +prop - property to add to those that will be returned +******************************************************************************/ + +void PLY::setup_property(const PlyProperty& prop) +{ + PlyElement *elem = which_elem; + + // deposit the property information into the element's description + int index = find_property(elem, prop.name.c_str()); + if (index == -1) { + DEBUG("warning: Can't find property '%s' in element '%s'", prop.name.c_str(), elem->name.c_str()); + return; + } + PlyProperty *prop_ptr = elem->props[index]; + prop_ptr->internal_type = prop.internal_type; + prop_ptr->offset = prop.offset; + prop_ptr->count_internal = prop.count_internal; + prop_ptr->count_offset = prop.count_offset; + + // specify that the user wants this property + elem->store_prop[index] = STORE_PROP; +} + + +/****************************************************************************** +Specify that we want the "other" properties of the current element to be tucked +away within the user's structure. + +Entry: +offset - offset to where other_props will be stored inside user's structure + +Exit: +returns pointer to structure containing description of other_props +******************************************************************************/ + +PLY::PlyOtherProp* PLY::get_other_properties(int offset) +{ + return get_other_properties(which_elem, offset); +} + + +/****************************************************************************** +Describe which element is to be written next and state how many of them will +be written. + +Entry: +elem_name - name of element that information is being described +nelems - number of elements of this type to be written +******************************************************************************/ + +void PLY::describe_element(const char* elem_name, int nelems) +{ + // look for appropriate element + PlyElement *elem = find_element(elem_name); + if (elem == NULL) + abort_ply("error: describe_element: can't find element '%s'",elem_name); + + elem->num = nelems; + + // now this element is the current element + which_elem = elem; +} + + +/****************************************************************************** +Describe a property of an element. + +Entry: +prop - the new property +******************************************************************************/ + +void PLY::describe_property(const PlyProperty& prop) +{ + PlyElement *elem = which_elem; + + // copy the new property + PlyProperty *elem_prop = new PlyProperty; + copy_property(*elem_prop, prop); + elem->props.push_back(elem_prop); + elem->store_prop.push_back(NAMED_PROP); +} + + +/****************************************************************************** +Describe what the "other" properties are that are to be stored, and where +they are in an element. +******************************************************************************/ + +void PLY::describe_other_properties( + PlyOtherProp *other, + int offset + ) +{ + // look for appropriate element + PlyElement *elem = find_element(other->name.c_str()); + if (elem == NULL) { + DEBUG("warning: describe_other_properties: can't find element '%s'", other->name.c_str()); + return; + } + + // copy the other properties + for (size_t i = 0; i < other->props.size(); ++i) { + PlyProperty *prop = new PlyProperty; + copy_property(*prop, *other->props[i]); + elem->props.push_back(prop); + elem->store_prop.push_back(OTHER_PROP); + } + + // save other info about other properties + elem->other_size = other->size; + elem->other_offset = offset; +} + + +/****************************************************************************** +Pass along a pointer to "other" elements that we want to save in a given +PLY file. These other elements were presumably read from another PLY file. + +Entry: +other_elems - info about other elements that we want to store +******************************************************************************/ + +void PLY::describe_other_elements(PlyOtherElems* _other_elems) +{ + // ignore this call if there is no other element + if (_other_elems == NULL) + return; + + // save pointer to this information + other_elems = _other_elems; + + // describe the other properties of this element + for (size_t i = 0; i < other_elems->other_list.size(); ++i) { + OtherElem *other = &(other_elems->other_list[i]); + element_count(other->elem_name.c_str(), other->elem_count); + describe_other_properties(other->other_props, offsetof(OtherData,other_props)); + } +} + + + +/****************************************************************************** +Initialize the property propagation rules for an element. Default is to +use averaging (AVERAGE_RULE) for creating all new properties. + +Entry: +elem_name - name of the element that we're making the rules for + +Exit: +returns pointer to the default rules +******************************************************************************/ + +PLY::PlyPropRules* PLY::init_rule(const char* elem_name) +{ + PlyElement *elem = find_element(elem_name); + if (elem == NULL) + abort_ply("error: init_rule: Can't find element '%s'", elem_name); + + PlyPropRules *rules = new PlyPropRules; + rules->elem = elem; + rules->max_props = 0; + rules->rule_list = NULL; + + // see if there are other rules we should use + if (elem->props.empty()) + return rules; + + // default is to use averaging rule + rules->rule_list = new int[elem->props.size()]; + for (size_t i = 0; i < elem->props.size(); ++i) + rules->rule_list[i] = AVERAGE_RULE; + + // try to match the element, property and rule name + for (PlyRuleList *list = rule_list; list != NULL; list = list->next) { + + if (!equal_strings(list->element, elem->name.c_str())) + continue; + + int found_prop = 0; + for (size_t i = 0; i < elem->props.size(); ++i) + if (equal_strings(list->property, elem->props[i]->name.c_str())) { + + found_prop = 1; + + // look for matching rule name + for (int j = 0; rule_name_list[j].code != -1; ++j) + if (equal_strings(list->name, rule_name_list[j].name.c_str())) { + rules->rule_list[i] = rule_name_list[j].code; + break; + } + } + + if (!found_prop) { + DEBUG("warning: Can't find property '%s' for rule '%s'", list->property, list->name); + continue; + } + } + + return rules; +} + + +/****************************************************************************** +Modify a property propagation rule. + +Entry: +rules - rules for the element +prop_name - name of the property whose rule we're modifying +rule_type - type of rule (MAXIMUM_RULE, MINIMUM_RULE, MAJORITY_RULE, etc.) +******************************************************************************/ + +void PLY::modify_rule(PlyPropRules* rules, const char* prop_name, int rule_type) +{ + PlyElement *elem = rules->elem; + + // find the property and modify its rule type + for (size_t i = 0; i < elem->props.size(); ++i) + if (equal_strings(elem->props[i]->name.c_str(), prop_name)) { + rules->rule_list[i] = rule_type; + return; + } + + // we didn't find the property if we get here + abort_ply("error: modify_rule: Can't find property '%s'", prop_name); +} + + +/****************************************************************************** +Begin to create a set of properties from a set of propagation rules. + +Entry: +rules - rules for the element +******************************************************************************/ + +void PLY::start_props(PlyPropRules* rules) +{ + // save pointer to the rules in the PLY object + current_rules = rules; +} + + +/****************************************************************************** +Remember a set of properties and their weights for creating a new set of +properties. + +Entry: +weight - weights for this set of properties +other_props - the properties to use +******************************************************************************/ + +void PLY::weight_props(float weight, void* other_props) +{ + PlyPropRules *rules = current_rules; + + // allocate space for properties and weights, if necessary + if (rules->max_props == 0) { + rules->max_props = 6; + } + if (rules->props.size() == rules->max_props) { + rules->max_props *= 2; + } + rules->props.reserve(rules->max_props); + rules->weights.reserve(rules->max_props); + + // remember these new properties and their weights + rules->props.push_back(other_props); + rules->weights.push_back(weight); +} + + +/****************************************************************************** +Return a pointer to a new set of properties that have been created using +a specified set of property combination rules and a given collection of +"other" properties. + +Exit: +returns a pointer to the new properties +******************************************************************************/ + +void* PLY::get_new_props() +{ + PlyPropRules *rules = current_rules; + PlyElement *elem = rules->elem; + PlyProperty *prop; + int offset; + int type; + ValueType val; + + // return NULL if we've got no "other" properties + if (elem->other_size == 0) + return NULL; + + // create room for combined other properties + char *new_data = new char[elem->other_size]; + + // make sure there is enough room to store values we're to combine + vals.resize(rules->props.size()); + + // calculate the combination for each "other" property of the element + for (size_t i = 0; i < elem->props.size(); ++i) { + + // don't bother with properties we've been asked to store explicitly + if (elem->store_prop[i]) + continue; + + prop = elem->props[i]; + offset = prop->offset; + type = prop->external_type; + + // collect together all the values we're to combine + for (size_t j = 0; j < rules->props.size(); ++j) { + char* data = (char*)rules->props[j]; + void* ptr = (void *)(data + offset); + get_stored_item((void*)ptr, type, val); + vals[j] = ValueType2Type(val, type); + } + + // calculate the combined value + switch (rules->rule_list[i]) { + case AVERAGE_RULE: { + double sum = 0; + double weight_sum = 0; + for (size_t j = 0; j < rules->props.size(); ++j) { + sum += vals[j] * rules->weights[j]; + weight_sum += rules->weights[j]; + } + val.d = sum / weight_sum; + break; + } + case MINIMUM_RULE: { + val.d = vals[0]; + for (size_t j = 1; j < rules->props.size(); ++j) + if (val.d > vals[j]) + val.d = vals[j]; + break; + } + case MAXIMUM_RULE: { + val.d = vals[0]; + for (size_t j = 1; j < rules->props.size(); ++j) + if (val.d < vals[j]) + val.d = vals[j]; + break; + } + case RANDOM_RULE: { + val.d = vals[FLOOR2INT(SEACAVE::random() * rules->props.size())]; + break; + } + case SAME_RULE: { + val.d = vals[0]; + for (size_t j = 1; j < rules->props.size(); ++j) + if (val.d != vals[j]) + abort_ply("error: get_new_props: Error combining properties that should be the same"); + break; + } + default: + abort_ply("error: get_new_props: Bad rule = %d", rules->rule_list[i]); + } + + // store the combined value + store_item(new_data + offset, type, val, Float64); + } + + return new_data; +} + + +/****************************************************************************** +Set the list of user-specified property combination rules. +******************************************************************************/ + +void PLY::set_prop_rules(PlyRuleList* prop_rules) +{ + rule_list = prop_rules; +} + + +/****************************************************************************** +Append a property rule to a growing list of user-specified rules. + +Entry: +rule_list - current rule list +name - name of property combination rule +property - "element.property" says which property the rule affects + +Exit: +returns pointer to the new rule list +******************************************************************************/ + +PLY::PlyRuleList* PLY::append_prop_rule( + PlyRuleList* rule_list, + const char* name, + const char* property + ) +{ + char *str2; + char *ptr; + + // find . + char *str = strdup(property); + for (ptr = str; *ptr != '\0' && *ptr != '.'; ptr++) ; + + // split string at . + if (*ptr == '.') { + *ptr = '\0'; + str2 = ptr + 1; + } else { + DEBUG("warning: Can't find property '%s' for rule '%s'", property, name); + return rule_list; + } + + PlyRuleList *rule = new PlyRuleList; + rule->name = name; + rule->element = str; + rule->property = str2; + rule->next = NULL; + + // either start rule list or append to it + if (rule_list == NULL) + rule_list = rule; + else { // append new rule to current list + PlyRuleList *rule_ptr = rule_list; + while (rule_ptr->next != NULL) + rule_ptr = rule_ptr->next; + rule_ptr->next = rule; + } + + // return pointer to list + return rule_list; +} + + +/****************************************************************************** +See if a name matches the name of any property combination rules. + +Entry: +name - name of rule we're trying to match + +Exit: +returns 1 if we find a match, 0 if not +******************************************************************************/ + +int PLY::matches_rule_name(const char* name) +{ + for (int i = 0; rule_name_list[i].code != -1; ++i) + if (equal_strings(rule_name_list[i].name.c_str(), name)) + return 1; + return 0; +} diff --git a/libs/IO/PLY.h b/libs/IO/PLY.h new file mode 100644 index 0000000..44a0f25 --- /dev/null +++ b/libs/IO/PLY.h @@ -0,0 +1,315 @@ +//////////////////////////////////////////////////////////////////// +// PLY.h +// +// Copyright 2023 cDc@seacave +// Distributed under the Boost Software License, Version 1.0 +// (See http://www.boost.org/LICENSE_1_0.txt) +// +// PLY polygon files parser. +// (originally by Greg Turk, heavily modified by cDc@seacave) +// +// A PLY file contains a single polygonal _object_. +// +// An object is composed of lists of _elements_. Typical elements are +// vertices, faces, edges and materials. +// +// Each type of element for a given object has one or more _properties_ +// associated with the element type. For instance, a vertex element may +// have as properties three floating-point values x,y,z and three unsigned +// chars for red, green and blue. + + +#ifndef __SEACAVE_PLY_H__ +#define __SEACAVE_PLY_H__ + + +// I N C L U D E S ///////////////////////////////////////////////// + + +// D E F I N E S /////////////////////////////////////////////////// + +#define PLY_OKAY 0 // ply routine worked okay +#define PLY_ERROR -1 // error in ply routine + + +// S T R U C T S /////////////////////////////////////////////////// + +class IO_API PLY +{ +public: + // scalar data types supported by PLY format + enum FileType { + ASCII = 1, // ascii PLY file + BINARY_BE = 2, // binary PLY file, big endian + BINARY_LE = 3, // binary PLY file, little endian + }; + enum DataType { + StartType = 0, + Int8 = 1, + Int16 = 2, + Int32 = 3, + Uint8 = 4, + Uint16 = 5, + Uint32 = 6, + Float32 = 7, + Float64 = 8, + EndType = 9, + }; + enum ListType { + SCALAR = 0, + LIST = 1, + STRING = 2, + }; + enum RuleType { + AVERAGE_RULE = 1, + MAJORITY_RULE = 2, + MINIMUM_RULE = 3, + MAXIMUM_RULE = 4, + SAME_RULE = 5, + RANDOM_RULE = 6, + }; + + // description of a property + struct PlyProperty { + std::string name; // property name + int external_type; // file's data type + int internal_type; // program's data type + int offset; // offset bytes of prop in a struct + + int is_list; // 0 = scalar, 1 = list, 2 = char string + int count_external; // file's count type + int count_internal; // program's count type + int count_offset; // offset byte for list count + }; + + // description of an element + struct PlyElement { + std::string name; // element name + int num; // number of elements in this object + int size; // size of element (bytes) or -1 if variable + std::vector props;// list of properties in the file + std::vector store_prop; // flags: property wanted by user? + int other_offset; // offset to un-asked-for props, or -1 if none + int other_size; // size of other_props structure + }; + + // describes other properties in an element + struct PlyOtherProp { + std::string name; // element name + int size; // size of other_props + std::vector props;// list of properties in other_props + }; + + // for storing other_props for an other element + struct OtherData { + void* other_props; + }; + + // data for one "other" element + struct OtherElem { + std::string elem_name; // names of other elements + int elem_count; // count of instances of each element + OtherData** other_data; // actual property data for the elements + PlyOtherProp* other_props; // description of the property data + }; + + // "other" elements, not interpreted by user + struct PlyOtherElems { + std::vector other_list;// list of data for other elements + }; + + // rules for combining "other" properties + struct PlyPropRules { + PlyElement* elem; // element whose rules we are making + int* rule_list; // types of rules (AVERAGE_PLY, MAJORITY_PLY, etc.) + uint32_t max_props; // maximum number of properties we have room for now + std::vector props; // list of properties we're combining + std::vector weights; // list of weights of the properties + }; + + struct PlyRuleList { + LPCSTR name; // name of the rule + char* element; // name of element that rule applies to + char* property; // name of property that rule applies to + struct PlyRuleList* next; // pointer for linked list of rules + }; + + // property propagation rules + struct RuleName { + int code; + std::string name; + }; + +protected: + struct ValueType { + union { + int8_t i8; + int16_t i16; + int32_t i32; + uint8_t u8; + uint16_t u16; + uint32_t u32; + float f; + double d; + }; + }; + +public: + PLY(); + ~PLY(); + + bool read(LPCSTR); + bool read(SEACAVE::ISTREAM*); + bool write(LPCSTR, int, LPCSTR*, int, size_t memBufferSize=0); + bool write(SEACAVE::OSTREAM*, int, LPCSTR*, int, size_t memBufferSize=0); + void release(); + + void set_legacy_type_names(); + + void get_info(float*, int*); + + void append_comment(const char*); + void append_obj_info(const char*); + void copy_comments(const PLY&); + void copy_obj_info(const PLY&); + std::vector& get_comments(); + std::vector& get_obj_info(); + + void describe_property(const char*, const PlyProperty&); + void describe_property(const char*, int nprops, const PlyProperty*); + void get_property(const char*, PlyProperty*); + void get_element(void*); + + PlyOtherElems* get_other_element(); + + int get_element_list(std::vector&) const; + void setup_property(const PlyProperty&); + LPCSTR setup_element_read(int, int*); + PlyOtherProp* get_other_properties(int); + + int get_elements_count() const { return (int)elems.size(); } + int get_current_element_count() const { return which_elem->num; } + void element_count(const char*, int); + void describe_element(const char*, int); + void describe_property(const PlyProperty&); + void describe_other_properties(PlyOtherProp*, int); + void describe_other_elements( PlyOtherElems*); + void get_element_setup(const char*, int, PlyProperty*); + int get_element_description(const char*, std::vector&) const; + void element_layout(const char*, int, int, PlyProperty*); + + bool header_complete(); + void put_element_setup(const char*); + void put_element(const void*); + void put_other_elements(); + + PlyPropRules* init_rule(const char*); + void modify_rule(PlyPropRules*, const char*, int); + void start_props(PlyPropRules*); + void weight_props(float, void*); + void* get_new_props(); + void set_prop_rules(PlyRuleList*); + PlyRuleList* append_prop_rule(PlyRuleList*, const char*, const char*); + + // find an element in a ply's list + PlyElement* find_element(const char*) const; + // find a property in an element's list + int find_property(PlyElement*, const char*) const; + + static inline bool equal_strings(const char* s1, const char* s2) { return _tcscmp(s1, s2) == 0; } + +protected: + // write to a file the word describing a PLY file data type + void write_scalar_type(int); + + // read a line from a file and break it up into separate words + typedef SEACAVE::TokenInputStream STRISTREAM; + char** get_words(STRISTREAM&, int*, char**); + + // write an item to a file + void write_binary_item(const ValueType&, int, int); + void write_ascii_item(const ValueType&, int, int); + + // return the value of a stored item + static void get_stored_item(const void*, int, ValueType&); + + // get binary or ascii item and store it according to ptr and type + void get_binary_item(int, ValueType&); + static void get_ascii_item(const char*, int, ValueType&); + + // store a value into where a pointer and a type specify + static void store_item(void*, int, const ValueType&, int); + + // add information to a PLY file descriptor + void add_element(const char**, int); + void add_property(const char**, int); + void add_comment(const char*); + void add_obj_info(const char*); + + // get a bunch of elements from a file + void ascii_get_element(uint8_t*); + void binary_get_element(uint8_t*); + + void setup_other_props(PlyElement*); + PlyOtherProp* get_other_properties(PlyElement*, int); + PlyOtherProp* get_other_properties(const char*, int); + + int matches_rule_name(const char*); + int get_prop_type(const char*); + + // copy a property + static void copy_property(PlyProperty&, const PlyProperty&); + + // return the value as T stored in val as type + template + static inline T ValueType2Type(const ValueType& val, int type) { + switch (type) { + case Int8: + return (T)val.i8; + case Int16: + return (T)val.i16; + case Int32: + return (T)val.i32; + case Uint8: + return (T)val.u8; + case Uint16: + return (T)val.u16; + case Uint32: + return (T)val.u32; + case Float32: + return (T)val.f; + case Float64: + return (T)val.d; + } + ASSERT("error: bad type" == NULL); + return T(0); + } + +protected: + // description of PLY file + std::string filename; // file name + int file_type; // ascii or binary + float version; // version number of file + std::vector elems; // list of elements + std::vector comments;// list of comments + std::vector obj_info;// list of object info items + PlyElement* which_elem; // element we're currently reading or writing + PlyOtherElems* other_elems; // "other" elements from a PLY file + PlyPropRules* current_rules; // current propagation rules + PlyRuleList* rule_list; // rule list from user + std::vector vals; // rule list from user + + union { + SEACAVE::ISTREAM* istream; // input file pointer + SEACAVE::OSTREAM* ostream; // output file pointer + }; + SEACAVE::MemFile* mfp; // mem-file pointer (optional) + const char* const* write_type_names; // names of scalar types to be used for writing (new types by default) + + static const char* const type_names[9]; // names of scalar types + static const char* const old_type_names[9]; // old names of types for backward compatibility + static const int ply_type_size[9]; + static const RuleName rule_name_list[7]; +}; + +#endif // __SEACAVE_PLY_H__ diff --git a/libs/IO/TinyXML2.cpp b/libs/IO/TinyXML2.cpp new file mode 100644 index 0000000..bea3414 --- /dev/null +++ b/libs/IO/TinyXML2.cpp @@ -0,0 +1,2987 @@ +/* +Original code by Lee Thomason (www.grinninglizard.com) + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any +damages arising from the use of this software. + +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must +not claim that you wrote the original software. If you use this +software in a product, an acknowledgment in the product documentation +would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and +must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source +distribution. +*/ + +#include "Common.h" +#include "TinyXML2.h" + +#include // yes, this one new style header, is in the Android SDK. +#if defined(ANDROID_NDK) || defined(__BORLANDC__) || defined(__QNXNTO__) +# include +# include +#else +# include +# include +#endif + +#if defined(_MSC_VER) && (_MSC_VER >= 1400 ) && (!defined WINCE) + // Microsoft Visual Studio, version 2005 and higher. Not WinCE. + /*int _snprintf_s( + char *buffer, + size_t sizeOfBuffer, + size_t count, + const char *format [, + argument] ... + );*/ + static inline int TIXML_SNPRINTF( char* buffer, size_t size, const char* format, ... ) + { + va_list va; + va_start( va, format ); + const int result = vsnprintf_s( buffer, size, _TRUNCATE, format, va ); + va_end( va ); + return result; + } + + static inline int TIXML_VSNPRINTF( char* buffer, size_t size, const char* format, va_list va ) + { + const int result = vsnprintf_s( buffer, size, _TRUNCATE, format, va ); + return result; + } + + #define TIXML_VSCPRINTF _vscprintf + #define TIXML_SSCANF sscanf_s +#elif defined _MSC_VER + // Microsoft Visual Studio 2003 and earlier or WinCE + #define TIXML_SNPRINTF _snprintf + #define TIXML_VSNPRINTF _vsnprintf + #define TIXML_SSCANF sscanf + #if (_MSC_VER < 1400 ) && (!defined WINCE) + // Microsoft Visual Studio 2003 and not WinCE. + #define TIXML_VSCPRINTF _vscprintf // VS2003's C runtime has this, but VC6 C runtime or WinCE SDK doesn't have. + #else + // Microsoft Visual Studio 2003 and earlier or WinCE. + static inline int TIXML_VSCPRINTF( const char* format, va_list va ) + { + int len = 512; + for (;;) { + len = len*2; + char* str = new char[len](); + const int required = _vsnprintf(str, len, format, va); + delete[] str; + if ( required != -1 ) { + TIXMLASSERT( required >= 0 ); + len = required; + break; + } + } + TIXMLASSERT( len >= 0 ); + return len; + } + #endif +#else + // GCC version 3 and higher + //#warning( "Using sn* functions." ) + #define TIXML_SNPRINTF snprintf + #define TIXML_VSNPRINTF vsnprintf + static inline int TIXML_VSCPRINTF( const char* format, va_list va ) + { + int len = vsnprintf( 0, 0, format, va ); + TIXMLASSERT( len >= 0 ); + return len; + } + #define TIXML_SSCANF sscanf +#endif + +#if defined(_WIN64) + #define TIXML_FSEEK _fseeki64 + #define TIXML_FTELL _ftelli64 +#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__ANDROID__) + #define TIXML_FSEEK fseeko + #define TIXML_FTELL ftello +#elif defined(__unix__) && defined(__x86_64__) + #define TIXML_FSEEK fseeko64 + #define TIXML_FTELL ftello64 +#else + #define TIXML_FSEEK fseek + #define TIXML_FTELL ftell +#endif + + +static const char LINE_FEED = static_cast(0x0a); // all line endings are normalized to LF +static const char LF = LINE_FEED; +static const char CARRIAGE_RETURN = static_cast(0x0d); // CR gets filtered out +static const char CR = CARRIAGE_RETURN; +static const char SINGLE_QUOTE = '\''; +static const char DOUBLE_QUOTE = '\"'; + +// Bunch of unicode info at: +// http://www.unicode.org/faq/utf_bom.html +// ef bb bf (Microsoft "lead bytes") - designates UTF-8 + +static const unsigned char TIXML_UTF_LEAD_0 = 0xefU; +static const unsigned char TIXML_UTF_LEAD_1 = 0xbbU; +static const unsigned char TIXML_UTF_LEAD_2 = 0xbfU; + +namespace tinyxml2 +{ + +struct Entity { + const char* pattern; + int length; + char value; +}; + +static const int NUM_ENTITIES = 5; +static const Entity entities[NUM_ENTITIES] = { + { "quot", 4, DOUBLE_QUOTE }, + { "amp", 3, '&' }, + { "apos", 4, SINGLE_QUOTE }, + { "lt", 2, '<' }, + { "gt", 2, '>' } +}; + + +StrPair::~StrPair() +{ + Reset(); +} + + +void StrPair::TransferTo( StrPair* other ) +{ + if ( this == other ) { + return; + } + // This in effect implements the assignment operator by "moving" + // ownership (as in auto_ptr). + + TIXMLASSERT( other != 0 ); + TIXMLASSERT( other->_flags == 0 ); + TIXMLASSERT( other->_start == 0 ); + TIXMLASSERT( other->_end == 0 ); + + other->Reset(); + + other->_flags = _flags; + other->_start = _start; + other->_end = _end; + + _flags = 0; + _start = 0; + _end = 0; +} + + +void StrPair::Reset() +{ + if ( _flags & NEEDS_DELETE ) { + delete [] _start; + } + _flags = 0; + _start = 0; + _end = 0; +} + + +void StrPair::SetStr( const char* str, int flags ) +{ + TIXMLASSERT( str ); + Reset(); + size_t len = strlen( str ); + TIXMLASSERT( _start == 0 ); + _start = new char[ len+1 ]; + memcpy( _start, str, len+1 ); + _end = _start + len; + _flags = flags | NEEDS_DELETE; +} + + +char* StrPair::ParseText( char* p, const char* endTag, int strFlags, int* curLineNumPtr ) +{ + TIXMLASSERT( p ); + TIXMLASSERT( endTag && *endTag ); + TIXMLASSERT(curLineNumPtr); + + char* start = p; + const char endChar = *endTag; + size_t length = strlen( endTag ); + + // Inner loop of text parsing. + while ( *p ) { + if ( *p == endChar && strncmp( p, endTag, length ) == 0 ) { + Set( start, p, strFlags ); + return p + length; + } else if (*p == '\n') { + ++(*curLineNumPtr); + } + ++p; + TIXMLASSERT( p ); + } + return 0; +} + + +char* StrPair::ParseName( char* p ) +{ + if ( !p || !(*p) ) { + return 0; + } + if ( !XMLUtil::IsNameStartChar( (unsigned char) *p ) ) { + return 0; + } + + char* const start = p; + ++p; + while ( *p && XMLUtil::IsNameChar( (unsigned char) *p ) ) { + ++p; + } + + Set( start, p, 0 ); + return p; +} + + +void StrPair::CollapseWhitespace() +{ + // Adjusting _start would cause undefined behavior on delete[] + TIXMLASSERT( ( _flags & NEEDS_DELETE ) == 0 ); + // Trim leading space. + _start = XMLUtil::SkipWhiteSpace( _start, 0 ); + + if ( *_start ) { + const char* p = _start; // the read pointer + char* q = _start; // the write pointer + + while( *p ) { + if ( XMLUtil::IsWhiteSpace( *p )) { + p = XMLUtil::SkipWhiteSpace( p, 0 ); + if ( *p == 0 ) { + break; // don't write to q; this trims the trailing space. + } + *q = ' '; + ++q; + } + *q = *p; + ++q; + ++p; + } + *q = 0; + } +} + + +const char* StrPair::GetStr() +{ + TIXMLASSERT( _start ); + TIXMLASSERT( _end ); + if ( _flags & NEEDS_FLUSH ) { + *_end = 0; + _flags ^= NEEDS_FLUSH; + + if ( _flags ) { + const char* p = _start; // the read pointer + char* q = _start; // the write pointer + + while( p < _end ) { + if ( (_flags & NEEDS_NEWLINE_NORMALIZATION) && *p == CR ) { + // CR-LF pair becomes LF + // CR alone becomes LF + // LF-CR becomes LF + if ( *(p+1) == LF ) { + p += 2; + } + else { + ++p; + } + *q = LF; + ++q; + } + else if ( (_flags & NEEDS_NEWLINE_NORMALIZATION) && *p == LF ) { + if ( *(p+1) == CR ) { + p += 2; + } + else { + ++p; + } + *q = LF; + ++q; + } + else if ( (_flags & NEEDS_ENTITY_PROCESSING) && *p == '&' ) { + // Entities handled by tinyXML2: + // - special entities in the entity table [in/out] + // - numeric character reference [in] + // 中 or 中 + + if ( *(p+1) == '#' ) { + const int buflen = 10; + char buf[buflen] = { 0 }; + int len = 0; + const char* adjusted = const_cast( XMLUtil::GetCharacterRef( p, buf, &len ) ); + if ( adjusted == 0 ) { + *q = *p; + ++p; + ++q; + } + else { + TIXMLASSERT( 0 <= len && len <= buflen ); + TIXMLASSERT( q + len <= adjusted ); + p = adjusted; + memcpy( q, buf, len ); + q += len; + } + } + else { + bool entityFound = false; + for( int i = 0; i < NUM_ENTITIES; ++i ) { + const Entity& entity = entities[i]; + if ( strncmp( p + 1, entity.pattern, entity.length ) == 0 + && *( p + entity.length + 1 ) == ';' ) { + // Found an entity - convert. + *q = entity.value; + ++q; + p += entity.length + 2; + entityFound = true; + break; + } + } + if ( !entityFound ) { + // fixme: treat as error? + ++p; + ++q; + } + } + } + else { + *q = *p; + ++p; + ++q; + } + } + *q = 0; + } + // The loop below has plenty going on, and this + // is a less useful mode. Break it out. + if ( _flags & NEEDS_WHITESPACE_COLLAPSING ) { + CollapseWhitespace(); + } + _flags = (_flags & NEEDS_DELETE); + } + TIXMLASSERT( _start ); + return _start; +} + + + + +// --------- XMLUtil ----------- // + +const char* XMLUtil::writeBoolTrue = "true"; +const char* XMLUtil::writeBoolFalse = "false"; + +void XMLUtil::SetBoolSerialization(const char* writeTrue, const char* writeFalse) +{ + static const char* defTrue = "true"; + static const char* defFalse = "false"; + + writeBoolTrue = (writeTrue) ? writeTrue : defTrue; + writeBoolFalse = (writeFalse) ? writeFalse : defFalse; +} + + +const char* XMLUtil::ReadBOM( const char* p, bool* bom ) +{ + TIXMLASSERT( p ); + TIXMLASSERT( bom ); + *bom = false; + const unsigned char* pu = reinterpret_cast(p); + // Check for BOM: + if ( *(pu+0) == TIXML_UTF_LEAD_0 + && *(pu+1) == TIXML_UTF_LEAD_1 + && *(pu+2) == TIXML_UTF_LEAD_2 ) { + *bom = true; + p += 3; + } + TIXMLASSERT( p ); + return p; +} + + +void XMLUtil::ConvertUTF32ToUTF8( unsigned long input, char* output, int* length ) +{ + const unsigned long BYTE_MASK = 0xBF; + const unsigned long BYTE_MARK = 0x80; + const unsigned long FIRST_BYTE_MARK[7] = { 0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }; + + if (input < 0x80) { + *length = 1; + } + else if ( input < 0x800 ) { + *length = 2; + } + else if ( input < 0x10000 ) { + *length = 3; + } + else if ( input < 0x200000 ) { + *length = 4; + } + else { + *length = 0; // This code won't convert this correctly anyway. + return; + } + + output += *length; + + // Scary scary fall throughs are annotated with carefully designed comments + // to suppress compiler warnings such as -Wimplicit-fallthrough in gcc + switch (*length) { + case 4: + --output; + *output = static_cast((input | BYTE_MARK) & BYTE_MASK); + input >>= 6; + //fall through + case 3: + --output; + *output = static_cast((input | BYTE_MARK) & BYTE_MASK); + input >>= 6; + //fall through + case 2: + --output; + *output = static_cast((input | BYTE_MARK) & BYTE_MASK); + input >>= 6; + //fall through + case 1: + --output; + *output = static_cast(input | FIRST_BYTE_MARK[*length]); + break; + default: + TIXMLASSERT( false ); + } +} + + +const char* XMLUtil::GetCharacterRef( const char* p, char* value, int* length ) +{ + // Presume an entity, and pull it out. + *length = 0; + + if ( *(p+1) == '#' && *(p+2) ) { + unsigned long ucs = 0; + TIXMLASSERT( sizeof( ucs ) >= 4 ); + ptrdiff_t delta = 0; + unsigned mult = 1; + static const char SEMICOLON = ';'; + + if ( *(p+2) == 'x' ) { + // Hexadecimal. + const char* q = p+3; + if ( !(*q) ) { + return 0; + } + + q = strchr( q, SEMICOLON ); + + if ( !q ) { + return 0; + } + TIXMLASSERT( *q == SEMICOLON ); + + delta = q-p; + --q; + + while ( *q != 'x' ) { + unsigned int digit = 0; + + if ( *q >= '0' && *q <= '9' ) { + digit = *q - '0'; + } + else if ( *q >= 'a' && *q <= 'f' ) { + digit = *q - 'a' + 10; + } + else if ( *q >= 'A' && *q <= 'F' ) { + digit = *q - 'A' + 10; + } + else { + return 0; + } + TIXMLASSERT( digit < 16 ); + TIXMLASSERT( digit == 0 || mult <= UINT_MAX / digit ); + const unsigned int digitScaled = mult * digit; + TIXMLASSERT( ucs <= ULONG_MAX - digitScaled ); + ucs += digitScaled; + TIXMLASSERT( mult <= UINT_MAX / 16 ); + mult *= 16; + --q; + } + } + else { + // Decimal. + const char* q = p+2; + if ( !(*q) ) { + return 0; + } + + q = strchr( q, SEMICOLON ); + + if ( !q ) { + return 0; + } + TIXMLASSERT( *q == SEMICOLON ); + + delta = q-p; + --q; + + while ( *q != '#' ) { + if ( *q >= '0' && *q <= '9' ) { + const unsigned int digit = *q - '0'; + TIXMLASSERT( digit < 10 ); + TIXMLASSERT( digit == 0 || mult <= UINT_MAX / digit ); + const unsigned int digitScaled = mult * digit; + TIXMLASSERT( ucs <= ULONG_MAX - digitScaled ); + ucs += digitScaled; + } + else { + return 0; + } + TIXMLASSERT( mult <= UINT_MAX / 10 ); + mult *= 10; + --q; + } + } + // convert the UCS to UTF-8 + ConvertUTF32ToUTF8( ucs, value, length ); + return p + delta + 1; + } + return p+1; +} + + +void XMLUtil::ToStr( int v, char* buffer, int bufferSize ) +{ + TIXML_SNPRINTF( buffer, bufferSize, "%d", v ); +} + + +void XMLUtil::ToStr( unsigned v, char* buffer, int bufferSize ) +{ + TIXML_SNPRINTF( buffer, bufferSize, "%u", v ); +} + + +void XMLUtil::ToStr( bool v, char* buffer, int bufferSize ) +{ + TIXML_SNPRINTF( buffer, bufferSize, "%s", v ? writeBoolTrue : writeBoolFalse); +} + +/* + ToStr() of a number is a very tricky topic. + https://github.com/leethomason/tinyxml2/issues/106 +*/ +void XMLUtil::ToStr( float v, char* buffer, int bufferSize ) +{ + TIXML_SNPRINTF( buffer, bufferSize, "%.8g", v ); +} + + +void XMLUtil::ToStr( double v, char* buffer, int bufferSize ) +{ + TIXML_SNPRINTF( buffer, bufferSize, "%.17g", v ); +} + + +void XMLUtil::ToStr( int64_t v, char* buffer, int bufferSize ) +{ + // horrible syntax trick to make the compiler happy about %lld + TIXML_SNPRINTF(buffer, bufferSize, "%lld", static_cast(v)); +} + +void XMLUtil::ToStr( uint64_t v, char* buffer, int bufferSize ) +{ + // horrible syntax trick to make the compiler happy about %llu + TIXML_SNPRINTF(buffer, bufferSize, "%llu", (long long)v); +} + +bool XMLUtil::ToInt(const char* str, int* value) +{ + if (IsPrefixHex(str)) { + unsigned v; + if (TIXML_SSCANF(str, "%x", &v) == 1) { + *value = static_cast(v); + return true; + } + } + else { + if (TIXML_SSCANF(str, "%d", value) == 1) { + return true; + } + } + return false; +} + +bool XMLUtil::ToUnsigned(const char* str, unsigned* value) +{ + if (TIXML_SSCANF(str, IsPrefixHex(str) ? "%x" : "%u", value) == 1) { + return true; + } + return false; +} + +bool XMLUtil::ToBool( const char* str, bool* value ) +{ + int ival = 0; + if ( ToInt( str, &ival )) { + *value = (ival==0) ? false : true; + return true; + } + static const char* TRUE_VALS[] = { "true", "True", "TRUE", 0 }; + static const char* FALSE_VALS[] = { "false", "False", "FALSE", 0 }; + + for (int i = 0; TRUE_VALS[i]; ++i) { + if (StringEqual(str, TRUE_VALS[i])) { + *value = true; + return true; + } + } + for (int i = 0; FALSE_VALS[i]; ++i) { + if (StringEqual(str, FALSE_VALS[i])) { + *value = false; + return true; + } + } + return false; +} + + +bool XMLUtil::ToFloat( const char* str, float* value ) +{ + if ( TIXML_SSCANF( str, "%f", value ) == 1 ) { + return true; + } + return false; +} + + +bool XMLUtil::ToDouble( const char* str, double* value ) +{ + if ( TIXML_SSCANF( str, "%lf", value ) == 1 ) { + return true; + } + return false; +} + + +bool XMLUtil::ToInt64(const char* str, int64_t* value) +{ + if (IsPrefixHex(str)) { + unsigned long long v = 0; // horrible syntax trick to make the compiler happy about %llx + if (TIXML_SSCANF(str, "%llx", &v) == 1) { + *value = static_cast(v); + return true; + } + } + else { + long long v = 0; // horrible syntax trick to make the compiler happy about %lld + if (TIXML_SSCANF(str, "%lld", &v) == 1) { + *value = static_cast(v); + return true; + } + } + return false; +} + + +bool XMLUtil::ToUnsigned64(const char* str, uint64_t* value) { + unsigned long long v = 0; // horrible syntax trick to make the compiler happy about %llu + if(TIXML_SSCANF(str, IsPrefixHex(str) ? "%llx" : "%llu", &v) == 1) { + *value = (uint64_t)v; + return true; + } + return false; +} + + +char* XMLDocument::Identify( char* p, XMLNode** node ) +{ + TIXMLASSERT( node ); + TIXMLASSERT( p ); + char* const start = p; + int const startLine = _parseCurLineNum; + p = XMLUtil::SkipWhiteSpace( p, &_parseCurLineNum ); + if( !*p ) { + *node = 0; + TIXMLASSERT( p ); + return p; + } + + // These strings define the matching patterns: + static const char* xmlHeader = { "( _commentPool ); + returnNode->_parseLineNum = _parseCurLineNum; + p += xmlHeaderLen; + } + else if ( XMLUtil::StringEqual( p, commentHeader, commentHeaderLen ) ) { + returnNode = CreateUnlinkedNode( _commentPool ); + returnNode->_parseLineNum = _parseCurLineNum; + p += commentHeaderLen; + } + else if ( XMLUtil::StringEqual( p, cdataHeader, cdataHeaderLen ) ) { + XMLText* text = CreateUnlinkedNode( _textPool ); + returnNode = text; + returnNode->_parseLineNum = _parseCurLineNum; + p += cdataHeaderLen; + text->SetCData( true ); + } + else if ( XMLUtil::StringEqual( p, dtdHeader, dtdHeaderLen ) ) { + returnNode = CreateUnlinkedNode( _commentPool ); + returnNode->_parseLineNum = _parseCurLineNum; + p += dtdHeaderLen; + } + else if ( XMLUtil::StringEqual( p, elementHeader, elementHeaderLen ) ) { + returnNode = CreateUnlinkedNode( _elementPool ); + returnNode->_parseLineNum = _parseCurLineNum; + p += elementHeaderLen; + } + else { + returnNode = CreateUnlinkedNode( _textPool ); + returnNode->_parseLineNum = _parseCurLineNum; // Report line of first non-whitespace character + p = start; // Back it up, all the text counts. + _parseCurLineNum = startLine; + } + + TIXMLASSERT( returnNode ); + TIXMLASSERT( p ); + *node = returnNode; + return p; +} + + +bool XMLDocument::Accept( XMLVisitor* visitor ) const +{ + TIXMLASSERT( visitor ); + if ( visitor->VisitEnter( *this ) ) { + for ( const XMLNode* node=FirstChild(); node; node=node->NextSibling() ) { + if ( !node->Accept( visitor ) ) { + break; + } + } + } + return visitor->VisitExit( *this ); +} + + +// --------- XMLNode ----------- // + +XMLNode::XMLNode( XMLDocument* doc ) : + _document( doc ), + _parent( 0 ), + _value(), + _parseLineNum( 0 ), + _firstChild( 0 ), _lastChild( 0 ), + _prev( 0 ), _next( 0 ), + _userData( 0 ), + _memPool( 0 ) +{ +} + + +XMLNode::~XMLNode() +{ + DeleteChildren(); + if ( _parent ) { + _parent->Unlink( this ); + } +} + +const char* XMLNode::Value() const +{ + // Edge case: XMLDocuments don't have a Value. Return null. + if ( this->ToDocument() ) + return 0; + return _value.GetStr(); +} + +void XMLNode::SetValue( const char* str, bool staticMem ) +{ + if ( staticMem ) { + _value.SetInternedStr( str ); + } + else { + _value.SetStr( str ); + } +} + +XMLNode* XMLNode::DeepClone(XMLDocument* target) const +{ + XMLNode* clone = this->ShallowClone(target); + if (!clone) return 0; + + for (const XMLNode* child = this->FirstChild(); child; child = child->NextSibling()) { + XMLNode* childClone = child->DeepClone(target); + TIXMLASSERT(childClone); + clone->InsertEndChild(childClone); + } + return clone; +} + +void XMLNode::DeleteChildren() +{ + while( _firstChild ) { + TIXMLASSERT( _lastChild ); + DeleteChild( _firstChild ); + } + _firstChild = _lastChild = 0; +} + + +void XMLNode::Unlink( XMLNode* child ) +{ + TIXMLASSERT( child ); + TIXMLASSERT( child->_document == _document ); + TIXMLASSERT( child->_parent == this ); + if ( child == _firstChild ) { + _firstChild = _firstChild->_next; + } + if ( child == _lastChild ) { + _lastChild = _lastChild->_prev; + } + + if ( child->_prev ) { + child->_prev->_next = child->_next; + } + if ( child->_next ) { + child->_next->_prev = child->_prev; + } + child->_next = 0; + child->_prev = 0; + child->_parent = 0; +} + + +void XMLNode::DeleteChild( XMLNode* node ) +{ + TIXMLASSERT( node ); + TIXMLASSERT( node->_document == _document ); + TIXMLASSERT( node->_parent == this ); + Unlink( node ); + TIXMLASSERT(node->_prev == 0); + TIXMLASSERT(node->_next == 0); + TIXMLASSERT(node->_parent == 0); + DeleteNode( node ); +} + + +XMLNode* XMLNode::InsertEndChild( XMLNode* addThis ) +{ + TIXMLASSERT( addThis ); + if ( addThis->_document != _document ) { + TIXMLASSERT( false ); + return 0; + } + InsertChildPreamble( addThis ); + + if ( _lastChild ) { + TIXMLASSERT( _firstChild ); + TIXMLASSERT( _lastChild->_next == 0 ); + _lastChild->_next = addThis; + addThis->_prev = _lastChild; + _lastChild = addThis; + + addThis->_next = 0; + } + else { + TIXMLASSERT( _firstChild == 0 ); + _firstChild = _lastChild = addThis; + + addThis->_prev = 0; + addThis->_next = 0; + } + addThis->_parent = this; + return addThis; +} + + +XMLNode* XMLNode::InsertFirstChild( XMLNode* addThis ) +{ + TIXMLASSERT( addThis ); + if ( addThis->_document != _document ) { + TIXMLASSERT( false ); + return 0; + } + InsertChildPreamble( addThis ); + + if ( _firstChild ) { + TIXMLASSERT( _lastChild ); + TIXMLASSERT( _firstChild->_prev == 0 ); + + _firstChild->_prev = addThis; + addThis->_next = _firstChild; + _firstChild = addThis; + + addThis->_prev = 0; + } + else { + TIXMLASSERT( _lastChild == 0 ); + _firstChild = _lastChild = addThis; + + addThis->_prev = 0; + addThis->_next = 0; + } + addThis->_parent = this; + return addThis; +} + + +XMLNode* XMLNode::InsertAfterChild( XMLNode* afterThis, XMLNode* addThis ) +{ + TIXMLASSERT( addThis ); + if ( addThis->_document != _document ) { + TIXMLASSERT( false ); + return 0; + } + + TIXMLASSERT( afterThis ); + + if ( afterThis->_parent != this ) { + TIXMLASSERT( false ); + return 0; + } + if ( afterThis == addThis ) { + // Current state: BeforeThis -> AddThis -> OneAfterAddThis + // Now AddThis must disappear from it's location and then + // reappear between BeforeThis and OneAfterAddThis. + // So just leave it where it is. + return addThis; + } + + if ( afterThis->_next == 0 ) { + // The last node or the only node. + return InsertEndChild( addThis ); + } + InsertChildPreamble( addThis ); + addThis->_prev = afterThis; + addThis->_next = afterThis->_next; + afterThis->_next->_prev = addThis; + afterThis->_next = addThis; + addThis->_parent = this; + return addThis; +} + + + + +const XMLElement* XMLNode::FirstChildElement( const char* name ) const +{ + for( const XMLNode* node = _firstChild; node; node = node->_next ) { + const XMLElement* element = node->ToElementWithName( name ); + if ( element ) { + return element; + } + } + return 0; +} + + +const XMLElement* XMLNode::LastChildElement( const char* name ) const +{ + for( const XMLNode* node = _lastChild; node; node = node->_prev ) { + const XMLElement* element = node->ToElementWithName( name ); + if ( element ) { + return element; + } + } + return 0; +} + + +const XMLElement* XMLNode::NextSiblingElement( const char* name ) const +{ + for( const XMLNode* node = _next; node; node = node->_next ) { + const XMLElement* element = node->ToElementWithName( name ); + if ( element ) { + return element; + } + } + return 0; +} + + +const XMLElement* XMLNode::PreviousSiblingElement( const char* name ) const +{ + for( const XMLNode* node = _prev; node; node = node->_prev ) { + const XMLElement* element = node->ToElementWithName( name ); + if ( element ) { + return element; + } + } + return 0; +} + + +char* XMLNode::ParseDeep( char* p, StrPair* parentEndTag, int* curLineNumPtr ) +{ + // This is a recursive method, but thinking about it "at the current level" + // it is a pretty simple flat list: + // + // + // + // With a special case: + // + // + // + // + // Where the closing element (/foo) *must* be the next thing after the opening + // element, and the names must match. BUT the tricky bit is that the closing + // element will be read by the child. + // + // 'endTag' is the end tag for this node, it is returned by a call to a child. + // 'parentEnd' is the end tag for the parent, which is filled in and returned. + + XMLDocument::DepthTracker tracker(_document); + if (_document->Error()) + return 0; + + while( p && *p ) { + XMLNode* node = 0; + + p = _document->Identify( p, &node ); + TIXMLASSERT( p ); + if ( node == 0 ) { + break; + } + + const int initialLineNum = node->_parseLineNum; + + StrPair endTag; + p = node->ParseDeep( p, &endTag, curLineNumPtr ); + if ( !p ) { + DeleteNode( node ); + if ( !_document->Error() ) { + _document->SetError( XML_ERROR_PARSING, initialLineNum, 0); + } + break; + } + + const XMLDeclaration* const decl = node->ToDeclaration(); + if ( decl ) { + // Declarations are only allowed at document level + // + // Multiple declarations are allowed but all declarations + // must occur before anything else. + // + // Optimized due to a security test case. If the first node is + // a declaration, and the last node is a declaration, then only + // declarations have so far been added. + bool wellLocated = false; + + if (ToDocument()) { + if (FirstChild()) { + wellLocated = + FirstChild() && + FirstChild()->ToDeclaration() && + LastChild() && + LastChild()->ToDeclaration(); + } + else { + wellLocated = true; + } + } + if ( !wellLocated ) { + _document->SetError( XML_ERROR_PARSING_DECLARATION, initialLineNum, "XMLDeclaration value=%s", decl->Value()); + DeleteNode( node ); + break; + } + } + + XMLElement* ele = node->ToElement(); + if ( ele ) { + // We read the end tag. Return it to the parent. + if ( ele->ClosingType() == XMLElement::CLOSING ) { + if ( parentEndTag ) { + ele->_value.TransferTo( parentEndTag ); + } + node->_memPool->SetTracked(); // created and then immediately deleted. + DeleteNode( node ); + return p; + } + + // Handle an end tag returned to this level. + // And handle a bunch of annoying errors. + bool mismatch = false; + if ( endTag.Empty() ) { + if ( ele->ClosingType() == XMLElement::OPEN ) { + mismatch = true; + } + } + else { + if ( ele->ClosingType() != XMLElement::OPEN ) { + mismatch = true; + } + else if ( !XMLUtil::StringEqual( endTag.GetStr(), ele->Name() ) ) { + mismatch = true; + } + } + if ( mismatch ) { + _document->SetError( XML_ERROR_MISMATCHED_ELEMENT, initialLineNum, "XMLElement name=%s", ele->Name()); + DeleteNode( node ); + break; + } + } + InsertEndChild( node ); + } + return 0; +} + +/*static*/ void XMLNode::DeleteNode( XMLNode* node ) +{ + if ( node == 0 ) { + return; + } + TIXMLASSERT(node->_document); + if (!node->ToDocument()) { + node->_document->MarkInUse(node); + } + + MemPool* pool = node->_memPool; + node->~XMLNode(); + pool->Free( node ); +} + +void XMLNode::InsertChildPreamble( XMLNode* insertThis ) const +{ + TIXMLASSERT( insertThis ); + TIXMLASSERT( insertThis->_document == _document ); + + if (insertThis->_parent) { + insertThis->_parent->Unlink( insertThis ); + } + else { + insertThis->_document->MarkInUse(insertThis); + insertThis->_memPool->SetTracked(); + } +} + +const XMLElement* XMLNode::ToElementWithName( const char* name ) const +{ + const XMLElement* element = this->ToElement(); + if ( element == 0 ) { + return 0; + } + if ( name == 0 ) { + return element; + } + if ( XMLUtil::StringEqual( element->Name(), name ) ) { + return element; + } + return 0; +} + +// --------- XMLText ---------- // +char* XMLText::ParseDeep( char* p, StrPair*, int* curLineNumPtr ) +{ + if ( this->CData() ) { + p = _value.ParseText( p, "]]>", StrPair::NEEDS_NEWLINE_NORMALIZATION, curLineNumPtr ); + if ( !p ) { + _document->SetError( XML_ERROR_PARSING_CDATA, _parseLineNum, 0 ); + } + return p; + } + else { + int flags = _document->ProcessEntities() ? StrPair::TEXT_ELEMENT : StrPair::TEXT_ELEMENT_LEAVE_ENTITIES; + if ( _document->WhitespaceMode() == COLLAPSE_WHITESPACE ) { + flags |= StrPair::NEEDS_WHITESPACE_COLLAPSING; + } + + p = _value.ParseText( p, "<", flags, curLineNumPtr ); + if ( p && *p ) { + return p-1; + } + if ( !p ) { + _document->SetError( XML_ERROR_PARSING_TEXT, _parseLineNum, 0 ); + } + } + return 0; +} + + +XMLNode* XMLText::ShallowClone( XMLDocument* doc ) const +{ + if ( !doc ) { + doc = _document; + } + XMLText* text = doc->NewText( Value() ); // fixme: this will always allocate memory. Intern? + text->SetCData( this->CData() ); + return text; +} + + +bool XMLText::ShallowEqual( const XMLNode* compare ) const +{ + TIXMLASSERT( compare ); + const XMLText* text = compare->ToText(); + return ( text && XMLUtil::StringEqual( text->Value(), Value() ) ); +} + + +bool XMLText::Accept( XMLVisitor* visitor ) const +{ + TIXMLASSERT( visitor ); + return visitor->Visit( *this ); +} + + +// --------- XMLComment ---------- // + +XMLComment::XMLComment( XMLDocument* doc ) : XMLNode( doc ) +{ +} + + +XMLComment::~XMLComment() +{ +} + + +char* XMLComment::ParseDeep( char* p, StrPair*, int* curLineNumPtr ) +{ + // Comment parses as text. + p = _value.ParseText( p, "-->", StrPair::COMMENT, curLineNumPtr ); + if ( p == 0 ) { + _document->SetError( XML_ERROR_PARSING_COMMENT, _parseLineNum, 0 ); + } + return p; +} + + +XMLNode* XMLComment::ShallowClone( XMLDocument* doc ) const +{ + if ( !doc ) { + doc = _document; + } + XMLComment* comment = doc->NewComment( Value() ); // fixme: this will always allocate memory. Intern? + return comment; +} + + +bool XMLComment::ShallowEqual( const XMLNode* compare ) const +{ + TIXMLASSERT( compare ); + const XMLComment* comment = compare->ToComment(); + return ( comment && XMLUtil::StringEqual( comment->Value(), Value() )); +} + + +bool XMLComment::Accept( XMLVisitor* visitor ) const +{ + TIXMLASSERT( visitor ); + return visitor->Visit( *this ); +} + + +// --------- XMLDeclaration ---------- // + +XMLDeclaration::XMLDeclaration( XMLDocument* doc ) : XMLNode( doc ) +{ +} + + +XMLDeclaration::~XMLDeclaration() +{ + //printf( "~XMLDeclaration\n" ); +} + + +char* XMLDeclaration::ParseDeep( char* p, StrPair*, int* curLineNumPtr ) +{ + // Declaration parses as text. + p = _value.ParseText( p, "?>", StrPair::NEEDS_NEWLINE_NORMALIZATION, curLineNumPtr ); + if ( p == 0 ) { + _document->SetError( XML_ERROR_PARSING_DECLARATION, _parseLineNum, 0 ); + } + return p; +} + + +XMLNode* XMLDeclaration::ShallowClone( XMLDocument* doc ) const +{ + if ( !doc ) { + doc = _document; + } + XMLDeclaration* dec = doc->NewDeclaration( Value() ); // fixme: this will always allocate memory. Intern? + return dec; +} + + +bool XMLDeclaration::ShallowEqual( const XMLNode* compare ) const +{ + TIXMLASSERT( compare ); + const XMLDeclaration* declaration = compare->ToDeclaration(); + return ( declaration && XMLUtil::StringEqual( declaration->Value(), Value() )); +} + + + +bool XMLDeclaration::Accept( XMLVisitor* visitor ) const +{ + TIXMLASSERT( visitor ); + return visitor->Visit( *this ); +} + +// --------- XMLUnknown ---------- // + +XMLUnknown::XMLUnknown( XMLDocument* doc ) : XMLNode( doc ) +{ +} + + +XMLUnknown::~XMLUnknown() +{ +} + + +char* XMLUnknown::ParseDeep( char* p, StrPair*, int* curLineNumPtr ) +{ + // Unknown parses as text. + p = _value.ParseText( p, ">", StrPair::NEEDS_NEWLINE_NORMALIZATION, curLineNumPtr ); + if ( !p ) { + _document->SetError( XML_ERROR_PARSING_UNKNOWN, _parseLineNum, 0 ); + } + return p; +} + + +XMLNode* XMLUnknown::ShallowClone( XMLDocument* doc ) const +{ + if ( !doc ) { + doc = _document; + } + XMLUnknown* text = doc->NewUnknown( Value() ); // fixme: this will always allocate memory. Intern? + return text; +} + + +bool XMLUnknown::ShallowEqual( const XMLNode* compare ) const +{ + TIXMLASSERT( compare ); + const XMLUnknown* unknown = compare->ToUnknown(); + return ( unknown && XMLUtil::StringEqual( unknown->Value(), Value() )); +} + + +bool XMLUnknown::Accept( XMLVisitor* visitor ) const +{ + TIXMLASSERT( visitor ); + return visitor->Visit( *this ); +} + +// --------- XMLAttribute ---------- // + +const char* XMLAttribute::Name() const +{ + return _name.GetStr(); +} + +const char* XMLAttribute::Value() const +{ + return _value.GetStr(); +} + +char* XMLAttribute::ParseDeep( char* p, bool processEntities, int* curLineNumPtr ) +{ + // Parse using the name rules: bug fix, was using ParseText before + p = _name.ParseName( p ); + if ( !p || !*p ) { + return 0; + } + + // Skip white space before = + p = XMLUtil::SkipWhiteSpace( p, curLineNumPtr ); + if ( *p != '=' ) { + return 0; + } + + ++p; // move up to opening quote + p = XMLUtil::SkipWhiteSpace( p, curLineNumPtr ); + if ( *p != '\"' && *p != '\'' ) { + return 0; + } + + const char endTag[2] = { *p, 0 }; + ++p; // move past opening quote + + p = _value.ParseText( p, endTag, processEntities ? StrPair::ATTRIBUTE_VALUE : StrPair::ATTRIBUTE_VALUE_LEAVE_ENTITIES, curLineNumPtr ); + return p; +} + + +void XMLAttribute::SetName( const char* n ) +{ + _name.SetStr( n ); +} + + +XMLError XMLAttribute::QueryIntValue( int* value ) const +{ + if ( XMLUtil::ToInt( Value(), value )) { + return XML_SUCCESS; + } + return XML_WRONG_ATTRIBUTE_TYPE; +} + + +XMLError XMLAttribute::QueryUnsignedValue( unsigned int* value ) const +{ + if ( XMLUtil::ToUnsigned( Value(), value )) { + return XML_SUCCESS; + } + return XML_WRONG_ATTRIBUTE_TYPE; +} + + +XMLError XMLAttribute::QueryInt64Value(int64_t* value) const +{ + if (XMLUtil::ToInt64(Value(), value)) { + return XML_SUCCESS; + } + return XML_WRONG_ATTRIBUTE_TYPE; +} + + +XMLError XMLAttribute::QueryUnsigned64Value(uint64_t* value) const +{ + if(XMLUtil::ToUnsigned64(Value(), value)) { + return XML_SUCCESS; + } + return XML_WRONG_ATTRIBUTE_TYPE; +} + + +XMLError XMLAttribute::QueryBoolValue( bool* value ) const +{ + if ( XMLUtil::ToBool( Value(), value )) { + return XML_SUCCESS; + } + return XML_WRONG_ATTRIBUTE_TYPE; +} + + +XMLError XMLAttribute::QueryFloatValue( float* value ) const +{ + if ( XMLUtil::ToFloat( Value(), value )) { + return XML_SUCCESS; + } + return XML_WRONG_ATTRIBUTE_TYPE; +} + + +XMLError XMLAttribute::QueryDoubleValue( double* value ) const +{ + if ( XMLUtil::ToDouble( Value(), value )) { + return XML_SUCCESS; + } + return XML_WRONG_ATTRIBUTE_TYPE; +} + + +void XMLAttribute::SetAttribute( const char* v ) +{ + _value.SetStr( v ); +} + + +void XMLAttribute::SetAttribute( int v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + _value.SetStr( buf ); +} + + +void XMLAttribute::SetAttribute( unsigned v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + _value.SetStr( buf ); +} + + +void XMLAttribute::SetAttribute(int64_t v) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr(v, buf, BUF_SIZE); + _value.SetStr(buf); +} + +void XMLAttribute::SetAttribute(uint64_t v) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr(v, buf, BUF_SIZE); + _value.SetStr(buf); +} + + +void XMLAttribute::SetAttribute( bool v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + _value.SetStr( buf ); +} + +void XMLAttribute::SetAttribute( double v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + _value.SetStr( buf ); +} + +void XMLAttribute::SetAttribute( float v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + _value.SetStr( buf ); +} + + +// --------- XMLElement ---------- // +XMLElement::XMLElement( XMLDocument* doc ) : XMLNode( doc ), + _closingType( OPEN ), + _rootAttribute( 0 ) +{ +} + + +XMLElement::~XMLElement() +{ + while( _rootAttribute ) { + XMLAttribute* next = _rootAttribute->_next; + DeleteAttribute( _rootAttribute ); + _rootAttribute = next; + } +} + + +const XMLAttribute* XMLElement::FindAttribute( const char* name ) const +{ + for( XMLAttribute* a = _rootAttribute; a; a = a->_next ) { + if ( XMLUtil::StringEqual( a->Name(), name ) ) { + return a; + } + } + return 0; +} + + +const char* XMLElement::Attribute( const char* name, const char* value ) const +{ + const XMLAttribute* a = FindAttribute( name ); + if ( !a ) { + return 0; + } + if ( !value || XMLUtil::StringEqual( a->Value(), value )) { + return a->Value(); + } + return 0; +} + +int XMLElement::IntAttribute(const char* name, int defaultValue) const +{ + int i = defaultValue; + QueryIntAttribute(name, &i); + return i; +} + +unsigned XMLElement::UnsignedAttribute(const char* name, unsigned defaultValue) const +{ + unsigned i = defaultValue; + QueryUnsignedAttribute(name, &i); + return i; +} + +int64_t XMLElement::Int64Attribute(const char* name, int64_t defaultValue) const +{ + int64_t i = defaultValue; + QueryInt64Attribute(name, &i); + return i; +} + +uint64_t XMLElement::Unsigned64Attribute(const char* name, uint64_t defaultValue) const +{ + uint64_t i = defaultValue; + QueryUnsigned64Attribute(name, &i); + return i; +} + +bool XMLElement::BoolAttribute(const char* name, bool defaultValue) const +{ + bool b = defaultValue; + QueryBoolAttribute(name, &b); + return b; +} + +double XMLElement::DoubleAttribute(const char* name, double defaultValue) const +{ + double d = defaultValue; + QueryDoubleAttribute(name, &d); + return d; +} + +float XMLElement::FloatAttribute(const char* name, float defaultValue) const +{ + float f = defaultValue; + QueryFloatAttribute(name, &f); + return f; +} + +const char* XMLElement::GetText() const +{ + /* skip comment node */ + const XMLNode* node = FirstChild(); + while (node) { + if (node->ToComment()) { + node = node->NextSibling(); + continue; + } + break; + } + + if ( node && node->ToText() ) { + return node->Value(); + } + return 0; +} + + +void XMLElement::SetText( const char* inText ) +{ + if ( FirstChild() && FirstChild()->ToText() ) + FirstChild()->SetValue( inText ); + else { + XMLText* theText = GetDocument()->NewText( inText ); + InsertFirstChild( theText ); + } +} + + +void XMLElement::SetText( int v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + SetText( buf ); +} + + +void XMLElement::SetText( unsigned v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + SetText( buf ); +} + + +void XMLElement::SetText(int64_t v) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr(v, buf, BUF_SIZE); + SetText(buf); +} + +void XMLElement::SetText(uint64_t v) { + char buf[BUF_SIZE]; + XMLUtil::ToStr(v, buf, BUF_SIZE); + SetText(buf); +} + + +void XMLElement::SetText( bool v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + SetText( buf ); +} + + +void XMLElement::SetText( float v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + SetText( buf ); +} + + +void XMLElement::SetText( double v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + SetText( buf ); +} + + +XMLError XMLElement::QueryIntText( int* ival ) const +{ + if ( FirstChild() && FirstChild()->ToText() ) { + const char* t = FirstChild()->Value(); + if ( XMLUtil::ToInt( t, ival ) ) { + return XML_SUCCESS; + } + return XML_CAN_NOT_CONVERT_TEXT; + } + return XML_NO_TEXT_NODE; +} + + +XMLError XMLElement::QueryUnsignedText( unsigned* uval ) const +{ + if ( FirstChild() && FirstChild()->ToText() ) { + const char* t = FirstChild()->Value(); + if ( XMLUtil::ToUnsigned( t, uval ) ) { + return XML_SUCCESS; + } + return XML_CAN_NOT_CONVERT_TEXT; + } + return XML_NO_TEXT_NODE; +} + + +XMLError XMLElement::QueryInt64Text(int64_t* ival) const +{ + if (FirstChild() && FirstChild()->ToText()) { + const char* t = FirstChild()->Value(); + if (XMLUtil::ToInt64(t, ival)) { + return XML_SUCCESS; + } + return XML_CAN_NOT_CONVERT_TEXT; + } + return XML_NO_TEXT_NODE; +} + + +XMLError XMLElement::QueryUnsigned64Text(uint64_t* ival) const +{ + if(FirstChild() && FirstChild()->ToText()) { + const char* t = FirstChild()->Value(); + if(XMLUtil::ToUnsigned64(t, ival)) { + return XML_SUCCESS; + } + return XML_CAN_NOT_CONVERT_TEXT; + } + return XML_NO_TEXT_NODE; +} + + +XMLError XMLElement::QueryBoolText( bool* bval ) const +{ + if ( FirstChild() && FirstChild()->ToText() ) { + const char* t = FirstChild()->Value(); + if ( XMLUtil::ToBool( t, bval ) ) { + return XML_SUCCESS; + } + return XML_CAN_NOT_CONVERT_TEXT; + } + return XML_NO_TEXT_NODE; +} + + +XMLError XMLElement::QueryDoubleText( double* dval ) const +{ + if ( FirstChild() && FirstChild()->ToText() ) { + const char* t = FirstChild()->Value(); + if ( XMLUtil::ToDouble( t, dval ) ) { + return XML_SUCCESS; + } + return XML_CAN_NOT_CONVERT_TEXT; + } + return XML_NO_TEXT_NODE; +} + + +XMLError XMLElement::QueryFloatText( float* fval ) const +{ + if ( FirstChild() && FirstChild()->ToText() ) { + const char* t = FirstChild()->Value(); + if ( XMLUtil::ToFloat( t, fval ) ) { + return XML_SUCCESS; + } + return XML_CAN_NOT_CONVERT_TEXT; + } + return XML_NO_TEXT_NODE; +} + +int XMLElement::IntText(int defaultValue) const +{ + int i = defaultValue; + QueryIntText(&i); + return i; +} + +unsigned XMLElement::UnsignedText(unsigned defaultValue) const +{ + unsigned i = defaultValue; + QueryUnsignedText(&i); + return i; +} + +int64_t XMLElement::Int64Text(int64_t defaultValue) const +{ + int64_t i = defaultValue; + QueryInt64Text(&i); + return i; +} + +uint64_t XMLElement::Unsigned64Text(uint64_t defaultValue) const +{ + uint64_t i = defaultValue; + QueryUnsigned64Text(&i); + return i; +} + +bool XMLElement::BoolText(bool defaultValue) const +{ + bool b = defaultValue; + QueryBoolText(&b); + return b; +} + +double XMLElement::DoubleText(double defaultValue) const +{ + double d = defaultValue; + QueryDoubleText(&d); + return d; +} + +float XMLElement::FloatText(float defaultValue) const +{ + float f = defaultValue; + QueryFloatText(&f); + return f; +} + + +XMLAttribute* XMLElement::FindOrCreateAttribute( const char* name ) +{ + XMLAttribute* last = 0; + XMLAttribute* attrib = 0; + for( attrib = _rootAttribute; + attrib; + last = attrib, attrib = attrib->_next ) { + if ( XMLUtil::StringEqual( attrib->Name(), name ) ) { + break; + } + } + if ( !attrib ) { + attrib = CreateAttribute(); + TIXMLASSERT( attrib ); + if ( last ) { + TIXMLASSERT( last->_next == 0 ); + last->_next = attrib; + } + else { + TIXMLASSERT( _rootAttribute == 0 ); + _rootAttribute = attrib; + } + attrib->SetName( name ); + } + return attrib; +} + + +void XMLElement::DeleteAttribute( const char* name ) +{ + XMLAttribute* prev = 0; + for( XMLAttribute* a=_rootAttribute; a; a=a->_next ) { + if ( XMLUtil::StringEqual( name, a->Name() ) ) { + if ( prev ) { + prev->_next = a->_next; + } + else { + _rootAttribute = a->_next; + } + DeleteAttribute( a ); + break; + } + prev = a; + } +} + + +char* XMLElement::ParseAttributes( char* p, int* curLineNumPtr ) +{ + XMLAttribute* prevAttribute = 0; + + // Read the attributes. + while( p ) { + p = XMLUtil::SkipWhiteSpace( p, curLineNumPtr ); + if ( !(*p) ) { + _document->SetError( XML_ERROR_PARSING_ELEMENT, _parseLineNum, "XMLElement name=%s", Name() ); + return 0; + } + + // attribute. + if (XMLUtil::IsNameStartChar( (unsigned char) *p ) ) { + XMLAttribute* attrib = CreateAttribute(); + TIXMLASSERT( attrib ); + attrib->_parseLineNum = _document->_parseCurLineNum; + + const int attrLineNum = attrib->_parseLineNum; + + p = attrib->ParseDeep( p, _document->ProcessEntities(), curLineNumPtr ); + if ( !p || Attribute( attrib->Name() ) ) { + DeleteAttribute( attrib ); + _document->SetError( XML_ERROR_PARSING_ATTRIBUTE, attrLineNum, "XMLElement name=%s", Name() ); + return 0; + } + // There is a minor bug here: if the attribute in the source xml + // document is duplicated, it will not be detected and the + // attribute will be doubly added. However, tracking the 'prevAttribute' + // avoids re-scanning the attribute list. Preferring performance for + // now, may reconsider in the future. + if ( prevAttribute ) { + TIXMLASSERT( prevAttribute->_next == 0 ); + prevAttribute->_next = attrib; + } + else { + TIXMLASSERT( _rootAttribute == 0 ); + _rootAttribute = attrib; + } + prevAttribute = attrib; + } + // end of the tag + else if ( *p == '>' ) { + ++p; + break; + } + // end of the tag + else if ( *p == '/' && *(p+1) == '>' ) { + _closingType = CLOSED; + return p+2; // done; sealed element. + } + else { + _document->SetError( XML_ERROR_PARSING_ELEMENT, _parseLineNum, 0 ); + return 0; + } + } + return p; +} + +void XMLElement::DeleteAttribute( XMLAttribute* attribute ) +{ + if ( attribute == 0 ) { + return; + } + MemPool* pool = attribute->_memPool; + attribute->~XMLAttribute(); + pool->Free( attribute ); +} + +XMLAttribute* XMLElement::CreateAttribute() +{ + TIXMLASSERT( sizeof( XMLAttribute ) == _document->_attributePool.ItemSize() ); + XMLAttribute* attrib = new (_document->_attributePool.Alloc() ) XMLAttribute(); + TIXMLASSERT( attrib ); + attrib->_memPool = &_document->_attributePool; + attrib->_memPool->SetTracked(); + return attrib; +} + + +XMLElement* XMLElement::InsertNewChildElement(const char* name) +{ + XMLElement* node = _document->NewElement(name); + return InsertEndChild(node) ? node : 0; +} + +XMLComment* XMLElement::InsertNewComment(const char* comment) +{ + XMLComment* node = _document->NewComment(comment); + return InsertEndChild(node) ? node : 0; +} + +XMLText* XMLElement::InsertNewText(const char* text) +{ + XMLText* node = _document->NewText(text); + return InsertEndChild(node) ? node : 0; +} + +XMLDeclaration* XMLElement::InsertNewDeclaration(const char* text) +{ + XMLDeclaration* node = _document->NewDeclaration(text); + return InsertEndChild(node) ? node : 0; +} + +XMLUnknown* XMLElement::InsertNewUnknown(const char* text) +{ + XMLUnknown* node = _document->NewUnknown(text); + return InsertEndChild(node) ? node : 0; +} + + + +// +// +// foobar +// +char* XMLElement::ParseDeep( char* p, StrPair* parentEndTag, int* curLineNumPtr ) +{ + // Read the element name. + p = XMLUtil::SkipWhiteSpace( p, curLineNumPtr ); + + // The closing element is the form. It is + // parsed just like a regular element then deleted from + // the DOM. + if ( *p == '/' ) { + _closingType = CLOSING; + ++p; + } + + p = _value.ParseName( p ); + if ( _value.Empty() ) { + return 0; + } + + p = ParseAttributes( p, curLineNumPtr ); + if ( !p || !*p || _closingType != OPEN ) { + return p; + } + + p = XMLNode::ParseDeep( p, parentEndTag, curLineNumPtr ); + return p; +} + + + +XMLNode* XMLElement::ShallowClone( XMLDocument* doc ) const +{ + if ( !doc ) { + doc = _document; + } + XMLElement* element = doc->NewElement( Value() ); // fixme: this will always allocate memory. Intern? + for( const XMLAttribute* a=FirstAttribute(); a; a=a->Next() ) { + element->SetAttribute( a->Name(), a->Value() ); // fixme: this will always allocate memory. Intern? + } + return element; +} + + +bool XMLElement::ShallowEqual( const XMLNode* compare ) const +{ + TIXMLASSERT( compare ); + const XMLElement* other = compare->ToElement(); + if ( other && XMLUtil::StringEqual( other->Name(), Name() )) { + + const XMLAttribute* a=FirstAttribute(); + const XMLAttribute* b=other->FirstAttribute(); + + while ( a && b ) { + if ( !XMLUtil::StringEqual( a->Value(), b->Value() ) ) { + return false; + } + a = a->Next(); + b = b->Next(); + } + if ( a || b ) { + // different count + return false; + } + return true; + } + return false; +} + + +bool XMLElement::Accept( XMLVisitor* visitor ) const +{ + TIXMLASSERT( visitor ); + if ( visitor->VisitEnter( *this, _rootAttribute ) ) { + for ( const XMLNode* node=FirstChild(); node; node=node->NextSibling() ) { + if ( !node->Accept( visitor ) ) { + break; + } + } + } + return visitor->VisitExit( *this ); +} + + +// --------- XMLDocument ----------- // + +// Warning: List must match 'enum XMLError' +const char* XMLDocument::_errorNames[XML_ERROR_COUNT] = { + "XML_SUCCESS", + "XML_NO_ATTRIBUTE", + "XML_WRONG_ATTRIBUTE_TYPE", + "XML_ERROR_FILE_NOT_FOUND", + "XML_ERROR_FILE_COULD_NOT_BE_OPENED", + "XML_ERROR_FILE_READ_ERROR", + "XML_ERROR_PARSING_ELEMENT", + "XML_ERROR_PARSING_ATTRIBUTE", + "XML_ERROR_PARSING_TEXT", + "XML_ERROR_PARSING_CDATA", + "XML_ERROR_PARSING_COMMENT", + "XML_ERROR_PARSING_DECLARATION", + "XML_ERROR_PARSING_UNKNOWN", + "XML_ERROR_EMPTY_DOCUMENT", + "XML_ERROR_MISMATCHED_ELEMENT", + "XML_ERROR_PARSING", + "XML_CAN_NOT_CONVERT_TEXT", + "XML_NO_TEXT_NODE", + "XML_ELEMENT_DEPTH_EXCEEDED" +}; + + +XMLDocument::XMLDocument( bool processEntities, Whitespace whitespaceMode ) : + XMLNode( 0 ), + _writeBOM( false ), + _processEntities( processEntities ), + _errorID(XML_SUCCESS), + _whitespaceMode( whitespaceMode ), + _errorStr(), + _errorLineNum( 0 ), + _charBuffer( 0 ), + _parseCurLineNum( 0 ), + _parsingDepth(0), + _unlinked(), + _elementPool(), + _attributePool(), + _textPool(), + _commentPool() +{ + // avoid VC++ C4355 warning about 'this' in initializer list (C4355 is off by default in VS2012+) + _document = this; +} + + +XMLDocument::~XMLDocument() +{ + Clear(); +} + + +void XMLDocument::MarkInUse(const XMLNode* const node) +{ + TIXMLASSERT(node); + TIXMLASSERT(node->_parent == 0); + + for (int i = 0; i < _unlinked.Size(); ++i) { + if (node == _unlinked[i]) { + _unlinked.SwapRemove(i); + break; + } + } +} + +void XMLDocument::Clear() +{ + DeleteChildren(); + while( _unlinked.Size()) { + DeleteNode(_unlinked[0]); // Will remove from _unlinked as part of delete. + } + +#ifdef TINYXML2_DEBUG + const bool hadError = Error(); +#endif + ClearError(); + + delete [] _charBuffer; + _charBuffer = 0; + _parsingDepth = 0; + +#if 0 + _textPool.Trace( "text" ); + _elementPool.Trace( "element" ); + _commentPool.Trace( "comment" ); + _attributePool.Trace( "attribute" ); +#endif + +#ifdef TINYXML2_DEBUG + if ( !hadError ) { + TIXMLASSERT( _elementPool.CurrentAllocs() == _elementPool.Untracked() ); + TIXMLASSERT( _attributePool.CurrentAllocs() == _attributePool.Untracked() ); + TIXMLASSERT( _textPool.CurrentAllocs() == _textPool.Untracked() ); + TIXMLASSERT( _commentPool.CurrentAllocs() == _commentPool.Untracked() ); + } +#endif +} + + +void XMLDocument::DeepCopy(XMLDocument* target) const +{ + TIXMLASSERT(target); + if (target == this) { + return; // technically success - a no-op. + } + + target->Clear(); + for (const XMLNode* node = this->FirstChild(); node; node = node->NextSibling()) { + target->InsertEndChild(node->DeepClone(target)); + } +} + +XMLElement* XMLDocument::NewElement( const char* name ) +{ + XMLElement* ele = CreateUnlinkedNode( _elementPool ); + ele->SetName( name ); + return ele; +} + + +XMLComment* XMLDocument::NewComment( const char* str ) +{ + XMLComment* comment = CreateUnlinkedNode( _commentPool ); + comment->SetValue( str ); + return comment; +} + + +XMLText* XMLDocument::NewText( const char* str ) +{ + XMLText* text = CreateUnlinkedNode( _textPool ); + text->SetValue( str ); + return text; +} + + +XMLDeclaration* XMLDocument::NewDeclaration( const char* str ) +{ + XMLDeclaration* dec = CreateUnlinkedNode( _commentPool ); + dec->SetValue( str ? str : "xml version=\"1.0\" encoding=\"UTF-8\"" ); + return dec; +} + + +XMLUnknown* XMLDocument::NewUnknown( const char* str ) +{ + XMLUnknown* unk = CreateUnlinkedNode( _commentPool ); + unk->SetValue( str ); + return unk; +} + +static FILE* callfopen( const char* filepath, const char* mode ) +{ + TIXMLASSERT( filepath ); + TIXMLASSERT( mode ); +#if defined(_MSC_VER) && (_MSC_VER >= 1400 ) && (!defined WINCE) + FILE* fp = 0; + const errno_t err = fopen_s( &fp, filepath, mode ); + if ( err ) { + return 0; + } +#else + FILE* fp = fopen( filepath, mode ); +#endif + return fp; +} + +void XMLDocument::DeleteNode( XMLNode* node ) { + TIXMLASSERT( node ); + TIXMLASSERT(node->_document == this ); + if (node->_parent) { + node->_parent->DeleteChild( node ); + } + else { + // Isn't in the tree. + // Use the parent delete. + // Also, we need to mark it tracked: we 'know' + // it was never used. + node->_memPool->SetTracked(); + // Call the static XMLNode version: + XMLNode::DeleteNode(node); + } +} + + +XMLError XMLDocument::LoadFile( const char* filename ) +{ + if ( !filename ) { + TIXMLASSERT( false ); + SetError( XML_ERROR_FILE_COULD_NOT_BE_OPENED, 0, "filename=" ); + return _errorID; + } + + Clear(); + FILE* fp = callfopen( filename, "rb" ); + if ( !fp ) { + SetError( XML_ERROR_FILE_NOT_FOUND, 0, "filename=%s", filename ); + return _errorID; + } + LoadFile( fp ); + fclose( fp ); + return _errorID; +} + +XMLError XMLDocument::LoadFile( FILE* fp ) +{ + Clear(); + + TIXML_FSEEK( fp, 0, SEEK_SET ); + if ( fgetc( fp ) == EOF && ferror( fp ) != 0 ) { + SetError( XML_ERROR_FILE_READ_ERROR, 0, 0 ); + return _errorID; + } + + TIXML_FSEEK( fp, 0, SEEK_END ); + + unsigned long long filelength; + { + const long long fileLengthSigned = TIXML_FTELL( fp ); + TIXML_FSEEK( fp, 0, SEEK_SET ); + if ( fileLengthSigned == -1L ) { + SetError( XML_ERROR_FILE_READ_ERROR, 0, 0 ); + return _errorID; + } + TIXMLASSERT( fileLengthSigned >= 0 ); + filelength = static_cast(fileLengthSigned); + } + + const size_t maxSizeT = static_cast(-1); + // We'll do the comparison as an unsigned long long, because that's guaranteed to be at + // least 8 bytes, even on a 32-bit platform. + if ( filelength >= static_cast(maxSizeT) ) { + // Cannot handle files which won't fit in buffer together with null terminator + SetError( XML_ERROR_FILE_READ_ERROR, 0, 0 ); + return _errorID; + } + + if ( filelength == 0 ) { + SetError( XML_ERROR_EMPTY_DOCUMENT, 0, 0 ); + return _errorID; + } + + const size_t size = static_cast(filelength); + TIXMLASSERT( _charBuffer == 0 ); + _charBuffer = new char[size+1]; + const size_t read = fread( _charBuffer, 1, size, fp ); + if ( read != size ) { + SetError( XML_ERROR_FILE_READ_ERROR, 0, 0 ); + return _errorID; + } + + _charBuffer[size] = 0; + + Parse(); + return _errorID; +} + + +XMLError XMLDocument::SaveFile( const char* filename, bool compact ) +{ + if ( !filename ) { + TIXMLASSERT( false ); + SetError( XML_ERROR_FILE_COULD_NOT_BE_OPENED, 0, "filename=" ); + return _errorID; + } + + FILE* fp = callfopen( filename, "w" ); + if ( !fp ) { + SetError( XML_ERROR_FILE_COULD_NOT_BE_OPENED, 0, "filename=%s", filename ); + return _errorID; + } + SaveFile(fp, compact); + fclose( fp ); + return _errorID; +} + + +XMLError XMLDocument::SaveFile( FILE* fp, bool compact ) +{ + // Clear any error from the last save, otherwise it will get reported + // for *this* call. + ClearError(); + XMLPrinter stream( fp, compact ); + Print( &stream ); + return _errorID; +} + + +XMLError XMLDocument::Parse( const char* p, size_t len ) +{ + Clear(); + + if ( len == 0 || !p || !*p ) { + SetError( XML_ERROR_EMPTY_DOCUMENT, 0, 0 ); + return _errorID; + } + if ( len == static_cast(-1) ) { + len = strlen( p ); + } + TIXMLASSERT( _charBuffer == 0 ); + _charBuffer = new char[ len+1 ]; + memcpy( _charBuffer, p, len ); + _charBuffer[len] = 0; + + Parse(); + if ( Error() ) { + // clean up now essentially dangling memory. + // and the parse fail can put objects in the + // pools that are dead and inaccessible. + DeleteChildren(); + _elementPool.Clear(); + _attributePool.Clear(); + _textPool.Clear(); + _commentPool.Clear(); + } + return _errorID; +} + + +void XMLDocument::Print( XMLPrinter* streamer ) const +{ + if ( streamer ) { + Accept( streamer ); + } + else { + XMLPrinter stdoutStreamer( stdout ); + Accept( &stdoutStreamer ); + } +} + + +void XMLDocument::ClearError() { + _errorID = XML_SUCCESS; + _errorLineNum = 0; + _errorStr.Reset(); +} + + +void XMLDocument::SetError( XMLError error, int lineNum, const char* format, ... ) +{ + TIXMLASSERT( error >= 0 && error < XML_ERROR_COUNT ); + _errorID = error; + _errorLineNum = lineNum; + _errorStr.Reset(); + + const size_t BUFFER_SIZE = 1000; + char* buffer = new char[BUFFER_SIZE]; + + TIXMLASSERT(sizeof(error) <= sizeof(int)); + TIXML_SNPRINTF(buffer, BUFFER_SIZE, "Error=%s ErrorID=%d (0x%x) Line number=%d", ErrorIDToName(error), int(error), int(error), lineNum); + + if (format) { + size_t len = strlen(buffer); + TIXML_SNPRINTF(buffer + len, BUFFER_SIZE - len, ": "); + len = strlen(buffer); + + va_list va; + va_start(va, format); + TIXML_VSNPRINTF(buffer + len, BUFFER_SIZE - len, format, va); + va_end(va); + } + _errorStr.SetStr(buffer); + delete[] buffer; +} + + +/*static*/ const char* XMLDocument::ErrorIDToName(XMLError errorID) +{ + TIXMLASSERT( errorID >= 0 && errorID < XML_ERROR_COUNT ); + const char* errorName = _errorNames[errorID]; + TIXMLASSERT( errorName && errorName[0] ); + return errorName; +} + +const char* XMLDocument::ErrorStr() const +{ + return _errorStr.Empty() ? "" : _errorStr.GetStr(); +} + + +void XMLDocument::PrintError() const +{ + printf("%s\n", ErrorStr()); +} + +const char* XMLDocument::ErrorName() const +{ + return ErrorIDToName(_errorID); +} + +void XMLDocument::Parse() +{ + TIXMLASSERT( NoChildren() ); // Clear() must have been called previously + TIXMLASSERT( _charBuffer ); + _parseCurLineNum = 1; + _parseLineNum = 1; + char* p = _charBuffer; + p = XMLUtil::SkipWhiteSpace( p, &_parseCurLineNum ); + p = const_cast( XMLUtil::ReadBOM( p, &_writeBOM ) ); + if ( !*p ) { + SetError( XML_ERROR_EMPTY_DOCUMENT, 0, 0 ); + return; + } + ParseDeep(p, 0, &_parseCurLineNum ); +} + +void XMLDocument::PushDepth() +{ + _parsingDepth++; + if (_parsingDepth == TINYXML2_MAX_ELEMENT_DEPTH) { + SetError(XML_ELEMENT_DEPTH_EXCEEDED, _parseCurLineNum, "Element nesting is too deep." ); + } +} + +void XMLDocument::PopDepth() +{ + TIXMLASSERT(_parsingDepth > 0); + --_parsingDepth; +} + +XMLPrinter::XMLPrinter( FILE* file, bool compact, int depth ) : + _elementJustOpened( false ), + _stack(), + _firstElement( true ), + _fp( file ), + _depth( depth ), + _textDepth( -1 ), + _processEntities( true ), + _compactMode( compact ), + _buffer() +{ + for( int i=0; i(entityValue); + TIXMLASSERT( flagIndex < ENTITY_RANGE ); + _entityFlag[flagIndex] = true; + } + _restrictedEntityFlag[static_cast('&')] = true; + _restrictedEntityFlag[static_cast('<')] = true; + _restrictedEntityFlag[static_cast('>')] = true; // not required, but consistency is nice + _buffer.Push( 0 ); +} + + +void XMLPrinter::Print( const char* format, ... ) +{ + va_list va; + va_start( va, format ); + + if ( _fp ) { + vfprintf( _fp, format, va ); + } + else { + const int len = TIXML_VSCPRINTF( format, va ); + // Close out and re-start the va-args + va_end( va ); + TIXMLASSERT( len >= 0 ); + va_start( va, format ); + TIXMLASSERT( _buffer.Size() > 0 && _buffer[_buffer.Size() - 1] == 0 ); + char* p = _buffer.PushArr( len ) - 1; // back up over the null terminator. + TIXML_VSNPRINTF( p, len+1, format, va ); + } + va_end( va ); +} + + +void XMLPrinter::Write( const char* data, size_t size ) +{ + if ( _fp ) { + fwrite ( data , sizeof(char), size, _fp); + } + else { + char* p = _buffer.PushArr( static_cast(size) ) - 1; // back up over the null terminator. + memcpy( p, data, size ); + p[size] = 0; + } +} + + +void XMLPrinter::Putc( char ch ) +{ + if ( _fp ) { + fputc ( ch, _fp); + } + else { + char* p = _buffer.PushArr( sizeof(char) ) - 1; // back up over the null terminator. + p[0] = ch; + p[1] = 0; + } +} + + +void XMLPrinter::PrintSpace( int depth ) +{ + for( int i=0; i 0 && *q < ENTITY_RANGE ) { + // Check for entities. If one is found, flush + // the stream up until the entity, write the + // entity, and keep looking. + if ( flag[static_cast(*q)] ) { + while ( p < q ) { + const size_t delta = q - p; + const int toPrint = ( INT_MAX < delta ) ? INT_MAX : static_cast(delta); + Write( p, toPrint ); + p += toPrint; + } + bool entityPatternPrinted = false; + for( int i=0; i(delta); + Write( p, toPrint ); + } + } + else { + Write( p ); + } +} + + +void XMLPrinter::PushHeader( bool writeBOM, bool writeDec ) +{ + if ( writeBOM ) { + static const unsigned char bom[] = { TIXML_UTF_LEAD_0, TIXML_UTF_LEAD_1, TIXML_UTF_LEAD_2, 0 }; + Write( reinterpret_cast< const char* >( bom ) ); + } + if ( writeDec ) { + PushDeclaration( "xml version=\"1.0\"" ); + } +} + +void XMLPrinter::PrepareForNewNode( bool compactMode ) +{ + SealElementIfJustOpened(); + + if ( compactMode ) { + return; + } + + if ( _firstElement ) { + PrintSpace (_depth); + } else if ( _textDepth < 0) { + Putc( '\n' ); + PrintSpace( _depth ); + } + + _firstElement = false; +} + +void XMLPrinter::OpenElement( const char* name, bool compactMode ) +{ + PrepareForNewNode( compactMode ); + _stack.Push( name ); + + Write ( "<" ); + Write ( name ); + + _elementJustOpened = true; + ++_depth; +} + + +void XMLPrinter::PushAttribute( const char* name, const char* value ) +{ + TIXMLASSERT( _elementJustOpened ); + Putc ( ' ' ); + Write( name ); + Write( "=\"" ); + PrintString( value, false ); + Putc ( '\"' ); +} + + +void XMLPrinter::PushAttribute( const char* name, int v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + PushAttribute( name, buf ); +} + + +void XMLPrinter::PushAttribute( const char* name, unsigned v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + PushAttribute( name, buf ); +} + + +void XMLPrinter::PushAttribute(const char* name, int64_t v) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr(v, buf, BUF_SIZE); + PushAttribute(name, buf); +} + + +void XMLPrinter::PushAttribute(const char* name, uint64_t v) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr(v, buf, BUF_SIZE); + PushAttribute(name, buf); +} + + +void XMLPrinter::PushAttribute( const char* name, bool v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + PushAttribute( name, buf ); +} + + +void XMLPrinter::PushAttribute( const char* name, double v ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( v, buf, BUF_SIZE ); + PushAttribute( name, buf ); +} + + +void XMLPrinter::CloseElement( bool compactMode ) +{ + --_depth; + const char* name = _stack.Pop(); + + if ( _elementJustOpened ) { + Write( "/>" ); + } + else { + if ( _textDepth < 0 && !compactMode) { + Putc( '\n' ); + PrintSpace( _depth ); + } + Write ( "" ); + } + + if ( _textDepth == _depth ) { + _textDepth = -1; + } + if ( _depth == 0 && !compactMode) { + Putc( '\n' ); + } + _elementJustOpened = false; +} + + +void XMLPrinter::SealElementIfJustOpened() +{ + if ( !_elementJustOpened ) { + return; + } + _elementJustOpened = false; + Putc( '>' ); +} + + +void XMLPrinter::PushText( const char* text, bool cdata ) +{ + _textDepth = _depth-1; + + SealElementIfJustOpened(); + if ( cdata ) { + Write( "" ); + } + else { + PrintString( text, true ); + } +} + + +void XMLPrinter::PushText( int64_t value ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( value, buf, BUF_SIZE ); + PushText( buf, false ); +} + + +void XMLPrinter::PushText( uint64_t value ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr(value, buf, BUF_SIZE); + PushText(buf, false); +} + + +void XMLPrinter::PushText( int value ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( value, buf, BUF_SIZE ); + PushText( buf, false ); +} + + +void XMLPrinter::PushText( unsigned value ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( value, buf, BUF_SIZE ); + PushText( buf, false ); +} + + +void XMLPrinter::PushText( bool value ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( value, buf, BUF_SIZE ); + PushText( buf, false ); +} + + +void XMLPrinter::PushText( float value ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( value, buf, BUF_SIZE ); + PushText( buf, false ); +} + + +void XMLPrinter::PushText( double value ) +{ + char buf[BUF_SIZE]; + XMLUtil::ToStr( value, buf, BUF_SIZE ); + PushText( buf, false ); +} + + +void XMLPrinter::PushComment( const char* comment ) +{ + PrepareForNewNode( _compactMode ); + + Write( "" ); +} + + +void XMLPrinter::PushDeclaration( const char* value ) +{ + PrepareForNewNode( _compactMode ); + + Write( "" ); +} + + +void XMLPrinter::PushUnknown( const char* value ) +{ + PrepareForNewNode( _compactMode ); + + Write( "' ); +} + + +bool XMLPrinter::VisitEnter( const XMLDocument& doc ) +{ + _processEntities = doc.ProcessEntities(); + if ( doc.HasBOM() ) { + PushHeader( true, false ); + } + return true; +} + + +bool XMLPrinter::VisitEnter( const XMLElement& element, const XMLAttribute* attribute ) +{ + const XMLElement* parentElem = 0; + if ( element.Parent() ) { + parentElem = element.Parent()->ToElement(); + } + const bool compactMode = parentElem ? CompactMode( *parentElem ) : _compactMode; + OpenElement( element.Name(), compactMode ); + while ( attribute ) { + PushAttribute( attribute->Name(), attribute->Value() ); + attribute = attribute->Next(); + } + return true; +} + + +bool XMLPrinter::VisitExit( const XMLElement& element ) +{ + CloseElement( CompactMode(element) ); + return true; +} + + +bool XMLPrinter::Visit( const XMLText& text ) +{ + PushText( text.Value(), text.CData() ); + return true; +} + + +bool XMLPrinter::Visit( const XMLComment& comment ) +{ + PushComment( comment.Value() ); + return true; +} + +bool XMLPrinter::Visit( const XMLDeclaration& declaration ) +{ + PushDeclaration( declaration.Value() ); + return true; +} + + +bool XMLPrinter::Visit( const XMLUnknown& unknown ) +{ + PushUnknown( unknown.Value() ); + return true; +} + +} // namespace tinyxml2 diff --git a/libs/IO/TinyXML2.h b/libs/IO/TinyXML2.h new file mode 100644 index 0000000..ae237ad --- /dev/null +++ b/libs/IO/TinyXML2.h @@ -0,0 +1,2368 @@ +/* +Original code by Lee Thomason (www.grinninglizard.com) + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any +damages arising from the use of this software. + +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must +not claim that you wrote the original software. If you use this +software in a product, an acknowledgment in the product documentation +would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and +must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source +distribution. +*/ + +#ifndef TINYXML2_INCLUDED +#define TINYXML2_INCLUDED + +#if defined(ANDROID_NDK) || defined(__BORLANDC__) || defined(__QNXNTO__) +# include +# include +# include +# include +# include +# if defined(__PS3__) +# include +# endif +#else +# include +# include +# include +# include +# include +#endif +#include + +/* + TODO: intern strings instead of allocation. +*/ +/* + gcc: + g++ -Wall -DTINYXML2_DEBUG tinyxml2.cpp xmltest.cpp -o gccxmltest.exe + + Formatting, Artistic Style: + AStyle.exe --style=1tbs --indent-switches --break-closing-brackets --indent-preprocessor tinyxml2.cpp tinyxml2.h +*/ + +#if defined( _DEBUG ) || defined (__DEBUG__) +# ifndef TINYXML2_DEBUG +# define TINYXML2_DEBUG +# endif +#endif + +#ifdef _MSC_VER +# pragma warning(push) +# pragma warning(disable: 4251) +#endif + +#ifndef TINYXML2_LIB +#define TINYXML2_LIB IO_API +#endif + +#if !defined(TIXMLASSERT) +#if defined(TINYXML2_DEBUG) +# if defined(_MSC_VER) +# define TIXMLASSERT ASSERT +# elif defined (ANDROID_NDK) +# include +# define TIXMLASSERT( x ) if ( !(x)) { __android_log_assert( "assert", "grinliz", "ASSERT in '%s' at %d.", __FILE__, __LINE__ ); } +# else +# include +# define TIXMLASSERT ASSERT +# endif +#else +# define TIXMLASSERT( x ) {} +#endif +#endif + +/* Versioning, past 1.0.14: + http://semver.org/ +*/ +static const int TIXML2_MAJOR_VERSION = 9; +static const int TIXML2_MINOR_VERSION = 0; +static const int TIXML2_PATCH_VERSION = 0; + +#define TINYXML2_MAJOR_VERSION 9 +#define TINYXML2_MINOR_VERSION 0 +#define TINYXML2_PATCH_VERSION 0 + +// A fixed element depth limit is problematic. There needs to be a +// limit to avoid a stack overflow. However, that limit varies per +// system, and the capacity of the stack. On the other hand, it's a trivial +// attack that can result from ill, malicious, or even correctly formed XML, +// so there needs to be a limit in place. +static const int TINYXML2_MAX_ELEMENT_DEPTH = 100; + +namespace tinyxml2 +{ +class XMLDocument; +class XMLElement; +class XMLAttribute; +class XMLComment; +class XMLText; +class XMLDeclaration; +class XMLUnknown; +class XMLPrinter; + +/* + A class that wraps strings. Normally stores the start and end + pointers into the XML file itself, and will apply normalization + and entity translation if actually read. Can also store (and memory + manage) a traditional char[] + + Isn't clear why TINYXML2_LIB is needed; but seems to fix #719 +*/ +class TINYXML2_LIB StrPair +{ +public: + enum Mode { + NEEDS_ENTITY_PROCESSING = 0x01, + NEEDS_NEWLINE_NORMALIZATION = 0x02, + NEEDS_WHITESPACE_COLLAPSING = 0x04, + + TEXT_ELEMENT = NEEDS_ENTITY_PROCESSING | NEEDS_NEWLINE_NORMALIZATION, + TEXT_ELEMENT_LEAVE_ENTITIES = NEEDS_NEWLINE_NORMALIZATION, + ATTRIBUTE_NAME = 0, + ATTRIBUTE_VALUE = NEEDS_ENTITY_PROCESSING | NEEDS_NEWLINE_NORMALIZATION, + ATTRIBUTE_VALUE_LEAVE_ENTITIES = NEEDS_NEWLINE_NORMALIZATION, + COMMENT = NEEDS_NEWLINE_NORMALIZATION + }; + + StrPair() : _flags( 0 ), _start( 0 ), _end( 0 ) {} + ~StrPair(); + + void Set( char* start, char* end, int flags ) { + TIXMLASSERT( start ); + TIXMLASSERT( end ); + Reset(); + _start = start; + _end = end; + _flags = flags | NEEDS_FLUSH; + } + + const char* GetStr(); + + bool Empty() const { + return _start == _end; + } + + void SetInternedStr( const char* str ) { + Reset(); + _start = const_cast(str); + } + + void SetStr( const char* str, int flags=0 ); + + char* ParseText( char* in, const char* endTag, int strFlags, int* curLineNumPtr ); + char* ParseName( char* in ); + + void TransferTo( StrPair* other ); + void Reset(); + +private: + void CollapseWhitespace(); + + enum { + NEEDS_FLUSH = 0x100, + NEEDS_DELETE = 0x200 + }; + + int _flags; + char* _start; + char* _end; + + StrPair( const StrPair& other ); // not supported + void operator=( const StrPair& other ); // not supported, use TransferTo() +}; + + +/* + A dynamic array of Plain Old Data. Doesn't support constructors, etc. + Has a small initial memory pool, so that low or no usage will not + cause a call to new/delete +*/ +template +class DynArray +{ +public: + DynArray() : + _mem( _pool ), + _allocated( INITIAL_SIZE ), + _size( 0 ) + { + } + + ~DynArray() { + if ( _mem != _pool ) { + delete [] _mem; + } + } + + void Clear() { + _size = 0; + } + + void Push( T t ) { + TIXMLASSERT( _size < INT_MAX ); + EnsureCapacity( _size+1 ); + _mem[_size] = t; + ++_size; + } + + T* PushArr( int count ) { + TIXMLASSERT( count >= 0 ); + TIXMLASSERT( _size <= INT_MAX - count ); + EnsureCapacity( _size+count ); + T* ret = &_mem[_size]; + _size += count; + return ret; + } + + T Pop() { + TIXMLASSERT( _size > 0 ); + --_size; + return _mem[_size]; + } + + void PopArr( int count ) { + TIXMLASSERT( _size >= count ); + _size -= count; + } + + bool Empty() const { + return _size == 0; + } + + T& operator[](int i) { + TIXMLASSERT( i>= 0 && i < _size ); + return _mem[i]; + } + + const T& operator[](int i) const { + TIXMLASSERT( i>= 0 && i < _size ); + return _mem[i]; + } + + const T& PeekTop() const { + TIXMLASSERT( _size > 0 ); + return _mem[ _size - 1]; + } + + int Size() const { + TIXMLASSERT( _size >= 0 ); + return _size; + } + + int Capacity() const { + TIXMLASSERT( _allocated >= INITIAL_SIZE ); + return _allocated; + } + + void SwapRemove(int i) { + TIXMLASSERT(i >= 0 && i < _size); + TIXMLASSERT(_size > 0); + _mem[i] = _mem[_size - 1]; + --_size; + } + + const T* Mem() const { + TIXMLASSERT( _mem ); + return _mem; + } + + T* Mem() { + TIXMLASSERT( _mem ); + return _mem; + } + +private: + DynArray( const DynArray& ); // not supported + void operator=( const DynArray& ); // not supported + + void EnsureCapacity( int cap ) { + TIXMLASSERT( cap > 0 ); + if ( cap > _allocated ) { + TIXMLASSERT( cap <= INT_MAX / 2 ); + const int newAllocated = cap * 2; + T* newMem = new T[newAllocated]; + TIXMLASSERT( newAllocated >= _size ); + memcpy( newMem, _mem, sizeof(T)*_size ); // warning: not using constructors, only works for PODs + if ( _mem != _pool ) { + delete [] _mem; + } + _mem = newMem; + _allocated = newAllocated; + } + } + + T* _mem; + T _pool[INITIAL_SIZE]; + int _allocated; // objects allocated + int _size; // number objects in use +}; + + +/* + Parent virtual class of a pool for fast allocation + and deallocation of objects. +*/ +class MemPool +{ +public: + MemPool() {} + virtual ~MemPool() {} + + virtual int ItemSize() const = 0; + virtual void* Alloc() = 0; + virtual void Free( void* ) = 0; + virtual void SetTracked() = 0; +}; + + +/* + Template child class to create pools of the correct type. +*/ +template< int ITEM_SIZE > +class MemPoolT : public MemPool +{ +public: + MemPoolT() : _blockPtrs(), _root(0), _currentAllocs(0), _nAllocs(0), _maxAllocs(0), _nUntracked(0) {} + ~MemPoolT() { + MemPoolT< ITEM_SIZE >::Clear(); + } + + void Clear() { + // Delete the blocks. + while( !_blockPtrs.Empty()) { + Block* lastBlock = _blockPtrs.Pop(); + delete lastBlock; + } + _root = 0; + _currentAllocs = 0; + _nAllocs = 0; + _maxAllocs = 0; + _nUntracked = 0; + } + + virtual int ItemSize() const { + return ITEM_SIZE; + } + int CurrentAllocs() const { + return _currentAllocs; + } + + virtual void* Alloc() { + if ( !_root ) { + // Need a new block. + Block* block = new Block(); + _blockPtrs.Push( block ); + + Item* blockItems = block->items; + for( int i = 0; i < ITEMS_PER_BLOCK - 1; ++i ) { + blockItems[i].next = &(blockItems[i + 1]); + } + blockItems[ITEMS_PER_BLOCK - 1].next = 0; + _root = blockItems; + } + Item* const result = _root; + TIXMLASSERT( result != 0 ); + _root = _root->next; + + ++_currentAllocs; + if ( _currentAllocs > _maxAllocs ) { + _maxAllocs = _currentAllocs; + } + ++_nAllocs; + ++_nUntracked; + return result; + } + + virtual void Free( void* mem ) { + if ( !mem ) { + return; + } + --_currentAllocs; + Item* item = static_cast( mem ); +#ifdef TINYXML2_DEBUG + memset( item, 0xfe, sizeof( *item ) ); +#endif + item->next = _root; + _root = item; + } + void Trace( const char* name ) { + printf( "Mempool %s watermark=%d [%dk] current=%d size=%d nAlloc=%d blocks=%d\n", + name, _maxAllocs, _maxAllocs * ITEM_SIZE / 1024, _currentAllocs, + ITEM_SIZE, _nAllocs, _blockPtrs.Size() ); + } + + void SetTracked() { + --_nUntracked; + } + + int Untracked() const { + return _nUntracked; + } + + // This number is perf sensitive. 4k seems like a good tradeoff on my machine. + // The test file is large, 170k. + // Release: VS2010 gcc(no opt) + // 1k: 4000 + // 2k: 4000 + // 4k: 3900 21000 + // 16k: 5200 + // 32k: 4300 + // 64k: 4000 21000 + // Declared public because some compilers do not accept to use ITEMS_PER_BLOCK + // in private part if ITEMS_PER_BLOCK is private + enum { ITEMS_PER_BLOCK = (4 * 1024) / ITEM_SIZE }; + +private: + MemPoolT( const MemPoolT& ); // not supported + void operator=( const MemPoolT& ); // not supported + + union Item { + Item* next; + char itemData[ITEM_SIZE]; + }; + struct Block { + Item items[ITEMS_PER_BLOCK]; + }; + DynArray< Block*, 10 > _blockPtrs; + Item* _root; + + int _currentAllocs; + int _nAllocs; + int _maxAllocs; + int _nUntracked; +}; + + + +/** + Implements the interface to the "Visitor pattern" (see the Accept() method.) + If you call the Accept() method, it requires being passed a XMLVisitor + class to handle callbacks. For nodes that contain other nodes (Document, Element) + you will get called with a VisitEnter/VisitExit pair. Nodes that are always leafs + are simply called with Visit(). + + If you return 'true' from a Visit method, recursive parsing will continue. If you return + false, no children of this node or its siblings will be visited. + + All flavors of Visit methods have a default implementation that returns 'true' (continue + visiting). You need to only override methods that are interesting to you. + + Generally Accept() is called on the XMLDocument, although all nodes support visiting. + + You should never change the document from a callback. + + @sa XMLNode::Accept() +*/ +class TINYXML2_LIB XMLVisitor +{ +public: + virtual ~XMLVisitor() {} + + /// Visit a document. + virtual bool VisitEnter( const XMLDocument& /*doc*/ ) { + return true; + } + /// Visit a document. + virtual bool VisitExit( const XMLDocument& /*doc*/ ) { + return true; + } + + /// Visit an element. + virtual bool VisitEnter( const XMLElement& /*element*/, const XMLAttribute* /*firstAttribute*/ ) { + return true; + } + /// Visit an element. + virtual bool VisitExit( const XMLElement& /*element*/ ) { + return true; + } + + /// Visit a declaration. + virtual bool Visit( const XMLDeclaration& /*declaration*/ ) { + return true; + } + /// Visit a text node. + virtual bool Visit( const XMLText& /*text*/ ) { + return true; + } + /// Visit a comment node. + virtual bool Visit( const XMLComment& /*comment*/ ) { + return true; + } + /// Visit an unknown node. + virtual bool Visit( const XMLUnknown& /*unknown*/ ) { + return true; + } +}; + +// WARNING: must match XMLDocument::_errorNames[] +enum XMLError { + XML_SUCCESS = 0, + XML_NO_ATTRIBUTE, + XML_WRONG_ATTRIBUTE_TYPE, + XML_ERROR_FILE_NOT_FOUND, + XML_ERROR_FILE_COULD_NOT_BE_OPENED, + XML_ERROR_FILE_READ_ERROR, + XML_ERROR_PARSING_ELEMENT, + XML_ERROR_PARSING_ATTRIBUTE, + XML_ERROR_PARSING_TEXT, + XML_ERROR_PARSING_CDATA, + XML_ERROR_PARSING_COMMENT, + XML_ERROR_PARSING_DECLARATION, + XML_ERROR_PARSING_UNKNOWN, + XML_ERROR_EMPTY_DOCUMENT, + XML_ERROR_MISMATCHED_ELEMENT, + XML_ERROR_PARSING, + XML_CAN_NOT_CONVERT_TEXT, + XML_NO_TEXT_NODE, + XML_ELEMENT_DEPTH_EXCEEDED, + + XML_ERROR_COUNT +}; + + +/* + Utility functionality. +*/ +class TINYXML2_LIB XMLUtil +{ +public: + static const char* SkipWhiteSpace( const char* p, int* curLineNumPtr ) { + TIXMLASSERT( p ); + + while( IsWhiteSpace(*p) ) { + if (curLineNumPtr && *p == '\n') { + ++(*curLineNumPtr); + } + ++p; + } + TIXMLASSERT( p ); + return p; + } + static char* SkipWhiteSpace( char* const p, int* curLineNumPtr ) { + return const_cast( SkipWhiteSpace( const_cast(p), curLineNumPtr ) ); + } + + // Anything in the high order range of UTF-8 is assumed to not be whitespace. This isn't + // correct, but simple, and usually works. + static bool IsWhiteSpace( char p ) { + return !IsUTF8Continuation(p) && isspace( static_cast(p) ); + } + + inline static bool IsNameStartChar( unsigned char ch ) { + if ( ch >= 128 ) { + // This is a heuristic guess in attempt to not implement Unicode-aware isalpha() + return true; + } + if ( isalpha( ch ) ) { + return true; + } + return ch == ':' || ch == '_'; + } + + inline static bool IsNameChar( unsigned char ch ) { + return IsNameStartChar( ch ) + || isdigit( ch ) + || ch == '.' + || ch == '-'; + } + + inline static bool IsPrefixHex( const char* p) { + p = SkipWhiteSpace(p, 0); + return p && *p == '0' && ( *(p + 1) == 'x' || *(p + 1) == 'X'); + } + + inline static bool StringEqual( const char* p, const char* q, int nChar=INT_MAX ) { + if ( p == q ) { + return true; + } + TIXMLASSERT( p ); + TIXMLASSERT( q ); + TIXMLASSERT( nChar >= 0 ); + return strncmp( p, q, nChar ) == 0; + } + + inline static bool IsUTF8Continuation( const char p ) { + return ( p & 0x80 ) != 0; + } + + static const char* ReadBOM( const char* p, bool* hasBOM ); + // p is the starting location, + // the UTF-8 value of the entity will be placed in value, and length filled in. + static const char* GetCharacterRef( const char* p, char* value, int* length ); + static void ConvertUTF32ToUTF8( unsigned long input, char* output, int* length ); + + // converts primitive types to strings + static void ToStr( int v, char* buffer, int bufferSize ); + static void ToStr( unsigned v, char* buffer, int bufferSize ); + static void ToStr( bool v, char* buffer, int bufferSize ); + static void ToStr( float v, char* buffer, int bufferSize ); + static void ToStr( double v, char* buffer, int bufferSize ); + static void ToStr(int64_t v, char* buffer, int bufferSize); + static void ToStr(uint64_t v, char* buffer, int bufferSize); + + // converts strings to primitive types + static bool ToInt( const char* str, int* value ); + static bool ToUnsigned( const char* str, unsigned* value ); + static bool ToBool( const char* str, bool* value ); + static bool ToFloat( const char* str, float* value ); + static bool ToDouble( const char* str, double* value ); + static bool ToInt64(const char* str, int64_t* value); + static bool ToUnsigned64(const char* str, uint64_t* value); + // Changes what is serialized for a boolean value. + // Default to "true" and "false". Shouldn't be changed + // unless you have a special testing or compatibility need. + // Be careful: static, global, & not thread safe. + // Be sure to set static const memory as parameters. + static void SetBoolSerialization(const char* writeTrue, const char* writeFalse); + +private: + static const char* writeBoolTrue; + static const char* writeBoolFalse; +}; + + +/** XMLNode is a base class for every object that is in the + XML Document Object Model (DOM), except XMLAttributes. + Nodes have siblings, a parent, and children which can + be navigated. A node is always in a XMLDocument. + The type of a XMLNode can be queried, and it can + be cast to its more defined type. + + A XMLDocument allocates memory for all its Nodes. + When the XMLDocument gets deleted, all its Nodes + will also be deleted. + + @verbatim + A Document can contain: Element (container or leaf) + Comment (leaf) + Unknown (leaf) + Declaration( leaf ) + + An Element can contain: Element (container or leaf) + Text (leaf) + Attributes (not on tree) + Comment (leaf) + Unknown (leaf) + + @endverbatim +*/ +class TINYXML2_LIB XMLNode +{ + friend class XMLDocument; + friend class XMLElement; +public: + + /// Get the XMLDocument that owns this XMLNode. + const XMLDocument* GetDocument() const { + TIXMLASSERT( _document ); + return _document; + } + /// Get the XMLDocument that owns this XMLNode. + XMLDocument* GetDocument() { + TIXMLASSERT( _document ); + return _document; + } + + /// Safely cast to an Element, or null. + virtual XMLElement* ToElement() { + return 0; + } + /// Safely cast to Text, or null. + virtual XMLText* ToText() { + return 0; + } + /// Safely cast to a Comment, or null. + virtual XMLComment* ToComment() { + return 0; + } + /// Safely cast to a Document, or null. + virtual XMLDocument* ToDocument() { + return 0; + } + /// Safely cast to a Declaration, or null. + virtual XMLDeclaration* ToDeclaration() { + return 0; + } + /// Safely cast to an Unknown, or null. + virtual XMLUnknown* ToUnknown() { + return 0; + } + + virtual const XMLElement* ToElement() const { + return 0; + } + virtual const XMLText* ToText() const { + return 0; + } + virtual const XMLComment* ToComment() const { + return 0; + } + virtual const XMLDocument* ToDocument() const { + return 0; + } + virtual const XMLDeclaration* ToDeclaration() const { + return 0; + } + virtual const XMLUnknown* ToUnknown() const { + return 0; + } + + /** The meaning of 'value' changes for the specific type. + @verbatim + Document: empty (NULL is returned, not an empty string) + Element: name of the element + Comment: the comment text + Unknown: the tag contents + Text: the text string + @endverbatim + */ + const char* Value() const; + + /** Set the Value of an XML node. + @sa Value() + */ + void SetValue( const char* val, bool staticMem=false ); + + /// Gets the line number the node is in, if the document was parsed from a file. + int GetLineNum() const { return _parseLineNum; } + + /// Get the parent of this node on the DOM. + const XMLNode* Parent() const { + return _parent; + } + + XMLNode* Parent() { + return _parent; + } + + /// Returns true if this node has no children. + bool NoChildren() const { + return !_firstChild; + } + + /// Get the first child node, or null if none exists. + const XMLNode* FirstChild() const { + return _firstChild; + } + + XMLNode* FirstChild() { + return _firstChild; + } + + /** Get the first child element, or optionally the first child + element with the specified name. + */ + const XMLElement* FirstChildElement( const char* name = 0 ) const; + + XMLElement* FirstChildElement( const char* name = 0 ) { + return const_cast(const_cast(this)->FirstChildElement( name )); + } + + /// Get the last child node, or null if none exists. + const XMLNode* LastChild() const { + return _lastChild; + } + + XMLNode* LastChild() { + return _lastChild; + } + + /** Get the last child element or optionally the last child + element with the specified name. + */ + const XMLElement* LastChildElement( const char* name = 0 ) const; + + XMLElement* LastChildElement( const char* name = 0 ) { + return const_cast(const_cast(this)->LastChildElement(name) ); + } + + /// Get the previous (left) sibling node of this node. + const XMLNode* PreviousSibling() const { + return _prev; + } + + XMLNode* PreviousSibling() { + return _prev; + } + + /// Get the previous (left) sibling element of this node, with an optionally supplied name. + const XMLElement* PreviousSiblingElement( const char* name = 0 ) const ; + + XMLElement* PreviousSiblingElement( const char* name = 0 ) { + return const_cast(const_cast(this)->PreviousSiblingElement( name ) ); + } + + /// Get the next (right) sibling node of this node. + const XMLNode* NextSibling() const { + return _next; + } + + XMLNode* NextSibling() { + return _next; + } + + /// Get the next (right) sibling element of this node, with an optionally supplied name. + const XMLElement* NextSiblingElement( const char* name = 0 ) const; + + XMLElement* NextSiblingElement( const char* name = 0 ) { + return const_cast(const_cast(this)->NextSiblingElement( name ) ); + } + + /** + Add a child node as the last (right) child. + If the child node is already part of the document, + it is moved from its old location to the new location. + Returns the addThis argument or 0 if the node does not + belong to the same document. + */ + XMLNode* InsertEndChild( XMLNode* addThis ); + + XMLNode* LinkEndChild( XMLNode* addThis ) { + return InsertEndChild( addThis ); + } + /** + Add a child node as the first (left) child. + If the child node is already part of the document, + it is moved from its old location to the new location. + Returns the addThis argument or 0 if the node does not + belong to the same document. + */ + XMLNode* InsertFirstChild( XMLNode* addThis ); + /** + Add a node after the specified child node. + If the child node is already part of the document, + it is moved from its old location to the new location. + Returns the addThis argument or 0 if the afterThis node + is not a child of this node, or if the node does not + belong to the same document. + */ + XMLNode* InsertAfterChild( XMLNode* afterThis, XMLNode* addThis ); + + /** + Delete all the children of this node. + */ + void DeleteChildren(); + + /** + Delete a child of this node. + */ + void DeleteChild( XMLNode* node ); + + /** + Make a copy of this node, but not its children. + You may pass in a Document pointer that will be + the owner of the new Node. If the 'document' is + null, then the node returned will be allocated + from the current Document. (this->GetDocument()) + + Note: if called on a XMLDocument, this will return null. + */ + virtual XMLNode* ShallowClone( XMLDocument* document ) const = 0; + + /** + Make a copy of this node and all its children. + + If the 'target' is null, then the nodes will + be allocated in the current document. If 'target' + is specified, the memory will be allocated is the + specified XMLDocument. + + NOTE: This is probably not the correct tool to + copy a document, since XMLDocuments can have multiple + top level XMLNodes. You probably want to use + XMLDocument::DeepCopy() + */ + XMLNode* DeepClone( XMLDocument* target ) const; + + /** + Test if 2 nodes are the same, but don't test children. + The 2 nodes do not need to be in the same Document. + + Note: if called on a XMLDocument, this will return false. + */ + virtual bool ShallowEqual( const XMLNode* compare ) const = 0; + + /** Accept a hierarchical visit of the nodes in the TinyXML-2 DOM. Every node in the + XML tree will be conditionally visited and the host will be called back + via the XMLVisitor interface. + + This is essentially a SAX interface for TinyXML-2. (Note however it doesn't re-parse + the XML for the callbacks, so the performance of TinyXML-2 is unchanged by using this + interface versus any other.) + + The interface has been based on ideas from: + + - http://www.saxproject.org/ + - http://c2.com/cgi/wiki?HierarchicalVisitorPattern + + Which are both good references for "visiting". + + An example of using Accept(): + @verbatim + XMLPrinter printer; + tinyxmlDoc.Accept( &printer ); + const char* xmlcstr = printer.CStr(); + @endverbatim + */ + virtual bool Accept( XMLVisitor* visitor ) const = 0; + + /** + Set user data into the XMLNode. TinyXML-2 in + no way processes or interprets user data. + It is initially 0. + */ + void SetUserData(void* userData) { _userData = userData; } + + /** + Get user data set into the XMLNode. TinyXML-2 in + no way processes or interprets user data. + It is initially 0. + */ + void* GetUserData() const { return _userData; } + +protected: + explicit XMLNode( XMLDocument* ); + virtual ~XMLNode(); + + virtual char* ParseDeep( char* p, StrPair* parentEndTag, int* curLineNumPtr); + + XMLDocument* _document; + XMLNode* _parent; + mutable StrPair _value; + int _parseLineNum; + + XMLNode* _firstChild; + XMLNode* _lastChild; + + XMLNode* _prev; + XMLNode* _next; + + void* _userData; + +private: + MemPool* _memPool; + void Unlink( XMLNode* child ); + static void DeleteNode( XMLNode* node ); + void InsertChildPreamble( XMLNode* insertThis ) const; + const XMLElement* ToElementWithName( const char* name ) const; + + XMLNode( const XMLNode& ); // not supported + XMLNode& operator=( const XMLNode& ); // not supported +}; + + +/** XML text. + + Note that a text node can have child element nodes, for example: + @verbatim + This is bold + @endverbatim + + A text node can have 2 ways to output the next. "normal" output + and CDATA. It will default to the mode it was parsed from the XML file and + you generally want to leave it alone, but you can change the output mode with + SetCData() and query it with CData(). +*/ +class TINYXML2_LIB XMLText : public XMLNode +{ + friend class XMLDocument; +public: + virtual bool Accept( XMLVisitor* visitor ) const; + + virtual XMLText* ToText() { + return this; + } + virtual const XMLText* ToText() const { + return this; + } + + /// Declare whether this should be CDATA or standard text. + void SetCData( bool isCData ) { + _isCData = isCData; + } + /// Returns true if this is a CDATA text element. + bool CData() const { + return _isCData; + } + + virtual XMLNode* ShallowClone( XMLDocument* document ) const; + virtual bool ShallowEqual( const XMLNode* compare ) const; + +protected: + explicit XMLText( XMLDocument* doc ) : XMLNode( doc ), _isCData( false ) {} + virtual ~XMLText() {} + + char* ParseDeep( char* p, StrPair* parentEndTag, int* curLineNumPtr ); + +private: + bool _isCData; + + XMLText( const XMLText& ); // not supported + XMLText& operator=( const XMLText& ); // not supported +}; + + +/** An XML Comment. */ +class TINYXML2_LIB XMLComment : public XMLNode +{ + friend class XMLDocument; +public: + virtual XMLComment* ToComment() { + return this; + } + virtual const XMLComment* ToComment() const { + return this; + } + + virtual bool Accept( XMLVisitor* visitor ) const; + + virtual XMLNode* ShallowClone( XMLDocument* document ) const; + virtual bool ShallowEqual( const XMLNode* compare ) const; + +protected: + explicit XMLComment( XMLDocument* doc ); + virtual ~XMLComment(); + + char* ParseDeep( char* p, StrPair* parentEndTag, int* curLineNumPtr); + +private: + XMLComment( const XMLComment& ); // not supported + XMLComment& operator=( const XMLComment& ); // not supported +}; + + +/** In correct XML the declaration is the first entry in the file. + @verbatim + + @endverbatim + + TinyXML-2 will happily read or write files without a declaration, + however. + + The text of the declaration isn't interpreted. It is parsed + and written as a string. +*/ +class TINYXML2_LIB XMLDeclaration : public XMLNode +{ + friend class XMLDocument; +public: + virtual XMLDeclaration* ToDeclaration() { + return this; + } + virtual const XMLDeclaration* ToDeclaration() const { + return this; + } + + virtual bool Accept( XMLVisitor* visitor ) const; + + virtual XMLNode* ShallowClone( XMLDocument* document ) const; + virtual bool ShallowEqual( const XMLNode* compare ) const; + +protected: + explicit XMLDeclaration( XMLDocument* doc ); + virtual ~XMLDeclaration(); + + char* ParseDeep( char* p, StrPair* parentEndTag, int* curLineNumPtr ); + +private: + XMLDeclaration( const XMLDeclaration& ); // not supported + XMLDeclaration& operator=( const XMLDeclaration& ); // not supported +}; + + +/** Any tag that TinyXML-2 doesn't recognize is saved as an + unknown. It is a tag of text, but should not be modified. + It will be written back to the XML, unchanged, when the file + is saved. + + DTD tags get thrown into XMLUnknowns. +*/ +class TINYXML2_LIB XMLUnknown : public XMLNode +{ + friend class XMLDocument; +public: + virtual XMLUnknown* ToUnknown() { + return this; + } + virtual const XMLUnknown* ToUnknown() const { + return this; + } + + virtual bool Accept( XMLVisitor* visitor ) const; + + virtual XMLNode* ShallowClone( XMLDocument* document ) const; + virtual bool ShallowEqual( const XMLNode* compare ) const; + +protected: + explicit XMLUnknown( XMLDocument* doc ); + virtual ~XMLUnknown(); + + char* ParseDeep( char* p, StrPair* parentEndTag, int* curLineNumPtr ); + +private: + XMLUnknown( const XMLUnknown& ); // not supported + XMLUnknown& operator=( const XMLUnknown& ); // not supported +}; + + + +/** An attribute is a name-value pair. Elements have an arbitrary + number of attributes, each with a unique name. + + @note The attributes are not XMLNodes. You may only query the + Next() attribute in a list. +*/ +class TINYXML2_LIB XMLAttribute +{ + friend class XMLElement; +public: + /// The name of the attribute. + const char* Name() const; + + /// The value of the attribute. + const char* Value() const; + + /// Gets the line number the attribute is in, if the document was parsed from a file. + int GetLineNum() const { return _parseLineNum; } + + /// The next attribute in the list. + const XMLAttribute* Next() const { + return _next; + } + + /** IntValue interprets the attribute as an integer, and returns the value. + If the value isn't an integer, 0 will be returned. There is no error checking; + use QueryIntValue() if you need error checking. + */ + int IntValue() const { + int i = 0; + QueryIntValue(&i); + return i; + } + + int64_t Int64Value() const { + int64_t i = 0; + QueryInt64Value(&i); + return i; + } + + uint64_t Unsigned64Value() const { + uint64_t i = 0; + QueryUnsigned64Value(&i); + return i; + } + + /// Query as an unsigned integer. See IntValue() + unsigned UnsignedValue() const { + unsigned i=0; + QueryUnsignedValue( &i ); + return i; + } + /// Query as a boolean. See IntValue() + bool BoolValue() const { + bool b=false; + QueryBoolValue( &b ); + return b; + } + /// Query as a double. See IntValue() + double DoubleValue() const { + double d=0; + QueryDoubleValue( &d ); + return d; + } + /// Query as a float. See IntValue() + float FloatValue() const { + float f=0; + QueryFloatValue( &f ); + return f; + } + + /** QueryIntValue interprets the attribute as an integer, and returns the value + in the provided parameter. The function will return XML_SUCCESS on success, + and XML_WRONG_ATTRIBUTE_TYPE if the conversion is not successful. + */ + XMLError QueryIntValue( int* value ) const; + /// See QueryIntValue + XMLError QueryUnsignedValue( unsigned int* value ) const; + /// See QueryIntValue + XMLError QueryInt64Value(int64_t* value) const; + /// See QueryIntValue + XMLError QueryUnsigned64Value(uint64_t* value) const; + /// See QueryIntValue + XMLError QueryBoolValue( bool* value ) const; + /// See QueryIntValue + XMLError QueryDoubleValue( double* value ) const; + /// See QueryIntValue + XMLError QueryFloatValue( float* value ) const; + + /// Set the attribute to a string value. + void SetAttribute( const char* value ); + /// Set the attribute to value. + void SetAttribute( int value ); + /// Set the attribute to value. + void SetAttribute( unsigned value ); + /// Set the attribute to value. + void SetAttribute(int64_t value); + /// Set the attribute to value. + void SetAttribute(uint64_t value); + /// Set the attribute to value. + void SetAttribute( bool value ); + /// Set the attribute to value. + void SetAttribute( double value ); + /// Set the attribute to value. + void SetAttribute( float value ); + +private: + enum { BUF_SIZE = 200 }; + + XMLAttribute() : _name(), _value(),_parseLineNum( 0 ), _next( 0 ), _memPool( 0 ) {} + virtual ~XMLAttribute() {} + + XMLAttribute( const XMLAttribute& ); // not supported + void operator=( const XMLAttribute& ); // not supported + void SetName( const char* name ); + + char* ParseDeep( char* p, bool processEntities, int* curLineNumPtr ); + + mutable StrPair _name; + mutable StrPair _value; + int _parseLineNum; + XMLAttribute* _next; + MemPool* _memPool; +}; + + +/** The element is a container class. It has a value, the element name, + and can contain other elements, text, comments, and unknowns. + Elements also contain an arbitrary number of attributes. +*/ +class TINYXML2_LIB XMLElement : public XMLNode +{ + friend class XMLDocument; +public: + /// Get the name of an element (which is the Value() of the node.) + const char* Name() const { + return Value(); + } + /// Set the name of the element. + void SetName( const char* str, bool staticMem=false ) { + SetValue( str, staticMem ); + } + + virtual XMLElement* ToElement() { + return this; + } + virtual const XMLElement* ToElement() const { + return this; + } + virtual bool Accept( XMLVisitor* visitor ) const; + + /** Given an attribute name, Attribute() returns the value + for the attribute of that name, or null if none + exists. For example: + + @verbatim + const char* value = ele->Attribute( "foo" ); + @endverbatim + + The 'value' parameter is normally null. However, if specified, + the attribute will only be returned if the 'name' and 'value' + match. This allow you to write code: + + @verbatim + if ( ele->Attribute( "foo", "bar" ) ) callFooIsBar(); + @endverbatim + + rather than: + @verbatim + if ( ele->Attribute( "foo" ) ) { + if ( strcmp( ele->Attribute( "foo" ), "bar" ) == 0 ) callFooIsBar(); + } + @endverbatim + */ + const char* Attribute( const char* name, const char* value=0 ) const; + + /** Given an attribute name, IntAttribute() returns the value + of the attribute interpreted as an integer. The default + value will be returned if the attribute isn't present, + or if there is an error. (For a method with error + checking, see QueryIntAttribute()). + */ + int IntAttribute(const char* name, int defaultValue = 0) const; + /// See IntAttribute() + unsigned UnsignedAttribute(const char* name, unsigned defaultValue = 0) const; + /// See IntAttribute() + int64_t Int64Attribute(const char* name, int64_t defaultValue = 0) const; + /// See IntAttribute() + uint64_t Unsigned64Attribute(const char* name, uint64_t defaultValue = 0) const; + /// See IntAttribute() + bool BoolAttribute(const char* name, bool defaultValue = false) const; + /// See IntAttribute() + double DoubleAttribute(const char* name, double defaultValue = 0) const; + /// See IntAttribute() + float FloatAttribute(const char* name, float defaultValue = 0) const; + + /** Given an attribute name, QueryIntAttribute() returns + XML_SUCCESS, XML_WRONG_ATTRIBUTE_TYPE if the conversion + can't be performed, or XML_NO_ATTRIBUTE if the attribute + doesn't exist. If successful, the result of the conversion + will be written to 'value'. If not successful, nothing will + be written to 'value'. This allows you to provide default + value: + + @verbatim + int value = 10; + QueryIntAttribute( "foo", &value ); // if "foo" isn't found, value will still be 10 + @endverbatim + */ + XMLError QueryIntAttribute( const char* name, int* value ) const { + const XMLAttribute* a = FindAttribute( name ); + if ( !a ) { + return XML_NO_ATTRIBUTE; + } + return a->QueryIntValue( value ); + } + + /// See QueryIntAttribute() + XMLError QueryUnsignedAttribute( const char* name, unsigned int* value ) const { + const XMLAttribute* a = FindAttribute( name ); + if ( !a ) { + return XML_NO_ATTRIBUTE; + } + return a->QueryUnsignedValue( value ); + } + + /// See QueryIntAttribute() + XMLError QueryInt64Attribute(const char* name, int64_t* value) const { + const XMLAttribute* a = FindAttribute(name); + if (!a) { + return XML_NO_ATTRIBUTE; + } + return a->QueryInt64Value(value); + } + + /// See QueryIntAttribute() + XMLError QueryUnsigned64Attribute(const char* name, uint64_t* value) const { + const XMLAttribute* a = FindAttribute(name); + if(!a) { + return XML_NO_ATTRIBUTE; + } + return a->QueryUnsigned64Value(value); + } + + /// See QueryIntAttribute() + XMLError QueryBoolAttribute( const char* name, bool* value ) const { + const XMLAttribute* a = FindAttribute( name ); + if ( !a ) { + return XML_NO_ATTRIBUTE; + } + return a->QueryBoolValue( value ); + } + /// See QueryIntAttribute() + XMLError QueryDoubleAttribute( const char* name, double* value ) const { + const XMLAttribute* a = FindAttribute( name ); + if ( !a ) { + return XML_NO_ATTRIBUTE; + } + return a->QueryDoubleValue( value ); + } + /// See QueryIntAttribute() + XMLError QueryFloatAttribute( const char* name, float* value ) const { + const XMLAttribute* a = FindAttribute( name ); + if ( !a ) { + return XML_NO_ATTRIBUTE; + } + return a->QueryFloatValue( value ); + } + + /// See QueryIntAttribute() + XMLError QueryStringAttribute(const char* name, const char** value) const { + const XMLAttribute* a = FindAttribute(name); + if (!a) { + return XML_NO_ATTRIBUTE; + } + *value = a->Value(); + return XML_SUCCESS; + } + + + + /** Given an attribute name, QueryAttribute() returns + XML_SUCCESS, XML_WRONG_ATTRIBUTE_TYPE if the conversion + can't be performed, or XML_NO_ATTRIBUTE if the attribute + doesn't exist. It is overloaded for the primitive types, + and is a generally more convenient replacement of + QueryIntAttribute() and related functions. + + If successful, the result of the conversion + will be written to 'value'. If not successful, nothing will + be written to 'value'. This allows you to provide default + value: + + @verbatim + int value = 10; + QueryAttribute( "foo", &value ); // if "foo" isn't found, value will still be 10 + @endverbatim + */ + XMLError QueryAttribute( const char* name, int* value ) const { + return QueryIntAttribute( name, value ); + } + + XMLError QueryAttribute( const char* name, unsigned int* value ) const { + return QueryUnsignedAttribute( name, value ); + } + + XMLError QueryAttribute(const char* name, int64_t* value) const { + return QueryInt64Attribute(name, value); + } + + XMLError QueryAttribute(const char* name, uint64_t* value) const { + return QueryUnsigned64Attribute(name, value); + } + + XMLError QueryAttribute( const char* name, bool* value ) const { + return QueryBoolAttribute( name, value ); + } + + XMLError QueryAttribute( const char* name, double* value ) const { + return QueryDoubleAttribute( name, value ); + } + + XMLError QueryAttribute( const char* name, float* value ) const { + return QueryFloatAttribute( name, value ); + } + + XMLError QueryAttribute(const char* name, const char** value) const { + return QueryStringAttribute(name, value); + } + + /// Sets the named attribute to value. + void SetAttribute( const char* name, const char* value ) { + XMLAttribute* a = FindOrCreateAttribute( name ); + a->SetAttribute( value ); + } + /// Sets the named attribute to value. + void SetAttribute( const char* name, int value ) { + XMLAttribute* a = FindOrCreateAttribute( name ); + a->SetAttribute( value ); + } + /// Sets the named attribute to value. + void SetAttribute( const char* name, unsigned value ) { + XMLAttribute* a = FindOrCreateAttribute( name ); + a->SetAttribute( value ); + } + + /// Sets the named attribute to value. + void SetAttribute(const char* name, int64_t value) { + XMLAttribute* a = FindOrCreateAttribute(name); + a->SetAttribute(value); + } + + /// Sets the named attribute to value. + void SetAttribute(const char* name, uint64_t value) { + XMLAttribute* a = FindOrCreateAttribute(name); + a->SetAttribute(value); + } + + /// Sets the named attribute to value. + void SetAttribute( const char* name, bool value ) { + XMLAttribute* a = FindOrCreateAttribute( name ); + a->SetAttribute( value ); + } + /// Sets the named attribute to value. + void SetAttribute( const char* name, double value ) { + XMLAttribute* a = FindOrCreateAttribute( name ); + a->SetAttribute( value ); + } + /// Sets the named attribute to value. + void SetAttribute( const char* name, float value ) { + XMLAttribute* a = FindOrCreateAttribute( name ); + a->SetAttribute( value ); + } + + /** + Delete an attribute. + */ + void DeleteAttribute( const char* name ); + + /// Return the first attribute in the list. + const XMLAttribute* FirstAttribute() const { + return _rootAttribute; + } + /// Query a specific attribute in the list. + const XMLAttribute* FindAttribute( const char* name ) const; + + /** Convenience function for easy access to the text inside an element. Although easy + and concise, GetText() is limited compared to getting the XMLText child + and accessing it directly. + + If the first child of 'this' is a XMLText, the GetText() + returns the character string of the Text node, else null is returned. + + This is a convenient method for getting the text of simple contained text: + @verbatim + This is text + const char* str = fooElement->GetText(); + @endverbatim + + 'str' will be a pointer to "This is text". + + Note that this function can be misleading. If the element foo was created from + this XML: + @verbatim + This is text + @endverbatim + + then the value of str would be null. The first child node isn't a text node, it is + another element. From this XML: + @verbatim + This is text + @endverbatim + GetText() will return "This is ". + */ + const char* GetText() const; + + /** Convenience function for easy access to the text inside an element. Although easy + and concise, SetText() is limited compared to creating an XMLText child + and mutating it directly. + + If the first child of 'this' is a XMLText, SetText() sets its value to + the given string, otherwise it will create a first child that is an XMLText. + + This is a convenient method for setting the text of simple contained text: + @verbatim + This is text + fooElement->SetText( "Hullaballoo!" ); + Hullaballoo! + @endverbatim + + Note that this function can be misleading. If the element foo was created from + this XML: + @verbatim + This is text + @endverbatim + + then it will not change "This is text", but rather prefix it with a text element: + @verbatim + Hullaballoo!This is text + @endverbatim + + For this XML: + @verbatim + + @endverbatim + SetText() will generate + @verbatim + Hullaballoo! + @endverbatim + */ + void SetText( const char* inText ); + /// Convenience method for setting text inside an element. See SetText() for important limitations. + void SetText( int value ); + /// Convenience method for setting text inside an element. See SetText() for important limitations. + void SetText( unsigned value ); + /// Convenience method for setting text inside an element. See SetText() for important limitations. + void SetText(int64_t value); + /// Convenience method for setting text inside an element. See SetText() for important limitations. + void SetText(uint64_t value); + /// Convenience method for setting text inside an element. See SetText() for important limitations. + void SetText( bool value ); + /// Convenience method for setting text inside an element. See SetText() for important limitations. + void SetText( double value ); + /// Convenience method for setting text inside an element. See SetText() for important limitations. + void SetText( float value ); + + /** + Convenience method to query the value of a child text node. This is probably best + shown by example. Given you have a document is this form: + @verbatim + + 1 + 1.4 + + @endverbatim + + The QueryIntText() and similar functions provide a safe and easier way to get to the + "value" of x and y. + + @verbatim + int x = 0; + float y = 0; // types of x and y are contrived for example + const XMLElement* xElement = pointElement->FirstChildElement( "x" ); + const XMLElement* yElement = pointElement->FirstChildElement( "y" ); + xElement->QueryIntText( &x ); + yElement->QueryFloatText( &y ); + @endverbatim + + @returns XML_SUCCESS (0) on success, XML_CAN_NOT_CONVERT_TEXT if the text cannot be converted + to the requested type, and XML_NO_TEXT_NODE if there is no child text to query. + + */ + XMLError QueryIntText( int* ival ) const; + /// See QueryIntText() + XMLError QueryUnsignedText( unsigned* uval ) const; + /// See QueryIntText() + XMLError QueryInt64Text(int64_t* uval) const; + /// See QueryIntText() + XMLError QueryUnsigned64Text(uint64_t* uval) const; + /// See QueryIntText() + XMLError QueryBoolText( bool* bval ) const; + /// See QueryIntText() + XMLError QueryDoubleText( double* dval ) const; + /// See QueryIntText() + XMLError QueryFloatText( float* fval ) const; + + int IntText(int defaultValue = 0) const; + + /// See QueryIntText() + unsigned UnsignedText(unsigned defaultValue = 0) const; + /// See QueryIntText() + int64_t Int64Text(int64_t defaultValue = 0) const; + /// See QueryIntText() + uint64_t Unsigned64Text(uint64_t defaultValue = 0) const; + /// See QueryIntText() + bool BoolText(bool defaultValue = false) const; + /// See QueryIntText() + double DoubleText(double defaultValue = 0) const; + /// See QueryIntText() + float FloatText(float defaultValue = 0) const; + + /** + Convenience method to create a new XMLElement and add it as last (right) + child of this node. Returns the created and inserted element. + */ + XMLElement* InsertNewChildElement(const char* name); + /// See InsertNewChildElement() + XMLComment* InsertNewComment(const char* comment); + /// See InsertNewChildElement() + XMLText* InsertNewText(const char* text); + /// See InsertNewChildElement() + XMLDeclaration* InsertNewDeclaration(const char* text); + /// See InsertNewChildElement() + XMLUnknown* InsertNewUnknown(const char* text); + + + // internal: + enum ElementClosingType { + OPEN, // + CLOSED, // + CLOSING // + }; + ElementClosingType ClosingType() const { + return _closingType; + } + virtual XMLNode* ShallowClone( XMLDocument* document ) const; + virtual bool ShallowEqual( const XMLNode* compare ) const; + +protected: + char* ParseDeep( char* p, StrPair* parentEndTag, int* curLineNumPtr ); + +private: + XMLElement( XMLDocument* doc ); + virtual ~XMLElement(); + XMLElement( const XMLElement& ); // not supported + void operator=( const XMLElement& ); // not supported + + XMLAttribute* FindOrCreateAttribute( const char* name ); + char* ParseAttributes( char* p, int* curLineNumPtr ); + static void DeleteAttribute( XMLAttribute* attribute ); + XMLAttribute* CreateAttribute(); + + enum { BUF_SIZE = 200 }; + ElementClosingType _closingType; + // The attribute list is ordered; there is no 'lastAttribute' + // because the list needs to be scanned for dupes before adding + // a new attribute. + XMLAttribute* _rootAttribute; +}; + + +enum Whitespace { + PRESERVE_WHITESPACE, + COLLAPSE_WHITESPACE +}; + + +/** A Document binds together all the functionality. + It can be saved, loaded, and printed to the screen. + All Nodes are connected and allocated to a Document. + If the Document is deleted, all its Nodes are also deleted. +*/ +class TINYXML2_LIB XMLDocument : public XMLNode +{ + friend class XMLElement; + // Gives access to SetError and Push/PopDepth, but over-access for everything else. + // Wishing C++ had "internal" scope. + friend class XMLNode; + friend class XMLText; + friend class XMLComment; + friend class XMLDeclaration; + friend class XMLUnknown; +public: + /// constructor + XMLDocument( bool processEntities = true, Whitespace whitespaceMode = PRESERVE_WHITESPACE ); + ~XMLDocument(); + + virtual XMLDocument* ToDocument() { + TIXMLASSERT( this == _document ); + return this; + } + virtual const XMLDocument* ToDocument() const { + TIXMLASSERT( this == _document ); + return this; + } + + /** + Parse an XML file from a character string. + Returns XML_SUCCESS (0) on success, or + an errorID. + + You may optionally pass in the 'nBytes', which is + the number of bytes which will be parsed. If not + specified, TinyXML-2 will assume 'xml' points to a + null terminated string. + */ + XMLError Parse( const char* xml, size_t nBytes=static_cast(-1) ); + + /** + Load an XML file from disk. + Returns XML_SUCCESS (0) on success, or + an errorID. + */ + XMLError LoadFile( const char* filename ); + + /** + Load an XML file from disk. You are responsible + for providing and closing the FILE*. + + NOTE: The file should be opened as binary ("rb") + not text in order for TinyXML-2 to correctly + do newline normalization. + + Returns XML_SUCCESS (0) on success, or + an errorID. + */ + XMLError LoadFile( FILE* ); + + /** + Save the XML file to disk. + Returns XML_SUCCESS (0) on success, or + an errorID. + */ + XMLError SaveFile( const char* filename, bool compact = false ); + + /** + Save the XML file to disk. You are responsible + for providing and closing the FILE*. + + Returns XML_SUCCESS (0) on success, or + an errorID. + */ + XMLError SaveFile( FILE* fp, bool compact = false ); + + bool ProcessEntities() const { + return _processEntities; + } + Whitespace WhitespaceMode() const { + return _whitespaceMode; + } + + /** + Returns true if this document has a leading Byte Order Mark of UTF8. + */ + bool HasBOM() const { + return _writeBOM; + } + /** Sets whether to write the BOM when writing the file. + */ + void SetBOM( bool useBOM ) { + _writeBOM = useBOM; + } + + /** Return the root element of DOM. Equivalent to FirstChildElement(). + To get the first node, use FirstChild(). + */ + XMLElement* RootElement() { + return FirstChildElement(); + } + const XMLElement* RootElement() const { + return FirstChildElement(); + } + + /** Print the Document. If the Printer is not provided, it will + print to stdout. If you provide Printer, this can print to a file: + @verbatim + XMLPrinter printer( fp ); + doc.Print( &printer ); + @endverbatim + + Or you can use a printer to print to memory: + @verbatim + XMLPrinter printer; + doc.Print( &printer ); + // printer.CStr() has a const char* to the XML + @endverbatim + */ + void Print( XMLPrinter* streamer=0 ) const; + virtual bool Accept( XMLVisitor* visitor ) const; + + /** + Create a new Element associated with + this Document. The memory for the Element + is managed by the Document. + */ + XMLElement* NewElement( const char* name ); + /** + Create a new Comment associated with + this Document. The memory for the Comment + is managed by the Document. + */ + XMLComment* NewComment( const char* comment ); + /** + Create a new Text associated with + this Document. The memory for the Text + is managed by the Document. + */ + XMLText* NewText( const char* text ); + /** + Create a new Declaration associated with + this Document. The memory for the object + is managed by the Document. + + If the 'text' param is null, the standard + declaration is used.: + @verbatim + + @endverbatim + */ + XMLDeclaration* NewDeclaration( const char* text=0 ); + /** + Create a new Unknown associated with + this Document. The memory for the object + is managed by the Document. + */ + XMLUnknown* NewUnknown( const char* text ); + + /** + Delete a node associated with this document. + It will be unlinked from the DOM. + */ + void DeleteNode( XMLNode* node ); + + /// Clears the error flags. + void ClearError(); + + /// Return true if there was an error parsing the document. + bool Error() const { + return _errorID != XML_SUCCESS; + } + /// Return the errorID. + XMLError ErrorID() const { + return _errorID; + } + const char* ErrorName() const; + static const char* ErrorIDToName(XMLError errorID); + + /** Returns a "long form" error description. A hopefully helpful + diagnostic with location, line number, and/or additional info. + */ + const char* ErrorStr() const; + + /// A (trivial) utility function that prints the ErrorStr() to stdout. + void PrintError() const; + + /// Return the line where the error occurred, or zero if unknown. + int ErrorLineNum() const + { + return _errorLineNum; + } + + /// Clear the document, resetting it to the initial state. + void Clear(); + + /** + Copies this document to a target document. + The target will be completely cleared before the copy. + If you want to copy a sub-tree, see XMLNode::DeepClone(). + + NOTE: that the 'target' must be non-null. + */ + void DeepCopy(XMLDocument* target) const; + + // internal + char* Identify( char* p, XMLNode** node ); + + // internal + void MarkInUse(const XMLNode* const); + + virtual XMLNode* ShallowClone( XMLDocument* /*document*/ ) const { + return 0; + } + virtual bool ShallowEqual( const XMLNode* /*compare*/ ) const { + return false; + } + +private: + XMLDocument( const XMLDocument& ); // not supported + void operator=( const XMLDocument& ); // not supported + + bool _writeBOM; + bool _processEntities; + XMLError _errorID; + Whitespace _whitespaceMode; + mutable StrPair _errorStr; + int _errorLineNum; + char* _charBuffer; + int _parseCurLineNum; + int _parsingDepth; + // Memory tracking does add some overhead. + // However, the code assumes that you don't + // have a bunch of unlinked nodes around. + // Therefore it takes less memory to track + // in the document vs. a linked list in the XMLNode, + // and the performance is the same. + DynArray _unlinked; + + MemPoolT< sizeof(XMLElement) > _elementPool; + MemPoolT< sizeof(XMLAttribute) > _attributePool; + MemPoolT< sizeof(XMLText) > _textPool; + MemPoolT< sizeof(XMLComment) > _commentPool; + + static const char* _errorNames[XML_ERROR_COUNT]; + + void Parse(); + + void SetError( XMLError error, int lineNum, const char* format, ... ); + + // Something of an obvious security hole, once it was discovered. + // Either an ill-formed XML or an excessively deep one can overflow + // the stack. Track stack depth, and error out if needed. + class DepthTracker { + public: + explicit DepthTracker(XMLDocument * document) { + this->_document = document; + document->PushDepth(); + } + ~DepthTracker() { + _document->PopDepth(); + } + private: + XMLDocument * _document; + }; + void PushDepth(); + void PopDepth(); + + template + NodeType* CreateUnlinkedNode( MemPoolT& pool ); +}; + +template +inline NodeType* XMLDocument::CreateUnlinkedNode( MemPoolT& pool ) +{ + TIXMLASSERT( sizeof( NodeType ) == PoolElementSize ); + TIXMLASSERT( sizeof( NodeType ) == pool.ItemSize() ); + NodeType* returnNode = new (pool.Alloc()) NodeType( this ); + TIXMLASSERT( returnNode ); + returnNode->_memPool = &pool; + + _unlinked.Push(returnNode); + return returnNode; +} + +/** + A XMLHandle is a class that wraps a node pointer with null checks; this is + an incredibly useful thing. Note that XMLHandle is not part of the TinyXML-2 + DOM structure. It is a separate utility class. + + Take an example: + @verbatim + + + + + + + @endverbatim + + Assuming you want the value of "attributeB" in the 2nd "Child" element, it's very + easy to write a *lot* of code that looks like: + + @verbatim + XMLElement* root = document.FirstChildElement( "Document" ); + if ( root ) + { + XMLElement* element = root->FirstChildElement( "Element" ); + if ( element ) + { + XMLElement* child = element->FirstChildElement( "Child" ); + if ( child ) + { + XMLElement* child2 = child->NextSiblingElement( "Child" ); + if ( child2 ) + { + // Finally do something useful. + @endverbatim + + And that doesn't even cover "else" cases. XMLHandle addresses the verbosity + of such code. A XMLHandle checks for null pointers so it is perfectly safe + and correct to use: + + @verbatim + XMLHandle docHandle( &document ); + XMLElement* child2 = docHandle.FirstChildElement( "Document" ).FirstChildElement( "Element" ).FirstChildElement().NextSiblingElement(); + if ( child2 ) + { + // do something useful + @endverbatim + + Which is MUCH more concise and useful. + + It is also safe to copy handles - internally they are nothing more than node pointers. + @verbatim + XMLHandle handleCopy = handle; + @endverbatim + + See also XMLConstHandle, which is the same as XMLHandle, but operates on const objects. +*/ +class TINYXML2_LIB XMLHandle +{ +public: + /// Create a handle from any node (at any depth of the tree.) This can be a null pointer. + explicit XMLHandle( XMLNode* node ) : _node( node ) { + } + /// Create a handle from a node. + explicit XMLHandle( XMLNode& node ) : _node( &node ) { + } + /// Copy constructor + XMLHandle( const XMLHandle& ref ) : _node( ref._node ) { + } + /// Assignment + XMLHandle& operator=( const XMLHandle& ref ) { + _node = ref._node; + return *this; + } + + /// Get the first child of this handle. + XMLHandle FirstChild() { + return XMLHandle( _node ? _node->FirstChild() : 0 ); + } + /// Get the first child element of this handle. + XMLHandle FirstChildElement( const char* name = 0 ) { + return XMLHandle( _node ? _node->FirstChildElement( name ) : 0 ); + } + /// Get the last child of this handle. + XMLHandle LastChild() { + return XMLHandle( _node ? _node->LastChild() : 0 ); + } + /// Get the last child element of this handle. + XMLHandle LastChildElement( const char* name = 0 ) { + return XMLHandle( _node ? _node->LastChildElement( name ) : 0 ); + } + /// Get the previous sibling of this handle. + XMLHandle PreviousSibling() { + return XMLHandle( _node ? _node->PreviousSibling() : 0 ); + } + /// Get the previous sibling element of this handle. + XMLHandle PreviousSiblingElement( const char* name = 0 ) { + return XMLHandle( _node ? _node->PreviousSiblingElement( name ) : 0 ); + } + /// Get the next sibling of this handle. + XMLHandle NextSibling() { + return XMLHandle( _node ? _node->NextSibling() : 0 ); + } + /// Get the next sibling element of this handle. + XMLHandle NextSiblingElement( const char* name = 0 ) { + return XMLHandle( _node ? _node->NextSiblingElement( name ) : 0 ); + } + + /// Safe cast to XMLNode. This can return null. + XMLNode* ToNode() { + return _node; + } + /// Safe cast to XMLElement. This can return null. + XMLElement* ToElement() { + return ( _node ? _node->ToElement() : 0 ); + } + /// Safe cast to XMLText. This can return null. + XMLText* ToText() { + return ( _node ? _node->ToText() : 0 ); + } + /// Safe cast to XMLUnknown. This can return null. + XMLUnknown* ToUnknown() { + return ( _node ? _node->ToUnknown() : 0 ); + } + /// Safe cast to XMLDeclaration. This can return null. + XMLDeclaration* ToDeclaration() { + return ( _node ? _node->ToDeclaration() : 0 ); + } + +private: + XMLNode* _node; +}; + + +/** + A variant of the XMLHandle class for working with const XMLNodes and Documents. It is the + same in all regards, except for the 'const' qualifiers. See XMLHandle for API. +*/ +class TINYXML2_LIB XMLConstHandle +{ +public: + explicit XMLConstHandle( const XMLNode* node ) : _node( node ) { + } + explicit XMLConstHandle( const XMLNode& node ) : _node( &node ) { + } + XMLConstHandle( const XMLConstHandle& ref ) : _node( ref._node ) { + } + + XMLConstHandle& operator=( const XMLConstHandle& ref ) { + _node = ref._node; + return *this; + } + + const XMLConstHandle FirstChild() const { + return XMLConstHandle( _node ? _node->FirstChild() : 0 ); + } + const XMLConstHandle FirstChildElement( const char* name = 0 ) const { + return XMLConstHandle( _node ? _node->FirstChildElement( name ) : 0 ); + } + const XMLConstHandle LastChild() const { + return XMLConstHandle( _node ? _node->LastChild() : 0 ); + } + const XMLConstHandle LastChildElement( const char* name = 0 ) const { + return XMLConstHandle( _node ? _node->LastChildElement( name ) : 0 ); + } + const XMLConstHandle PreviousSibling() const { + return XMLConstHandle( _node ? _node->PreviousSibling() : 0 ); + } + const XMLConstHandle PreviousSiblingElement( const char* name = 0 ) const { + return XMLConstHandle( _node ? _node->PreviousSiblingElement( name ) : 0 ); + } + const XMLConstHandle NextSibling() const { + return XMLConstHandle( _node ? _node->NextSibling() : 0 ); + } + const XMLConstHandle NextSiblingElement( const char* name = 0 ) const { + return XMLConstHandle( _node ? _node->NextSiblingElement( name ) : 0 ); + } + + + const XMLNode* ToNode() const { + return _node; + } + const XMLElement* ToElement() const { + return ( _node ? _node->ToElement() : 0 ); + } + const XMLText* ToText() const { + return ( _node ? _node->ToText() : 0 ); + } + const XMLUnknown* ToUnknown() const { + return ( _node ? _node->ToUnknown() : 0 ); + } + const XMLDeclaration* ToDeclaration() const { + return ( _node ? _node->ToDeclaration() : 0 ); + } + +private: + const XMLNode* _node; +}; + + +/** + Printing functionality. The XMLPrinter gives you more + options than the XMLDocument::Print() method. + + It can: + -# Print to memory. + -# Print to a file you provide. + -# Print XML without a XMLDocument. + + Print to Memory + + @verbatim + XMLPrinter printer; + doc.Print( &printer ); + SomeFunction( printer.CStr() ); + @endverbatim + + Print to a File + + You provide the file pointer. + @verbatim + XMLPrinter printer( fp ); + doc.Print( &printer ); + @endverbatim + + Print without a XMLDocument + + When loading, an XML parser is very useful. However, sometimes + when saving, it just gets in the way. The code is often set up + for streaming, and constructing the DOM is just overhead. + + The Printer supports the streaming case. The following code + prints out a trivially simple XML file without ever creating + an XML document. + + @verbatim + XMLPrinter printer( fp ); + printer.OpenElement( "foo" ); + printer.PushAttribute( "foo", "bar" ); + printer.CloseElement(); + @endverbatim +*/ +class TINYXML2_LIB XMLPrinter : public XMLVisitor +{ +public: + /** Construct the printer. If the FILE* is specified, + this will print to the FILE. Else it will print + to memory, and the result is available in CStr(). + If 'compact' is set to true, then output is created + with only required whitespace and newlines. + */ + XMLPrinter( FILE* file=0, bool compact = false, int depth = 0 ); + virtual ~XMLPrinter() {} + + /** If streaming, write the BOM and declaration. */ + void PushHeader( bool writeBOM, bool writeDeclaration ); + /** If streaming, start writing an element. + The element must be closed with CloseElement() + */ + void OpenElement( const char* name, bool compactMode=false ); + /// If streaming, add an attribute to an open element. + void PushAttribute( const char* name, const char* value ); + void PushAttribute( const char* name, int value ); + void PushAttribute( const char* name, unsigned value ); + void PushAttribute( const char* name, int64_t value ); + void PushAttribute( const char* name, uint64_t value ); + void PushAttribute( const char* name, bool value ); + void PushAttribute( const char* name, double value ); + /// If streaming, close the Element. + virtual void CloseElement( bool compactMode=false ); + + /// Add a text node. + void PushText( const char* text, bool cdata=false ); + /// Add a text node from an integer. + void PushText( int value ); + /// Add a text node from an unsigned. + void PushText( unsigned value ); + /// Add a text node from a signed 64bit integer. + void PushText( int64_t value ); + /// Add a text node from an unsigned 64bit integer. + void PushText( uint64_t value ); + /// Add a text node from a bool. + void PushText( bool value ); + /// Add a text node from a float. + void PushText( float value ); + /// Add a text node from a double. + void PushText( double value ); + + /// Add a comment + void PushComment( const char* comment ); + + void PushDeclaration( const char* value ); + void PushUnknown( const char* value ); + + virtual bool VisitEnter( const XMLDocument& /*doc*/ ); + virtual bool VisitExit( const XMLDocument& /*doc*/ ) { + return true; + } + + virtual bool VisitEnter( const XMLElement& element, const XMLAttribute* attribute ); + virtual bool VisitExit( const XMLElement& element ); + + virtual bool Visit( const XMLText& text ); + virtual bool Visit( const XMLComment& comment ); + virtual bool Visit( const XMLDeclaration& declaration ); + virtual bool Visit( const XMLUnknown& unknown ); + + /** + If in print to memory mode, return a pointer to + the XML file in memory. + */ + const char* CStr() const { + return _buffer.Mem(); + } + /** + If in print to memory mode, return the size + of the XML file in memory. (Note the size returned + includes the terminating null.) + */ + int CStrSize() const { + return _buffer.Size(); + } + /** + If in print to memory mode, reset the buffer to the + beginning. + */ + void ClearBuffer( bool resetToFirstElement = true ) { + _buffer.Clear(); + _buffer.Push(0); + _firstElement = resetToFirstElement; + } + +protected: + virtual bool CompactMode( const XMLElement& ) { return _compactMode; } + + /** Prints out the space before an element. You may override to change + the space and tabs used. A PrintSpace() override should call Print(). + */ + virtual void PrintSpace( int depth ); + virtual void Print( const char* format, ... ); + virtual void Write( const char* data, size_t size ); + virtual void Putc( char ch ); + + inline void Write(const char* data) { Write(data, strlen(data)); } + + void SealElementIfJustOpened(); + bool _elementJustOpened; + DynArray< const char*, 10 > _stack; + +private: + /** + Prepares to write a new node. This includes sealing an element that was + just opened, and writing any whitespace necessary if not in compact mode. + */ + void PrepareForNewNode( bool compactMode ); + void PrintString( const char*, bool restrictedEntitySet ); // prints out, after detecting entities. + + bool _firstElement; + FILE* _fp; + int _depth; + int _textDepth; + bool _processEntities; + bool _compactMode; + + enum { + ENTITY_RANGE = 64, + BUF_SIZE = 200 + }; + bool _entityFlag[ENTITY_RANGE]; + bool _restrictedEntityFlag[ENTITY_RANGE]; + + DynArray< char, 20 > _buffer; + + // Prohibit cloning, intentionally not implemented + XMLPrinter( const XMLPrinter& ); + XMLPrinter& operator=( const XMLPrinter& ); +}; + + +} // tinyxml2 + +#if defined(_MSC_VER) +# pragma warning(pop) +#endif + +#endif // TINYXML2_INCLUDED diff --git a/libs/IO/json.hpp b/libs/IO/json.hpp new file mode 100644 index 0000000..b6d30f8 --- /dev/null +++ b/libs/IO/json.hpp @@ -0,0 +1,20406 @@ +/* + __ _____ _____ _____ + __| | __| | | | JSON for Modern C++ +| | |__ | | | | | | version 3.5.0 +|_____|_____|_____|_|___| https://github.com/nlohmann/json + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2013-2018 Niels Lohmann . + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef NLOHMANN_JSON_HPP +#define NLOHMANN_JSON_HPP + +#define NLOHMANN_JSON_VERSION_MAJOR 3 +#define NLOHMANN_JSON_VERSION_MINOR 5 +#define NLOHMANN_JSON_VERSION_PATCH 0 + +#include // all_of, find, for_each +#include // assert +#include // and, not, or +#include // nullptr_t, ptrdiff_t, size_t +#include // hash, less +#include // initializer_list +#include // istream, ostream +#include // random_access_iterator_tag +#include // accumulate +#include // string, stoi, to_string +#include // declval, forward, move, pair, swap + +// #include +#ifndef NLOHMANN_JSON_FWD_HPP +#define NLOHMANN_JSON_FWD_HPP + +#include // int64_t, uint64_t +#include // map +#include // allocator +#include // string +#include // vector + +/*! +@brief namespace for Niels Lohmann +@see https://github.com/nlohmann +@since version 1.0.0 +*/ +namespace nlohmann +{ +/*! +@brief default JSONSerializer template argument + +This serializer ignores the template arguments and uses ADL +([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) +for serialization. +*/ +template +struct adl_serializer; + +template class ObjectType = + std::map, + template class ArrayType = std::vector, + class StringType = std::string, class BooleanType = bool, + class NumberIntegerType = std::int64_t, + class NumberUnsignedType = std::uint64_t, + class NumberFloatType = double, + template class AllocatorType = std::allocator, + template class JSONSerializer = + adl_serializer> +class basic_json; + +/*! +@brief JSON Pointer + +A JSON pointer defines a string syntax for identifying a specific value +within a JSON document. It can be used with functions `at` and +`operator[]`. Furthermore, JSON pointers are the base for JSON patches. + +@sa [RFC 6901](https://tools.ietf.org/html/rfc6901) + +@since version 2.0.0 +*/ +template +class json_pointer; + +/*! +@brief default JSON class + +This type is the default specialization of the @ref basic_json class which +uses the standard template types. + +@since version 1.0.0 +*/ +using json = basic_json<>; +} // namespace nlohmann + +#endif + +// #include + + +// This file contains all internal macro definitions +// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them + +// exclude unsupported compilers +#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK) + #if defined(__clang__) + #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400 + #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER)) + #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40800 + #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #endif +#endif + +// disable float-equal warnings on GCC/clang +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wfloat-equal" +#endif + +// disable documentation warnings on clang +#if defined(__clang__) + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdocumentation" +#endif + +// allow for portable deprecation warnings +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #define JSON_DEPRECATED __attribute__((deprecated)) +#elif defined(_MSC_VER) + #define JSON_DEPRECATED __declspec(deprecated) +#else + #define JSON_DEPRECATED +#endif + +// allow to disable exceptions +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + #define JSON_THROW(exception) throw exception + #define JSON_TRY try + #define JSON_CATCH(exception) catch(exception) + #define JSON_INTERNAL_CATCH(exception) catch(exception) +#else + #define JSON_THROW(exception) std::abort() + #define JSON_TRY if(true) + #define JSON_CATCH(exception) if(false) + #define JSON_INTERNAL_CATCH(exception) if(false) +#endif + +// override exception macros +#if defined(JSON_THROW_USER) + #undef JSON_THROW + #define JSON_THROW JSON_THROW_USER +#endif +#if defined(JSON_TRY_USER) + #undef JSON_TRY + #define JSON_TRY JSON_TRY_USER +#endif +#if defined(JSON_CATCH_USER) + #undef JSON_CATCH + #define JSON_CATCH JSON_CATCH_USER + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_CATCH_USER +#endif +#if defined(JSON_INTERNAL_CATCH_USER) + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER +#endif + +// manual branch prediction +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) + #define JSON_LIKELY(x) __builtin_expect(!!(x), 1) + #define JSON_UNLIKELY(x) __builtin_expect(!!(x), 0) +#else + #define JSON_LIKELY(x) x + #define JSON_UNLIKELY(x) x +#endif + +// C++ language standard detection +#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 +#elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) + #define JSON_HAS_CPP_14 +#endif + +/*! +@brief macro to briefly define a mapping between an enum and JSON +@def NLOHMANN_JSON_SERIALIZE_ENUM +@since version 3.4.0 +*/ +#define NLOHMANN_JSON_SERIALIZE_ENUM(ENUM_TYPE, ...) \ + template \ + inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \ + { \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [e](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.first == e; \ + }); \ + j = ((it != std::end(m)) ? it : std::begin(m))->second; \ + } \ + template \ + inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ + { \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [j](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.second == j; \ + }); \ + e = ((it != std::end(m)) ? it : std::begin(m))->first; \ + } + +// Ugly macros to avoid uglier copy-paste when specializing basic_json. They +// may be removed in the future once the class is split. + +#define NLOHMANN_BASIC_JSON_TPL_DECLARATION \ + template class ObjectType, \ + template class ArrayType, \ + class StringType, class BooleanType, class NumberIntegerType, \ + class NumberUnsignedType, class NumberFloatType, \ + template class AllocatorType, \ + template class JSONSerializer> + +#define NLOHMANN_BASIC_JSON_TPL \ + basic_json + +// #include + + +#include // not +#include // size_t +#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type + +namespace nlohmann +{ +namespace detail +{ +// alias templates to reduce boilerplate +template +using enable_if_t = typename std::enable_if::type; + +template +using uncvref_t = typename std::remove_cv::type>::type; + +// implementation of C++14 index_sequence and affiliates +// source: https://stackoverflow.com/a/32223343 +template +struct index_sequence +{ + using type = index_sequence; + using value_type = std::size_t; + static constexpr std::size_t size() noexcept + { + return sizeof...(Ints); + } +}; + +template +struct merge_and_renumber; + +template +struct merge_and_renumber, index_sequence> + : index_sequence < I1..., (sizeof...(I1) + I2)... > {}; + +template +struct make_index_sequence + : merge_and_renumber < typename make_index_sequence < N / 2 >::type, + typename make_index_sequence < N - N / 2 >::type > {}; + +template<> struct make_index_sequence<0> : index_sequence<> {}; +template<> struct make_index_sequence<1> : index_sequence<0> {}; + +template +using index_sequence_for = make_index_sequence; + +// dispatch utility (taken from ranges-v3) +template struct priority_tag : priority_tag < N - 1 > {}; +template<> struct priority_tag<0> {}; + +// taken from ranges-v3 +template +struct static_const +{ + static constexpr T value{}; +}; + +template +constexpr T static_const::value; +} // namespace detail +} // namespace nlohmann + +// #include + + +#include // not +#include // numeric_limits +#include // false_type, is_constructible, is_integral, is_same, true_type +#include // declval + +// #include + +// #include + + +#include // random_access_iterator_tag + +// #include + + +namespace nlohmann +{ +namespace detail +{ +template struct make_void +{ + using type = void; +}; +template using void_t = typename make_void::type; +} // namespace detail +} // namespace nlohmann + +// #include + + +namespace nlohmann +{ +namespace detail +{ +template +struct iterator_types {}; + +template +struct iterator_types < + It, + void_t> +{ + using difference_type = typename It::difference_type; + using value_type = typename It::value_type; + using pointer = typename It::pointer; + using reference = typename It::reference; + using iterator_category = typename It::iterator_category; +}; + +// This is required as some compilers implement std::iterator_traits in a way that +// doesn't work with SFINAE. See https://github.com/nlohmann/json/issues/1341. +template +struct iterator_traits +{ +}; + +template +struct iterator_traits < T, enable_if_t < !std::is_pointer::value >> + : iterator_types +{ +}; + +template +struct iterator_traits::value>> +{ + using iterator_category = std::random_access_iterator_tag; + using value_type = T; + using difference_type = ptrdiff_t; + using pointer = T*; + using reference = T&; +}; +} +} + +// #include + +// #include + + +#include + +// #include + + +// http://en.cppreference.com/w/cpp/experimental/is_detected +namespace nlohmann +{ +namespace detail +{ +struct nonesuch +{ + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + void operator=(nonesuch const&) = delete; +}; + +template class Op, + class... Args> +struct detector +{ + using value_t = std::false_type; + using type = Default; +}; + +template class Op, class... Args> +struct detector>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op; +}; + +template