diff --git a/CMakeLists.txt b/CMakeLists.txt index cb3c57d..19f7b04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,13 +51,18 @@ endif() # Create an executable add_executable( poncaplot + src/dataManager.h + src/dataManager.cpp src/myview.h src/myview.cpp + src/application.h + src/application.cpp + src/poncaTypes.h src/drawingPass.h src/drawingPasses/distanceField.h src/drawingPasses/poncaFitField.h src/main.cpp - ) +) # Include settings target_include_directories(poncaplot PUBLIC diff --git a/dataset/4points.dat b/dataset/4points.dat new file mode 100644 index 0000000..e36ec29 --- /dev/null +++ b/dataset/4points.dat @@ -0,0 +1,5 @@ +# x y nx ny +215.87 324.15 -4.37114e-08 1 +95.1759 262.079 -0.996128 -0.0879132 +212.422 150.35 0.0445487 -0.999007 +337.943 244.147 0.999632 -0.0271429 diff --git a/dataset/bunny.dat b/dataset/bunny.dat new file mode 100644 index 0000000..2608478 --- /dev/null +++ b/dataset/bunny.dat @@ -0,0 +1,129 @@ +# x y nx ny +228.969 18.6691 -0.142503 -0.989794 +214.614 20.8804 -0.271324 -0.962488 +201.227 26.4899 -0.270028 -0.962853 +187.027 28.6103 -0.024168 -0.999708 +172.552 27.1909 0.119336 -0.992854 +158.155 25.1401 0.0932225 -0.995645 +143.624 24.4823 -0.0251171 -0.999685 +129.198 25.8689 -0.285292 -0.958441 +116.491 32.5679 -0.917162 -0.398515 +114.078 46.0523 -0.986312 -0.164892 +113.047 58.3086 -0.933771 -0.357872 +107.169 70.0382 -0.998194 0.0600762 +114.214 82.7168 -0.779438 0.626479 +124.976 92.1766 -0.533188 0.845997 +138.334 97.9326 -0.602708 0.797962 +146.754 108.722 -0.99998 0.00636636 +136.929 115.636 -0.410366 -0.911921 +122.925 119.605 -0.320878 -0.947121 +109.408 124.96 -0.482556 -0.875865 +97.7304 133.49 -0.619289 -0.785163 +86.6565 142.904 -0.69817 -0.715932 +76.9588 153.746 -0.856326 -0.516435 +67.5209 164.833 -0.824662 -0.565626 + 60.66 177.532 -0.827018 -0.562176 + 54.496 190.655 -0.962354 -0.271799 +49.2984 204.147 -0.982173 0.187981 +49.1295 218.672 -0.99896 0.0456026 +50.6214 233.132 -0.97224 0.233986 +55.1744 246.216 -0.976651 -0.214833 +44.1788 252.743 -0.525772 -0.850626 +32.4983 261.072 -0.75969 -0.650286 +24.9319 273.362 -0.952743 0.303779 + 23.986 287.768 -0.995882 0.0906624 +27.5213 301.856 -0.968207 0.25015 +31.2583 315.916 -0.977588 0.210529 + 33.64 330.268 -0.986761 0.162181 +35.9799 344.644 -0.999184 0.0403894 +34.8005 359.07 -0.993444 0.114323 +39.2236 372.802 -0.83245 0.5541 +50.1519 382.122 -0.505101 0.86306 +63.7849 387.189 -0.294318 0.955707 +77.9122 390.671 -0.251752 0.967792 +91.3056 394.351 -0.780164 0.625575 +92.7628 408.839 -0.994975 0.100121 +94.2209 423.323 -0.986009 0.166691 +97.5992 437.442 -0.935884 0.352308 +104.321 450.132 -0.704323 0.709879 +117.155 456.866 -0.210798 0.97753 +131.551 455.997 0.355984 0.934492 +144.143 448.292 0.844111 0.536168 +146.473 433.199 0.996914 -0.0785065 + 144.98 418.774 0.90178 0.432195 +139.837 405.185 0.516433 0.856328 +146.181 405.872 -0.234265 0.972173 +157.091 415.51 -0.29162 0.956534 +167.883 425.279 -0.652211 0.758038 +179.147 434.487 -0.608992 0.793177 +190.953 442.992 -0.560781 0.827964 +203.241 450.806 -0.535292 0.844667 +215.504 458.55 -0.548279 0.836296 +227.544 466.739 -0.545749 0.837949 +239.897 474.438 -0.41457 0.910017 +253.418 478.504 0.196699 0.980464 + 264.74 469.527 0.764407 0.644734 +271.739 456.915 0.822954 0.568108 +274.838 442.769 0.995683 -0.0928154 +274.414 428.243 0.969379 -0.245568 +267.912 415.501 0.783857 -0.620942 +256.868 406.07 0.559931 -0.828539 +243.988 399.333 0.437505 -0.899216 +230.716 393.346 0.40087 -0.916135 +217.332 387.669 0.431133 -0.902288 +204.507 380.823 0.469868 -0.882737 +191.649 373.998 0.491561 -0.870843 +179.188 366.531 0.530932 -0.847414 +167.035 358.577 0.824017 -0.566565 + 164.58 345.056 0.995925 -0.0901891 +169.426 331.373 0.998315 0.0580234 +178.374 319.982 0.574449 0.81854 +192.019 315.535 0.288952 0.957344 +206.017 311.637 0.273907 0.961756 +219.951 307.58 0.09506 0.995472 + 234.37 308.931 -0.169144 0.985591 +248.477 312.474 -0.24806 0.968745 +262.565 316.151 -0.243243 0.969965 +276.718 319.556 -0.204287 0.978911 +291.036 322.092 -0.121988 0.992532 +305.553 323.101 -0.0368414 0.999321 +320.104 323.164 0.0386137 0.999254 +334.569 321.979 0.214754 0.976668 +348.227 316.981 0.393408 0.919364 +361.282 310.548 0.47724 0.878773 +373.783 303.103 0.567348 0.823478 +385.183 294.081 0.659128 0.75203 +395.877 284.838 0.725596 0.688121 +404.893 272.733 0.742014 0.670385 + 413.06 260.701 0.832799 0.553575 +420.705 248.285 0.958039 0.286638 +425.819 234.605 0.852343 0.522983 + 431.05 220.008 0.978726 0.205173 +432.721 206.391 0.988524 -0.151062 +434.553 191.957 0.9943 -0.106623 +435.824 177.454 0.998241 -0.0592836 +436.265 163.099 0.68304 0.730381 +450.814 163.261 0.170737 0.985317 +464.132 158.249 0.605563 0.795797 +473.121 146.902 0.939887 0.341486 +474.213 132.334 0.999997 -0.0025838 +472.654 117.864 0.979425 -0.201807 +468.376 104.012 0.895933 -0.44419 +459.942 92.2238 0.748117 -0.663567 +449.223 82.4162 0.619113 -0.785302 +437.176 74.275 0.516586 -0.856235 +424.354 67.4126 0.427279 -0.90412 +410.904 61.8593 0.346664 -0.937989 +397.089 57.3358 0.299092 -0.954224 +383.158 53.1626 0.505331 -0.862926 +372.752 43.0794 0.65725 -0.753672 +361.382 34.173 0.458552 -0.888668 +347.475 30.0311 0.221991 -0.975049 +333.107 27.7349 0.133278 -0.991079 + 318.63 26.1521 0.101192 -0.994867 +304.152 24.7898 0.096887 -0.995295 +289.656 23.3317 0.103085 -0.994673 +275.184 21.7876 0.107156 -0.994242 +260.722 20.2132 0.105711 -0.994397 +246.242 18.7108 0.0588211 -0.998269 +231.711 18.5042 0.00632855 -0.99998 diff --git a/src/application.cpp b/src/application.cpp new file mode 100644 index 0000000..b9a4c7a --- /dev/null +++ b/src/application.cpp @@ -0,0 +1,284 @@ +#include "application.h" + +#include "myview.h" +#include "dataManager.h" +#include "drawingPass.h" +#include "drawingPasses/distanceField.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include // GLFW_KEY_ESCAPE and others + +using namespace nanogui; + +#define CONFIG_PONCA_FIT_INTERFACE(FitWidget,fit,name) \ + { \ + FitWidget = new nanogui::Widget(window); \ + FitWidget->set_layout(new GroupLayout()); \ + } + +const int tex_width = 500; +const int tex_height = 500; + +PoncaPlotApplication::PoncaPlotApplication() : +Screen(Vector2i(1024, 768), "PoncaPlot"), m_dataMgr(new DataManager()){ + + m_dataMgr->setKdTreePostUpdateFunction([this]() { this->renderPasses(); }); + + passDFWithKdTree = new DistanceFieldWithKdTree(); + passPlaneFit = new PlaneFitField(); + passSphereFit = new SphereFitField(); + passOrientedSphereFit = new OrientedSphereFitField(); + passUnorientedSphereFit = new UnorientedSphereFitField(); + + m_passes[0] = new FillPass( {255,255,255,255}); + m_passes[1] = passDFWithKdTree; + m_passes[2] = new DisplayPoint({255,0,0,255}); + + inc_ref(); + auto *window = new Window(this, "Utils"); + window->set_position(Vector2i(0, 0)); + window->set_layout(new GroupLayout()); + + // IO + { + auto *tools = new Widget(window); + tools->set_layout(new BoxLayout(Orientation::Vertical, + Alignment::Middle, 0, 6)); + auto *b = new Button(tools, "Open point cloud"); + b->set_callback([&] { + auto path = file_dialog( + {{"dat", "Text file x y nx y"}, + {"txt", "Text file x y nx y"}}, false); + std::cout << "Load file from: " << path << std::endl; + m_dataMgr->loadPointCloud(path); + }); + b = new Button(tools, "Save point cloud"); + b->set_callback([&] { + auto path = file_dialog( + {{"dat", "Text file x y nx y"}, + {"txt", "Text file x y nx y"}}, true); + std::cout << "Save file to: " << path << std::endl; + m_dataMgr->savePointCloud(path); + }); + b = new Button(tools, "Fit point cloud view"); + b->set_callback([&] { + const int bordersize = 20; + m_dataMgr->fitPointCloudToRange({tex_width-bordersize, tex_height-bordersize},{bordersize,bordersize}); + }); + } + + window = new Window(this, "Fitting Controls"); + window->set_position(Vector2i(0, 200)); + window->set_layout(new GroupLayout()); + + + new nanogui::Label(window, "Select Fit Type", "sans-bold"); + auto combo =new nanogui::ComboBox(window, + { "Distance Field", + "Plane", + "Sphere", + "Oriented Sphere", + "Unoriented Sphere"}); + combo->set_callback([this](int id){ + switch (id) { + case 0: m_passes[1] = passDFWithKdTree; break; + case 1: m_passes[1] = passPlaneFit; break; + case 2: m_passes[1] = passSphereFit; break; + case 3: m_passes[1] = passOrientedSphereFit; break; + case 4: m_passes[1] = passUnorientedSphereFit; break; + default: throw std::runtime_error("Unknown Field type!"); + } + buildPassInterface(id); + renderPasses(); + }); + + + // create pass 1 interface + { + pass1Widget = new nanogui::Widget(window); + pass1Widget->set_layout(new GroupLayout()); + new nanogui::Label(pass1Widget, "Background filling", "sans-bold"); + new nanogui::Label(pass1Widget, "Color"); + // dunno why, but sets colorpicker in range [0-255], but reads in [0-1] + auto cp = new ColorPicker(pass1Widget, (dynamic_cast(m_passes[0]))->m_fillColor); + cp->set_final_callback([this](const Color &c) { + dynamic_cast(m_passes[0])->m_fillColor = 255.f * c; + renderPasses(); + }); + } + + // create configurable passes interface + { + distanceFieldWidget = new nanogui::Widget(window); + distanceFieldWidget->set_layout(new GroupLayout()); + new nanogui::Label(distanceFieldWidget, "Distance Field", "sans-bold"); + new nanogui::Label(distanceFieldWidget, "no parameter available"); + } + + { + genericFitWidget = new nanogui::Widget(window); + genericFitWidget->set_layout(new GroupLayout()); + new nanogui::Label(genericFitWidget, "Local Fitting", "sans-bold"); + new nanogui::Label(genericFitWidget, "Scale"); + auto slider = new Slider(genericFitWidget); + slider->set_value(passPlaneFit->m_scale); // init with plane, but sync with current. + slider->set_range({5,200}); + slider->set_callback([&](float value) { + passPlaneFit->m_scale = value; + passSphereFit->m_scale = value; + passOrientedSphereFit->m_scale = value; + passUnorientedSphereFit->m_scale = value; + renderPasses(); + }); + + new Label(genericFitWidget, "MLS Iterations :", "sans-bold"); + auto int_box = new IntBox(genericFitWidget, passPlaneFit->m_iter); + int_box->set_editable(true); + int_box->set_spinnable(true); + int_box->set_min_value(1); + int_box->set_max_value(10); + int_box->set_value_increment(1); + int_box->set_callback([&](int value) { + passPlaneFit->m_iter = value; + passSphereFit->m_iter = value; + passOrientedSphereFit->m_iter = value; + passUnorientedSphereFit->m_iter = value; + renderPasses(); + }); + } + + CONFIG_PONCA_FIT_INTERFACE(planeFitWidget,passPlaneFit,combo->items()[1]) + CONFIG_PONCA_FIT_INTERFACE(sphereFitWidget,passSphereFit,combo->items()[2]) + CONFIG_PONCA_FIT_INTERFACE(orientedSphereFitWidget,passOrientedSphereFit,combo->items()[3]) + CONFIG_PONCA_FIT_INTERFACE(unorientedSphereFitWidget,passUnorientedSphereFit,combo->items()[4]) + + // create pass 3 interface + { + pass3Widget = new nanogui::Widget(window); + pass3Widget->set_layout(new GroupLayout()); + new nanogui::Label(pass3Widget, "Points Display", "sans-bold"); + new nanogui::Label(pass3Widget, "Color"); + // dunno why, but sets colorpicker in range [0-255], but reads in [0-1] + auto cp = new ColorPicker(pass3Widget, (dynamic_cast(m_passes[2]))->m_pointColor); + cp->set_final_callback([this](const Color &c) { + dynamic_cast(m_passes[2])->m_pointColor = 255.f * c; + renderPasses(); + }); + auto slider = new Slider(pass3Widget); + slider->set_value(dynamic_cast(m_passes[2])->m_halfSize); + slider->set_range({1,20}); + slider->set_callback([&](float value) { + dynamic_cast(m_passes[2])->m_halfSize = int(value); + m_image_view->setSelectionThreshold(value); + renderPasses(); + }); + } + + window = new Window(this, "Image"); + window->set_position(Vector2i(200, 0)); + window->set_size(Vector2i(768,768)); + window->set_layout(new GroupLayout(3)); + + m_textureBufferPing = new uint8_t [tex_width * tex_height * 4]; // use UInt8 RGBA textures + m_textureBufferPong = new uint8_t [tex_width * tex_height * 4]; // use UInt8 RGBA textures + m_texture = new Texture( + Texture::PixelFormat::RGBA, + Texture::ComponentFormat::UInt8, + {tex_width,tex_height}, + Texture::InterpolationMode::Trilinear, + Texture::InterpolationMode::Nearest, + Texture::WrapMode::ClampToEdge); + + m_image_view = new MyView(window, m_dataMgr); + m_image_view->set_size(Vector2i(768, 768)); + m_image_view->set_image(m_texture ); + m_image_view->fitImage(); + m_image_view->center(); + + renderPasses(); // render twice to fill m_textureBufferPing and m_textureBufferPong + renderPasses(); + + // call perform_layout + buildPassInterface(0); +} + + +bool +PoncaPlotApplication::keyboard_event(int key, int scancode, int action, int modifiers) { + if (Screen::keyboard_event(key, scancode, action, modifiers)) + return true; + if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) { + set_visible(false); + return true; + } + return false; +} + +void +PoncaPlotApplication::draw(NVGcontext *ctx) { +// if (m_needUpdate) + Screen::draw(ctx); +} + +void +PoncaPlotApplication::draw_contents() { + if (m_needUpdate){ + m_texture->upload(m_computeInPing ? m_textureBufferPong : m_textureBufferPing); + m_needUpdate = false; + } + Screen::draw_contents(); +} + +void +PoncaPlotApplication::buildPassInterface(int id){ + distanceFieldWidget->set_visible(false); + genericFitWidget->set_visible(false); + planeFitWidget->set_visible(false); + sphereFitWidget->set_visible(false); + orientedSphereFitWidget->set_visible(false); + unorientedSphereFitWidget->set_visible(false); + switch (id) { + case 0: + distanceFieldWidget->set_visible(true); + break; + case 1: + genericFitWidget->set_visible(true); + planeFitWidget->set_visible(true); + break; + case 2: + genericFitWidget->set_visible(true); + sphereFitWidget->set_visible(true); + break; + case 3: + genericFitWidget->set_visible(true); + orientedSphereFitWidget->set_visible(true); + break; + case 4: + genericFitWidget->set_visible(true); + unorientedSphereFitWidget->set_visible(true); + break; + default: throw std::runtime_error("Unknown Field type!"); + } + perform_layout(); +} + +void +PoncaPlotApplication::renderPasses() { + std::cout << "[Main] Update texture" << std::endl; + const auto& points = m_dataMgr->getKdTree(); + for (auto* p : m_passes) { + p->render(points, m_computeInPing ? m_textureBufferPing : m_textureBufferPong, tex_width, tex_height); + } + + m_computeInPing = ! m_computeInPing; + m_needUpdate = true; +} + diff --git a/src/application.h b/src/application.h new file mode 100644 index 0000000..7c4b0d5 --- /dev/null +++ b/src/application.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +// cannot forward declare aliases +#include "drawingPasses/poncaFitField.h" + +// forward declarations +class DrawingPass; +class DistanceFieldWithKdTree; +class MyView; +class DataManager; + +namespace nanogui{ + class Texture; +} + + + +class PoncaPlotApplication : public nanogui::Screen { + +public: + PoncaPlotApplication(); + + bool keyboard_event(int key, int scancode, int action, int modifiers) override; + + void draw(NVGcontext *ctx) override; + + void draw_contents() override; + +private: + void buildPassInterface(int id); + + void renderPasses(); +private: + uint8_t* m_textureBufferPing {nullptr}, * m_textureBufferPong {nullptr}; + bool m_computeInPing{true}; + nanogui::Texture* m_texture {nullptr}; + std::array m_passes{nullptr, nullptr, nullptr}; + bool m_needUpdate{false}; + + DataManager *m_dataMgr{nullptr}; + + MyView *m_image_view {nullptr}; + Widget* pass1Widget, *distanceFieldWidget, + *genericFitWidget, //< parameters applicable to all fitting techniques + *planeFitWidget, *sphereFitWidget, *orientedSphereFitWidget, *unorientedSphereFitWidget, + *pass3Widget; + DistanceFieldWithKdTree *passDFWithKdTree; + PlaneFitField *passPlaneFit; + SphereFitField *passSphereFit; + OrientedSphereFitField *passOrientedSphereFit; + UnorientedSphereFitField *passUnorientedSphereFit; +}; diff --git a/src/dataManager.cpp b/src/dataManager.cpp new file mode 100644 index 0000000..ad2a9f0 --- /dev/null +++ b/src/dataManager.cpp @@ -0,0 +1,108 @@ +#include "dataManager.h" + +#include +#include + +bool +DataManager::savePointCloud(const std::string& path) const{ + if( path.empty() ) return false; + + std::ofstream file; + file.open (path); + + if( ! file.is_open() ) return false; + + file << "# x y nx ny " << "\n"; + for( const auto & p : m_tree.point_data() ){ + file << p.pos().transpose() << " " << p.normal().transpose() << "\n"; + } + file.close(); + return true; +} +bool +DataManager::loadPointCloud(const std::string& path){ + if( path.empty() ) return false; + + std::ifstream file; + file.open (path); + + if( ! file.is_open() ) return false; + + m_points.clear(); + + std::string line; + std::vector numbers; + + bool needToComputeNormals = false; + + while ( getline (file,line) ) + { + // trim comments + std::size_t found = line.find('#'); + if (found!=std::string::npos) + line = line.substr(0,found); + + if ( ! line.empty() ) { + numbers.clear(); + std::istringstream is(line); + numbers.assign(std::istream_iterator(is), std::istream_iterator()); + + if (numbers.size() == 2){ // loaded x-y only, set normal to default value + m_points.emplace_back(numbers[0], numbers[1], DEFAULT_POINT_ANGLE); + needToComputeNormals = true; + } else if (numbers.size() == 4){ // loaded x-y only, set normal to default value + m_points.emplace_back(numbers[0], numbers[1], std::atan2(numbers[3], numbers[2])); + } else { // malformed line + std::cerr << "Skipping malformed line: [" << line << "]" << std::endl; + } + } + } + file.close(); + updateKdTree(); + + // Use plane fit to compute unoriented normals. Use knn and constant weights + if(needToComputeNormals) computeNormals(); + + return true; +} + +void +DataManager::computeNormals(int k){ + using WeightFunc = Ponca::DistWeightFunc >; + using PlaneFit = Ponca::Basket; + + std::cout << "Recompute normals" << std::endl; + + for (auto& pp : m_points){ + VectorType p {pp.x(),pp.y()}; + PlaneFit fit; + fit.setWeightFunc(WeightFunc()); + // Set the evaluation position + fit.init(p); + // Fit plane (method compute handles multipass fitting + if (fit.computeWithIds(m_tree.k_nearest_neighbors(p, k), m_tree.point_data()) == Ponca::STABLE) { + pp.z() = std::acos(fit.primitiveGradient().normalized().x()); + } else + std::cerr << "Something weird happened here..." << std::endl; + } + updateKdTree(); +} + +void +DataManager::fitPointCloudToRange(const std::pair& rangesEnd, const std::pair& rangesStart){ + if (m_points.empty()) return; + if (m_tree.node_count() == 0) updateKdTree(); + auto aabb = m_tree.node_data()[0].getAabb(); + if (aabb){ + VectorType requestedSize {rangesEnd.first - rangesStart.first, rangesEnd.second - rangesStart.second}; + VectorType scaleFactors = requestedSize.array() / aabb->diagonal().array(); + float scale = scaleFactors.minCoeff(); + for (auto& p : m_points) + { + p.x() *= scale; + p.y() *= scale; + } + updateKdTree(); + } +} + diff --git a/src/dataManager.h b/src/dataManager.h new file mode 100644 index 0000000..78221b9 --- /dev/null +++ b/src/dataManager.h @@ -0,0 +1,103 @@ +#pragma once + +#include +#include //pair +#include +#include +#include + +#include + +#include + +#include "poncaTypes.h" + + +#ifndef M_PI +// Source: http://www.geom.uiuc.edu/~huberty/math5337/groupe/digits.html +#define M_PI 3.141592653589793238462643383279502884197169399375105820974944592307816406 +#endif +#define DEFAULT_POINT_ANGLE M_PI / 2. + +template > +struct MyKdTreeInnerNode : public Ponca::KdTreeDefaultInnerNode { + using AabbType = _AabbType; + AabbType m_aabb{}; +}; +// +//template +//using MyKdTreeNode = Ponca::KdTreeCustomizableNode >; + +template +struct MyKdTreeNode : Ponca::KdTreeCustomizableNode> { + + using Base = Ponca::KdTreeCustomizableNode>; + using AabbType = typename Base::AabbType; + + void configure_range(Index start, Index size, const AabbType &aabb) + { + Base::configure_range(start, size, aabb); + if (! Base::is_leaf() ) + { + Base::getAsInner().m_aabb = aabb; + } + } + [[nodiscard]] inline std::optional getAabb() const { + if (! Base::is_leaf()) + return Base::getAsInner().m_aabb; + else + return std::optional(); + } +}; + +/// Structure holding shared data +struct DataManager { +public: +// using KdTree = Ponca::KdTree; + using KdTree = Ponca::KdTreeBase>; + using PointContainer = std::vector; // stores x,y,normal angle in radians + using VectorType = typename KdTree::VectorType; + + /// Read access to point collection + inline const KdTree& getKdTree() const { return m_tree; } + + /// Update point collection from point container + inline void updateKdTree() { + if(m_points.empty()) m_tree.clear(); + else m_tree.build(m_points ); + m_updateFunction(); + } + + /// Read access to point container + inline const PointContainer& getPointContainer() const { return m_points; } + + /// Read access to point container + /// \warning + inline PointContainer& getPointContainer() { return m_points; } + + /// Set Update function, called after each point update + inline void setKdTreePostUpdateFunction(std::function &&f) { m_updateFunction = f; } + + /// IO: save current point cloud to file + bool savePointCloud(const std::string& path) const; + + /// IO: load point cloud from file + bool loadPointCloud(const std::string& path); + + /// Utils: fit point cloud to coordinates ranges + void fitPointCloudToRange(const std::pair& rangesEnd, + const std::pair& rangesStart = {0,0}); + + /// Utils: compute normals using covariance plane fit + /// \param Number of neighbors (3 means current point and 2 closest points: left and right) + void computeNormals(int k = 3); + +private: + PointContainer m_points; + KdTree m_tree; + std::function m_updateFunction {[](){}}; +}; + diff --git a/src/drawingPass.h b/src/drawingPass.h index 984795a..b8e9d49 100644 --- a/src/drawingPass.h +++ b/src/drawingPass.h @@ -3,19 +3,19 @@ #include #include -#include "myview.h" +#include "dataManager.h" /// Base class to rendering processes struct DrawingPass { - virtual void render(const MyView::PointCollection& points, uint8_t*buffer, int w, int h) = 0; + virtual void render(const DataManager::KdTree& points, uint8_t*buffer, int w, int h) = 0; virtual ~DrawingPass() = default; }; struct FillPass : public DrawingPass { inline explicit FillPass(const nanogui::Vector4i &fillColor = {255,255,255,255}) : m_fillColor(fillColor) {} - void render(const MyView::PointCollection& /*points*/, uint8_t*buffer, int w, int h) override{ + void render(const DataManager::KdTree& /*points*/, uint8_t*buffer, int w, int h) override{ #pragma omp parallel for default(none) shared(buffer, w, h) for(auto j = 0; j +#include "../drawingPass.h" +#include "../poncaTypes.h" struct BaseFitField : public DrawingPass{ inline explicit BaseFitField() : DrawingPass() {} @@ -19,7 +19,7 @@ struct FitField : public BaseFitField { using WeightFunc = typename FitType::WeightFunction; - void render(const MyView::PointCollection& points, uint8_t*buffer, int w, int h) override{ + void render(const DataManager::KdTree& points, uint8_t*buffer, int w, int h) override{ if(points.point_data().empty()) return; const auto normFactor = float(std::max(w,h)); @@ -61,11 +61,7 @@ struct FitField : public BaseFitField { } }; -using PointType = typename MyView::PointCollection::DataPoint; -using Scalar = typename PointType::Scalar; -using WeightFunc = Ponca::DistWeightFunc >; - -using PlaneFitField = FitField>; -using SphereFitField = FitField>; -using OrientedSphereFitField = FitField>; -using UnorientedSphereFitField = FitField>; +using PlaneFitField = FitField; +using SphereFitField = FitField; +using OrientedSphereFitField = FitField; +using UnorientedSphereFitField = FitField; diff --git a/src/main.cpp b/src/main.cpp index 41345d4..547ce4c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,295 +11,16 @@ BSD-style license that can be found in the LICENSE.txt file. */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - #include -#include -#include - -#include "myview.h" -#include "drawingPass.h" -#include "drawingPasses/distanceField.h" -#include "drawingPasses/poncaFitField.h" - - -#define STB_IMAGE_STATIC -#define STB_IMAGE_IMPLEMENTATION -#if defined(_MSC_VER) -# pragma warning (disable: 4505) // don't warn about dead code in stb_image.h -#elif defined(__GNUC__) -# pragma GCC diagnostic ignored "-Wunused-function" -#endif -#include - -const int tex_width = 500; -const int tex_height = 500; - -using namespace nanogui; - -MyView *image_view {nullptr}; - - - -class ExampleApplication : public Screen { -private: - // We need to use a macro, to avoid the lambda to be defined in a local context -#define CONFIG_PONCA_FIT_INTERFACE(FitWidget,fit,name) \ - { \ - FitWidget = new nanogui::Widget(window); \ - FitWidget->set_layout(new GroupLayout()); \ - } - - - -public: - ExampleApplication() - : Screen(Vector2i(1024, 768), "PoncaPlot"){ - - passDFWithKdTree = new DistanceFieldWithKdTree(); - passPlaneFit = new PlaneFitField(); - passSphereFit = new SphereFitField(); - passOrientedSphereFit = new OrientedSphereFitField(); - passUnorientedSphereFit = new UnorientedSphereFitField(); - - m_passes[0] = new FillPass( {255,255,255,255}); - m_passes[1] = passDFWithKdTree; - m_passes[2] = new DisplayPoint({255,0,0,255}); - - inc_ref(); - auto *window = new Window(this, "Controls"); - window->set_position(Vector2i(0, 0)); - window->set_layout(new GroupLayout()); - - - new nanogui::Label(window, "Select Fit Type", "sans-bold"); - auto combo =new nanogui::ComboBox(window, - { "Distance Field", - "Plane", - "Sphere", - "Oriented Sphere", - "Unoriented Sphere"}); - combo->set_callback([this](int id){ - switch (id) { - case 0: m_passes[1] = passDFWithKdTree; break; - case 1: m_passes[1] = passPlaneFit; break; - case 2: m_passes[1] = passSphereFit; break; - case 3: m_passes[1] = passOrientedSphereFit; break; - case 4: m_passes[1] = passUnorientedSphereFit; break; - default: throw std::runtime_error("Unknown Field type!"); - } - buildPassInterface(id); - renderPasses(); - }); - - - // create pass 1 interface - { - pass1Widget = new nanogui::Widget(window); - pass1Widget->set_layout(new GroupLayout()); - new nanogui::Label(pass1Widget, "Background filling", "sans-bold"); - new nanogui::Label(pass1Widget, "Color"); - // dunno why, but sets colorpicker in range [0-255], but reads in [0-1] - auto cp = new ColorPicker(pass1Widget, (dynamic_cast(m_passes[0]))->m_fillColor); - cp->set_final_callback([this](const Color &c) { - dynamic_cast(m_passes[0])->m_fillColor = 255.f * c; - renderPasses(); - }); - } - - // create configurable passes interface - { - distanceFieldWidget = new nanogui::Widget(window); - distanceFieldWidget->set_layout(new GroupLayout()); - new nanogui::Label(distanceFieldWidget, "Distance Field", "sans-bold"); - new nanogui::Label(distanceFieldWidget, "no parameter available"); - } - - { - genericFitWidget = new nanogui::Widget(window); - genericFitWidget->set_layout(new GroupLayout()); - new nanogui::Label(genericFitWidget, "Local Fitting", "sans-bold"); - new nanogui::Label(genericFitWidget, "Scale"); - auto slider = new Slider(genericFitWidget); - slider->set_value(passPlaneFit->m_scale); // init with plane, but sync with current. - slider->set_range({5,200}); - slider->set_callback([&](float value) { - passPlaneFit->m_scale = value; - passSphereFit->m_scale = value; - passOrientedSphereFit->m_scale = value; - passUnorientedSphereFit->m_scale = value; - renderPasses(); - }); - new Label(genericFitWidget, "MLS Iterations :", "sans-bold"); - auto int_box = new IntBox(genericFitWidget, passPlaneFit->m_iter); - int_box->set_editable(true); - int_box->set_spinnable(true); - int_box->set_min_value(1); - int_box->set_max_value(10); - int_box->set_value_increment(1); - int_box->set_callback([&](int value) { - passPlaneFit->m_iter = value; - passSphereFit->m_iter = value; - passOrientedSphereFit->m_iter = value; - passUnorientedSphereFit->m_iter = value; - renderPasses(); - }); - } - - CONFIG_PONCA_FIT_INTERFACE(planeFitWidget,passPlaneFit,combo->items()[1]) - CONFIG_PONCA_FIT_INTERFACE(sphereFitWidget,passSphereFit,combo->items()[2]) - CONFIG_PONCA_FIT_INTERFACE(orientedSphereFitWidget,passOrientedSphereFit,combo->items()[3]) - CONFIG_PONCA_FIT_INTERFACE(unorientedSphereFitWidget,passUnorientedSphereFit,combo->items()[4]) - - // create pass 3 interface - { - pass3Widget = new nanogui::Widget(window); - pass3Widget->set_layout(new GroupLayout()); - new nanogui::Label(pass3Widget, "Points Display", "sans-bold"); - new nanogui::Label(pass3Widget, "Color"); - // dunno why, but sets colorpicker in range [0-255], but reads in [0-1] - auto cp = new ColorPicker(pass3Widget, (dynamic_cast(m_passes[2]))->m_pointColor); - cp->set_final_callback([this](const Color &c) { - dynamic_cast(m_passes[2])->m_pointColor = 255.f * c; - renderPasses(); - }); - auto slider = new Slider(pass3Widget); - slider->set_value(dynamic_cast(m_passes[2])->m_halfSize); - slider->set_range({1,20}); - slider->set_callback([&](float value) { - dynamic_cast(m_passes[2])->m_halfSize = int(value); - image_view->setSelectionThreshold(value); - renderPasses(); - }); - } - - window = new Window(this, "Image"); - window->set_position(Vector2i(200, 0)); - window->set_size(Vector2i(768,768)); - window->set_layout(new GroupLayout(3)); - - m_textureBufferPing = new uint8_t [tex_width * tex_height * 4]; // use UInt8 RGBA textures - m_textureBufferPong = new uint8_t [tex_width * tex_height * 4]; // use UInt8 RGBA textures - m_texture = new Texture( - Texture::PixelFormat::RGBA, - Texture::ComponentFormat::UInt8, - {tex_width,tex_height}, - Texture::InterpolationMode::Trilinear, - Texture::InterpolationMode::Nearest, - Texture::WrapMode::ClampToEdge); - - image_view = new MyView(window); - image_view->set_size(Vector2i(768,768)); - image_view->set_image( m_texture ); - image_view->fitImage(); - image_view->center(); - image_view->setUpdateFunction([this](){ this->renderPasses();}); - - renderPasses(); // render twice to fill m_textureBufferPing and m_textureBufferPong - renderPasses(); - - // call perform_layout - buildPassInterface(0); - } - - bool keyboard_event(int key, int scancode, int action, int modifiers) override { - if (Screen::keyboard_event(key, scancode, action, modifiers)) - return true; - if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) { - set_visible(false); - return true; - } - return false; - } - - void draw(NVGcontext *ctx) override { -// if (m_needUpdate) - Screen::draw(ctx); - } - - void draw_contents() override { - if (m_needUpdate){ - m_texture->upload(m_computeInPing ? m_textureBufferPong : m_textureBufferPing); - m_needUpdate = false; - } - Screen::draw_contents(); - } - - void buildPassInterface(int id){ - distanceFieldWidget->set_visible(false); - genericFitWidget->set_visible(false); - planeFitWidget->set_visible(false); - sphereFitWidget->set_visible(false); - orientedSphereFitWidget->set_visible(false); - unorientedSphereFitWidget->set_visible(false); - switch (id) { - case 0: - distanceFieldWidget->set_visible(true); - break; - case 1: - genericFitWidget->set_visible(true); - planeFitWidget->set_visible(true); - break; - case 2: - genericFitWidget->set_visible(true); - sphereFitWidget->set_visible(true); - break; - case 3: - genericFitWidget->set_visible(true); - orientedSphereFitWidget->set_visible(true); - break; - case 4: - genericFitWidget->set_visible(true); - unorientedSphereFitWidget->set_visible(true); - break; - default: throw std::runtime_error("Unknown Field type!"); - } - perform_layout(); - } - - void renderPasses() { - std::cout << "[Main] Update texture" << std::endl; - const auto& points = image_view->getPointCollection(); - for (auto* p : m_passes) { - p->render(points, m_computeInPing ? m_textureBufferPing : m_textureBufferPong, tex_width, tex_height); - } - - m_computeInPing = ! m_computeInPing; - m_needUpdate = true; - } -private: - uint8_t* m_textureBufferPing {nullptr}, * m_textureBufferPong {nullptr}; - bool m_computeInPing{true}; - Texture* m_texture {nullptr}; - std::array m_passes{nullptr, nullptr, nullptr}; - bool m_needUpdate{false}; - Widget* pass1Widget, *distanceFieldWidget, - *genericFitWidget, //< parameters applicable to all fitting techniques - *planeFitWidget, *sphereFitWidget, *orientedSphereFitWidget, *unorientedSphereFitWidget, - *pass3Widget; - DistanceFieldWithKdTree *passDFWithKdTree; - PlaneFitField *passPlaneFit; - SphereFitField *passSphereFit; - OrientedSphereFitField *passOrientedSphereFit; - UnorientedSphereFitField *passUnorientedSphereFit; -}; +#include "application.h" int main(int /* argc */, char ** /* argv */) { try { nanogui::init(); /* scoped variables */ { - ref app = new ExampleApplication(); + nanogui::ref app = new PoncaPlotApplication(); app->dec_ref(); app->draw_all(); app->set_visible(true); @@ -309,11 +30,7 @@ int main(int /* argc */, char ** /* argv */) { nanogui::shutdown(); } catch (const std::exception &e) { std::string error_msg = std::string("Caught a fatal error: ") + std::string(e.what()); - #if defined(_WIN32) - MessageBoxA(nullptr, error_msg.c_str(), NULL, MB_ICONERROR | MB_OK); - #else - std::cerr << error_msg << std::endl; - #endif + std::cerr << error_msg << std::endl; return -1; } catch (...) { std::cerr << "Caught an unknown error!" << std::endl; diff --git a/src/myview.cpp b/src/myview.cpp index 0bf3928..13e325c 100644 --- a/src/myview.cpp +++ b/src/myview.cpp @@ -2,19 +2,15 @@ #include -#ifndef M_PI -// Source: http://www.geom.uiuc.edu/~huberty/math5337/groupe/digits.html -#define M_PI 3.141592653589793238462643383279502884197169399375105820974944592307816406 -#endif using namespace nanogui; -MyView::MyView(nanogui::Widget *parent) : ImageView(parent) { +MyView::MyView(nanogui::Widget *parent, DataManager* mgr) : ImageView(parent), m_dataMgr(mgr) { std::cout<< "Controls:\n" - << " ctrl+mouse move: move view\n" << " scroll: zoom in/out\n" << " left click: move point\n" - << " right click: change point normal" + << " right click + move: rotate point normal" + << " ctrl+click: invert normal\n" << std::endl; } @@ -43,10 +39,11 @@ MyView::fitImage() { //#define USE_KDTREE int MyView::findPointId(const Vector2f &lp) const{ - if(! m_points.empty()) { + const auto& points = m_dataMgr->getPointContainer(); + if(! points.empty()) { int i = 0; #ifndef USE_KDTREE - for(const auto&p : m_points) { + for(const auto&p : points) { Vector2f query(p.x(), p.y()); if (norm((lp - query)) <= m_selectionThreshold) return i; @@ -54,9 +51,10 @@ MyView::findPointId(const Vector2f &lp) const{ } #else DataPoint::VectorType query(lp.x(), lp.y()); - auto res = m_tree.nearest_neighbor(query); + const auto& tree = m_dataMgr->getKdTree(); + auto res = tree.nearest_neighbor(query); if (res.begin() != res.end() && - (query-m_tree.point_data()[res.get()].pos()).norm()getPointContainer().emplace_back(lp.x(), lp.y(), DEFAULT_POINT_ANGLE); + m_dataMgr->updateKdTree(); + } + } else { + m_movedPoint = pointId; + } + } else if (modifiers == 2) { // Ctrl + if (pointId >= 0) { + std::cout << "Flip normal of point " << pointId << std::endl; + auto& angle = m_dataMgr->getPointContainer()[pointId].z(); + angle = float(std::fmod(angle + M_PI, 2.*M_PI)); + m_dataMgr->updateKdTree(); } - } else { - m_movedPoint = pointId; } return true; } @@ -97,13 +102,14 @@ MyView::mouse_drag_event(const nanogui::Vector2i &p, const nanogui::Vector2i &re if (isInsideImage(lp)) { if (m_movedPoint>=0) { + auto& points = m_dataMgr->getPointContainer(); switch (button) { case 1: //left click // if is on a point // std::cout << "Move point by [" << rel << "]" << std::endl; - m_points[m_movedPoint].x() = lp.x(); - m_points[m_movedPoint].y() = lp.y(); - updateCollection(); + points[m_movedPoint].x() = lp.x(); + points[m_movedPoint].y() = lp.y(); + m_dataMgr->updateKdTree(); break; case 2: //right click { @@ -111,9 +117,9 @@ MyView::mouse_drag_event(const nanogui::Vector2i &p, const nanogui::Vector2i &re if (rel.x() < 0) dist *=-1; // if is on a point auto relAngle = std::asin(float(dist) / 50.1f); // move by 40px to get 90 degree angle - m_points[m_movedPoint].z() += relAngle; + points[m_movedPoint].z() += relAngle; // std::cout << "Change normal by " << rel << ". Gives angle " << m_points[m_movedPoint].z() << std::endl; - updateCollection(); + m_dataMgr->updateKdTree(); } break; default: @@ -123,14 +129,3 @@ MyView::mouse_drag_event(const nanogui::Vector2i &p, const nanogui::Vector2i &re } return true; } - -void -MyView::updateCollection() { - // recompute KdTree - if(m_points.empty()) - m_tree.clear(); - else - m_tree.build(m_points ); - - m_updateFunction(); -} diff --git a/src/myview.h b/src/myview.h index 9472547..e089c75 100644 --- a/src/myview.h +++ b/src/myview.h @@ -3,35 +3,16 @@ #include #include -#include +#include "dataManager.h" #include #include #include -class DataPoint -{ -public: - enum {Dim = 2}; - using Scalar = float; - using VectorType = Eigen::Vector; - using MatrixType = Eigen::Matrix; - [[nodiscard]] inline const auto& pos() const {return m_pos;} - [[nodiscard]] inline const auto& normal() const {return m_normal;} - /// \fixme Use maps to avoid duplication - explicit inline DataPoint(const nanogui::Vector3f &pn) - : m_pos({pn.x(), pn.y()}), m_normal({std::cos(pn.z()), std::sin(pn.z())}) {} -private: - VectorType m_pos, m_normal; -}; - - class MyView : public nanogui::ImageView { public: - using PointCollection = Ponca::KdTree; - /// Initialize the widget - explicit MyView(Widget *parent); + explicit MyView(Widget *parent, DataManager* mgr); // Check if a point is inside the image bool isInsideImage(const nanogui::Vector2f &lp) const; @@ -53,22 +34,11 @@ class MyView : public nanogui::ImageView { /// Disable scrolling inline bool scroll_event(const nanogui::Vector2i &p, const nanogui::Vector2f &rel) override {return true;} - /// Read access to point collection - inline const PointCollection& getPointCollection() const { return m_tree; } - - /// Set Update function, called after each point update - inline void setUpdateFunction(std::function &&f) { m_updateFunction = f; } - /// Set selection threshold inline void setSelectionThreshold(float dist) { m_selectionThreshold = dist; } private: - inline void updateCollection(); - - using PointContainer = std::vector; // stores x,y,normal angle in radians - PointContainer m_points; - PointCollection m_tree; - std::function m_updateFunction {[](){}}; int m_movedPoint{-1}; float m_selectionThreshold{2}; // distance in pixel used to select points + DataManager* m_dataMgr{nullptr}; }; diff --git a/src/poncaTypes.h b/src/poncaTypes.h new file mode 100644 index 0000000..b9b0873 --- /dev/null +++ b/src/poncaTypes.h @@ -0,0 +1,27 @@ +#pragma once + +#include + + +class DataPoint +{ +public: + enum {Dim = 2}; + using Scalar = float; + using VectorType = Eigen::Vector; + using MatrixType = Eigen::Matrix; + [[nodiscard]] inline const auto& pos() const {return m_pos;} + [[nodiscard]] inline const auto& normal() const {return m_normal;} + /// \fixme Use maps to avoid duplication + explicit inline DataPoint(const nanogui::Vector3f &pn) + : m_pos({pn.x(), pn.y()}), m_normal({std::cos(pn.z()), std::sin(pn.z())}) {} +private: + VectorType m_pos, m_normal; +}; + +using WeightFunc = Ponca::DistWeightFunc >; + +using PlaneFit = Ponca::Basket; +using SphereFit = Ponca::Basket; +using OrientedSphereFit= Ponca::Basket; +using UnorientedSphereFit = Ponca::Basket; \ No newline at end of file