diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1add0a7..b19c196 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install libqglviewer-dev-qt5 libboost-dev libeigen3-dev ninja-build libhdf5-serial-dev libboost-dev libcairo2-dev libgmp-dev libgraphicsmagick++1-dev libfftw3-dev + sudo apt-get install libqglviewer-dev-qt5 libboost-dev libeigen3-dev ninja-build libhdf5-serial-dev libboost-dev libcairo2-dev libgmp-dev libgraphicsmagick++1-dev libfftw3-dev xorg-dev libglu1-mesa-dev freeglut3-dev mesa-common-dev - name: Install macOS deps if: matrix.os == 'macOS-latest' diff --git a/CMakeLists.txt b/CMakeLists.txt index 7081bd4..0d0878d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,11 @@ PROJECT(DGtalTools-contrib) cmake_minimum_required (VERSION 3.11) cmake_policy(SET CMP0057 NEW) +list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) + SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +include(polyscope) + # ----------------------------------------------------------------------------- @@ -17,7 +21,6 @@ SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) set (CMAKE_CXX_STANDARD_REQUIRED TRUE) - FIND_PACKAGE(DGtal 1.3 REQUIRED) INCLUDE_DIRECTORIES(${DGTAL_INCLUDE_DIRS}) LINK_DIRECTORIES(${DGTAL_LIBRARY_DIRS}) @@ -43,8 +46,10 @@ if(WITH_OPENCV) endif() + # CLI11 include_directories( "${PROJECT_SOURCE_DIR}/ext/" ) +include_directories(${PROJECT_SOURCE_DIR}) diff --git a/ChangeLog.md b/ChangeLog.md index 24e30a0..7fe2616 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -15,12 +15,16 @@ - *Geometry3d* - basicMorphoFilter: fix a bug on the dilate/erode options. (Bertrand Kerautret [#74](https://github.com/DGtal-team/DGtalTools-contrib/pull/74)) + - graph2vol: new tool to transform graph based object into volumetric object. (Bertrand Kerautret [#76](https://github.com/DGtal-team/DGtalTools-contrib/pull/76)) - + - *visualisation* + - polyMeshEdit: tool to edit a mesh (add local noise, remove selected faces). + (Bertrand Kerautret [#78](https://github.com/DGtal-team/DGtalTools-contrib/pull/78)) + - # DGtalTools-contrib 1.3 +# DGtalTools-contrib 1.3 - *global* - Continuous integration does not use Travis anymore but Github Actions. diff --git a/README.md b/README.md index daaaa17..25bf72b 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,23 @@ This section, can contain all tools related to visualisation: - displayTgtCoverAlphaTS: to display alpha-thick segment given on a simple contour. - meshViewerEdit: tool to visualize a mesh and to apply simple edits (face removal, color edits...). - graphViewer: tool to display graphs from a list of edges, a list of vertex and an optionnal list of radius for each edge. + - polyMeshEdit: tool to edit a mesh (add local noise, remove selected faces). - -|![](https://cloud.githubusercontent.com/assets/772865/12538777/cd8c2d28-c2e2-11e5-93ab-cb4a6cfadc8e.png)| ![](https://cloud.githubusercontent.com/assets/772865/12523276/22205f46-c156-11e5-827d-ec788baf7029.png) |capture d ecran 2016-03-04 a 19 46 54| -| :-: | :-: | :-: | -|displayTgtCoverAlphaTS|meshViewerEdit|graphViewer| + + + + + + + + + + + + + + + + + +
displayTgtCoverAlphaTSmeshViewerEditgraphViewer
polyMeshEdit
diff --git a/cmake/polyscope.cmake b/cmake/polyscope.cmake new file mode 100644 index 0000000..2f9e4a4 --- /dev/null +++ b/cmake/polyscope.cmake @@ -0,0 +1,14 @@ +if (TARGET polyscope) + return() +endif() + +include(FetchContent) + +message(STATUS "Fetching polyscope") + +FetchContent_Declare( + polyscope + GIT_REPOSITORY https://github.com/nmwsharp/polyscope.git + GIT_SHALLOW TRUE + ) +FetchContent_MakeAvailable(polyscope) \ No newline at end of file diff --git a/polyscope.cmake b/polyscope.cmake new file mode 100644 index 0000000..2f9e4a4 --- /dev/null +++ b/polyscope.cmake @@ -0,0 +1,14 @@ +if (TARGET polyscope) + return() +endif() + +include(FetchContent) + +message(STATUS "Fetching polyscope") + +FetchContent_Declare( + polyscope + GIT_REPOSITORY https://github.com/nmwsharp/polyscope.git + GIT_SHALLOW TRUE + ) +FetchContent_MakeAvailable(polyscope) \ No newline at end of file diff --git a/visualisation/CMakeLists.txt b/visualisation/CMakeLists.txt index b710ff1..c04b7f4 100644 --- a/visualisation/CMakeLists.txt +++ b/visualisation/CMakeLists.txt @@ -4,6 +4,12 @@ SET(DGTAL_TOOLS_CONTRIB displaySet2dPts ) +SET(DGTAL_TOOLS_CONTRIB_POLY + polyMeshEdit +) + + + FOREACH(FILE ${DGTAL_TOOLS_CONTRIB}) @@ -16,6 +22,16 @@ FOREACH(FILE ${DGTAL_TOOLS_CONTRIB}) ENDFOREACH(FILE) +FOREACH(FILE ${DGTAL_TOOLS_CONTRIB_POLY}) + add_executable(${FILE} ${FILE}) + target_link_libraries (${FILE} polyscope ${DGTAL_LIBRARIES} ${DGtalToolsContribLibDependencies}) + install(TARGETS ${FILE} + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib) +ENDFOREACH(FILE) + + if ( WITH_VISU3D_QGLVIEWER ) diff --git a/visualisation/polyMeshEdit.cpp b/visualisation/polyMeshEdit.cpp new file mode 100644 index 0000000..df71b3c --- /dev/null +++ b/visualisation/polyMeshEdit.cpp @@ -0,0 +1,417 @@ +/** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + **/ + +/** + * @file + * @ingroup visualisation + * @author Bertrand Kerautret (\c bertrand.kerautret@univ-lyon2.fr ) + * + * + * @date 2023/11/17 + * + * Source file of the tool polyMeshEdit + * + * This file is part of the DGtal library/DGtalTools-contrib Project. + */ + +/////////////////////////////////////////////////////////////////////////////// +#define NO_ADD_STBIMAGE_IMPLEMENT //To avoid duplicated linking errors (like LNK2005 in MSVC) +#include +#include +#include +#include +#include +#include "DGtal/io/readers/MeshReader.h" +#include "DGtal/io/writers/MeshWriter.h" + +#include +#include +#include +#include + +#include "CLI11.hpp" + +/////////////////////////////////////////////////////////////////////////////// +using namespace std; +using namespace DGtal; +/////////////////////////////////////////////////////////////////////////////// + + +/** + @page polyMeshEdit polyMeshEdit + + @brief polyMeshEdit tools to edit a mesh (add local noise and remove selected faces). Note that the process relies on the halfedge data structure that can fail if the input is not topologically consistant. If you want use other type of mesh, you can use meshViewerEdit that is based on the simple soup of triangles process (slower selection process). + + @b Usage: polyMeshEdit [OPTIONS] 1 [2] + + + @b Allowed @b options @b are : + + @code + + Positionals: + 1 TEXT:FILE REQUIRED an input mesh file in .obj or .off format. + 2 TEXT:FILE=result.obj an output mesh file in .obj or .off format. + + + Options: + -h,--help Print this help message and exit + -i,--input TEXT:FILE REQUIRED an input mesh file in .obj or .off format. + -o,--output TEXT:FILE=result.obj an output mesh file in .obj or .off format. + @endcode + + @b Example: + + @code + polyMeshEdit $DGtal/examples/samples/bunnyhead.obj bunnyEdited.obj + @endcode + + @image html respolyMeshEdit.png "Example of result. " + + @see + @ref polyMeshEdit.cpp + + */ + + +typedef PolygonalSurface PolySurface; + +static PolySurface currentPolysurf; +static PolySurface firstPolysurf; + +static std::vector vectSelection; +static float minPaintRad = 1.0; +static float maxPaintRad = 100.0; +static float minNoiseLevel = 1.0; +static float maxNoiseLevel = 100.0; + + +static float paintRad = 1.0; +static float noiseLevel = 1.0; +static int partialF = 1; +static int randLarge = 100000; +static const int unselectFlag = 200; +static const int selectFlag = 50; +static const int cursorFlag = 1; + + +static std::string outputFileName {"result.obj"}; + +void updateSelection(){ + polyscope::removeStructure("selection"); + auto digsurf = polyscope::getSurfaceMesh("InputMesh"); + digsurf->addFaceScalarQuantity("selection", vectSelection) + ->setMapRange(std::pair{cursorFlag,unselectFlag}); + digsurf->setAllQuantitiesEnabled(true); +} + + +void addSurfaceInPolyscope(PolySurface &psurf){ + polyscope::removeStructure("InputMesh"); + std::vector> faces; + vectSelection.clear(); + for(auto &face: psurf.allFaces()){ + faces.push_back(psurf.verticesAroundFace( face )); + vectSelection.push_back(unselectFlag); + } + auto digsurf = polyscope::registerSurfaceMesh("InputMesh", + psurf.positions(), faces); + updateSelection(); +} + +static Z3i::RealPoint +getFaceBarycenter(const PolySurface &polysurff, const PolySurface::Face &aFace) { + Z3i::RealPoint res(0.0, 0.0, 0.0); + for(auto const &v: polysurff.verticesAroundFace(aFace)){ + res += polysurff.position(v); + } + return res/polysurff.verticesAroundFace(aFace).size(); +} + +// Helper function +std::vector faceAround(const PolySurface &polysurff, + PolySurface::Face faceId, + double radius){ + std::vector result; + std::queue q; + std::map fVisited; + std::map vVisited; + + for (auto const &v : polysurff.verticesAroundFace(faceId) ) + { + q.push(v); + } + fVisited[faceId] = true; + bool addNewFaces = true; + while (!q.empty() ) + { + PolySurface::Vertex v = q.front(); q.pop(); + auto listFace = polysurff.facesAroundVertex(v); + for (auto const & f : listFace) + { + if (fVisited.count(f)==0) + { + if((getFaceBarycenter(polysurff, f) - + getFaceBarycenter(polysurff, faceId)).norm() < radius) + { + fVisited[f]=true; + result.push_back(f); + for (auto const & v : polysurff.verticesAroundFace(f)) + { + if (vVisited.count(v)==0) + { + vVisited[v]=true; + q.push(v); + } + } + } + } + } + } + + return result; +} +/** + * Select faces from selection with a probability of 1/selFreq + */ +void partialSelect(int selFreq=1){ + srand((unsigned) time(NULL)); + for ( unsigned int i = 0; i< currentPolysurf.nbFaces(); i++ ) + { + if (vectSelection[i]==selectFlag) + { + if (rand()%selFreq==0) + { + vectSelection[i]=unselectFlag; + }else + { + vectSelection[i]=selectFlag; + } + }else{ + vectSelection[i]=unselectFlag; + } + } +} + +void noisify(double scale = 0.01){ + srand((unsigned) time(NULL)); + for ( unsigned int i = 0; i< currentPolysurf.nbFaces(); i++ ) + { + Z3i::RealPoint pDep((((double)(rand()%randLarge)-randLarge/2.0)/randLarge)*scale, + (((double)(rand()%randLarge)-randLarge/2.0)/randLarge)*scale, + (((double)(rand()%randLarge)-randLarge/2.0)/randLarge)*scale); + if (vectSelection[i]==selectFlag) + { + for (auto f: currentPolysurf.verticesAroundFace(i)) + { + currentPolysurf.positions()[f][0] += pDep[0]; + currentPolysurf.positions()[f][1] += pDep[1]; + currentPolysurf.positions()[f][2] += pDep[2]; + } + } + } + addSurfaceInPolyscope(currentPolysurf); +} + + + +void deleteSelectedFaces(){ + PolySurface newSur; + std::vector vertexUsed (currentPolysurf.nbVertices(), false); + for ( unsigned int i = 0; i< currentPolysurf.nbFaces(); i++ ){ + if (vectSelection[i]==unselectFlag){ + auto face = currentPolysurf.verticesAroundFace( i ); + for (auto v: face){ + vertexUsed[v] = true; + } + } + } + auto lp = currentPolysurf.positions(); + for(unsigned int i = 0; i < currentPolysurf.nbVertices(); i++){ + if(vertexUsed[i]){ + newSur.addVertex(lp[i]); + } + } + std::vector translateIndexId; + int currentIndex = 0; + for(unsigned int i = 0; i < currentPolysurf.nbVertices(); i++ ){ + if (vertexUsed[i]){ + translateIndexId.push_back(currentIndex); + currentIndex++; + }else{ + translateIndexId.push_back(-1); + } + } + for ( unsigned int f = 0; f< currentPolysurf.nbFaces(); f++ ){ + if (vectSelection[f]==unselectFlag){ + auto face = currentPolysurf.verticesAroundFace( f ); + for (unsigned int i = 0; i= currentPolysurf.nbVertices()) + { + nb = (unsigned long) polyscope::pick::getSelection().second - currentPolysurf.nbVertices(); + } + else + { + // vertex selected (selecting a face connected to it) + if(currentPolysurf.facesAroundVertex(polyscope::pick::getSelection().second) + .size()> 0) + { + nb = currentPolysurf.facesAroundVertex(polyscope::pick::getSelection().second)[0]; + } + } + + if (nb > 0 && nb < vectSelection.size()){ + auto fVois = faceAround(currentPolysurf, nb, paintRad); + vectSelection[nb] = cursorFlag; + srand((unsigned) time(NULL)); + for (auto f: fVois) + { + if (rand()%partialF==0) + { + vectSelection[f]=selectFlag; + } + else + { + vectSelection[f]=unselectFlag; + } + } + } + } + ImGui::End(); + updateSelection(); +} + + + + + +int main(int argc, char** argv) +{ + std::string inputFileName {""}; + + // parse command line using CLI ---------------------------------------------- + CLI::App app; + app.description("polyMeshEdit tool to edit a mesh (add local noise and remove selected faces). Note that the process relies on the halfedge data structure that can fail if the input is not topologically consistant. If you want use other type of mesh, you can use meshViewerEdit that is based on the simple soup of triangles process (slower selection process). \n" + " polyMeshEdit $DGtal/examples/samples/bunnyhead.obj bunnyEdited.obj \n"); + app.add_option("-i,--input,1", inputFileName, "an input mesh file in .obj or .off format." ) + ->required() + ->check(CLI::ExistingFile); + app.add_option("-o,--output,2", outputFileName, "an output mesh file in .obj or .off format.", true ); + + + app.get_formatter()->column_width(40); + CLI11_PARSE(app, argc, argv); + polyscope::options::programName = "PolyMeshEdit - (DGtalToolsContrib)"; + polyscope::init(); + polyscope::options::buildGui=false; + // read input mesh + DGtal::Mesh aMesh(true); + aMesh << inputFileName; + aMesh.removeIsolatedVertices(); + auto bb = aMesh.getBoundingBox(); + // Setting scale mesh dependant parameters + minPaintRad = (bb.second - bb.first).norm()/1000.0; + maxPaintRad = (bb.second - bb.first).norm()/2.0; + minNoiseLevel = (bb.second - bb.first).norm()/10000.0; + maxNoiseLevel = (bb.second - bb.first).norm()/100.0; + noiseLevel = (bb.second - bb.first).norm()/1000.0; + paintRad = (bb.second - bb.first).norm()/50.0; + + DGtal::MeshHelpers::mesh2PolygonalSurface(aMesh, currentPolysurf); + polyscope::state::userCallback = callbackFaceID; + addSurfaceInPolyscope(currentPolysurf); + firstPolysurf = currentPolysurf; + polyscope::show(); + return 0; + +}