From a9874ef5ffaa2db8ed4792b91560f67cd03a34ec Mon Sep 17 00:00:00 2001 From: Timo Koch <timo.koch@iws.uni-stuttgart.de> Date: Tue, 23 Jan 2018 18:10:31 +0100 Subject: [PATCH] [geometry] Add convex hull of point cloud + triangulation and test --- dumux/common/geometry/grahamconvexhull.hh | 163 ++++++++++++++++++ test/common/geometry/CMakeLists.txt | 5 +- .../geometry/test_graham_convex_hull.cc | 125 ++++++++++++++ test/common/geometry/writetriangulation.hh | 84 +++++++++ 4 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 dumux/common/geometry/grahamconvexhull.hh create mode 100644 test/common/geometry/test_graham_convex_hull.cc create mode 100644 test/common/geometry/writetriangulation.hh diff --git a/dumux/common/geometry/grahamconvexhull.hh b/dumux/common/geometry/grahamconvexhull.hh new file mode 100644 index 0000000000..67ef578a83 --- /dev/null +++ b/dumux/common/geometry/grahamconvexhull.hh @@ -0,0 +1,163 @@ +/***************************************************************************** + * See the file COPYING for full copying permissions. * + * * + * This program 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 2 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 <http://www.gnu.org/licenses/>. * + *****************************************************************************/ +/*! + * \file + * \ingroup Common + * \brief A function to compute the convex hull of a point cloud + * and a function to triangulate the polygon area spanned by the convex hull + */ +#ifndef DUMUX_GRAHAM_CONVEX_HULL_HH +#define DUMUX_GRAHAM_CONVEX_HULL_HH + +#include <vector> +#include <array> +#include <algorithm> + +#include <dune/common/exceptions.hh> +#include <dune/common/fvector.hh> + +#include <dumux/common/math.hh> + +namespace Dumux { + +/*! + * \brief Returns the orientation of a sequence a-->b-->c in one plane (defined by normal vector) + * \return -1 if a-->b-->c forms a counter-clockwise turn (given the normal vector) + * +1 for a clockwise turn, + * 0 if they are on one line (colinear) + */ +int getOrientation(const Dune::FieldVector<double, 3>& a, + const Dune::FieldVector<double, 3>& b, + const Dune::FieldVector<double, 3>& c, + const Dune::FieldVector<double, 3>& normal) +{ + const auto d = b-a; + const auto e = c-b; + const auto f = Dumux::crossProduct(d, e); + const auto area = f*normal; + return Dumux::sign(-area); +} + +/*! + * \brief Compute the points making up the convex hull around the given set of unordered points + * \note We assume that all points are coplanar + * \note this algorithm changes the order of the given points a bit + * as they are unordered anyway this shouldn't matter too much + */ +std::vector<Dune::FieldVector<double, 3>> +grahamConvexHull2d3d(std::vector<Dune::FieldVector<double, 3>>& points) +{ + using Point = Dune::FieldVector<double, 3>; + std::vector<Point> convexHull; convexHull.reserve(50); + + // return empty convex hull + if (points.size() < 3) + return convexHull; + + // return the points (already just one triangle) + if (points.size() == 3) + return points; + + // get the normal vector of the plane + const auto a = points[1] - points[0]; + const auto b = points[2] - points[0]; + auto normal = Dumux::crossProduct(a, b); + normal /= normal.two_norm(); + + // find the element with the smallest y coordinate (if y is the same, smallest x coordinate) + auto minIt = std::min_element(points.begin(), points.end(), + [](const auto& a, const auto& b) + { return a[1] != b[1] ? a[1] < b[1] : a[0] < b[0]; }); + + // swap the smallest element to the front + std::iter_swap(minIt, points.begin()); + + // choose the first (min element) as the pivot point + // sort in counter-clockwise order around pivot point + const auto pivot = points[0]; + std::sort(points.begin()+1, points.end(), [&](const auto& a, const auto& b) + { + int order = getOrientation(pivot, a, b, normal); + if (order == 0) + return (a-pivot).two_norm() < (b-pivot).two_norm(); + else + return (order == -1); + }); + + // push the first three points + convexHull.push_back(points[0]); + convexHull.push_back(points[1]); + convexHull.push_back(points[2]); + + // This is the heart of the algorithm + // Pop_back until the last point in the queue forms a counter-clockwise oriented line + // with the new vertices. Then add new points to the queue. + for (std::size_t i = 3; i < points.size(); ++i) + { + Point p = convexHull.back(); + convexHull.pop_back(); + // keep popping until the orientation a->b->currentp is counter-clockwise + while (getOrientation(convexHull.back(), p, points[i], normal) != -1) + { + p = convexHull.back(); + convexHull.pop_back(); + } + + // add back the last popped point and this point + convexHull.emplace_back(std::move(p)); + convexHull.push_back(points[i]); + } + + return convexHull; +} + +/*! + * \brief Triangulate area given points of the convex hull + * \note Assumes all points of the convex hull are coplanar + * \note This inserts a mid point and connects all corners with that point to triangles + */ +std::vector<std::array<Dune::FieldVector<double, 3>, 3> > +triangulateConvexHull(const std::vector<Dune::FieldVector<double, 3>>& convexHull) +{ + using Point = Dune::FieldVector<double, 3>; + using Triangle = std::array<Point, 3>; + + if (convexHull.size() < 3) + DUNE_THROW(Dune::InvalidStateException, "Try to triangulate point cloud with less than 3 points!"); + + if (convexHull.size() == 3) + return std::vector<Triangle>(1, {convexHull[0], convexHull[1], convexHull[2]}); + + Point midPoint(0.0); + for (const auto p : convexHull) + midPoint += p; + midPoint /= convexHull.size(); + + std::vector<Triangle> triangulation; + triangulation.reserve(convexHull.size()); + + for (std::size_t i = 0; i < convexHull.size()-1; ++i) + triangulation.emplace_back(Triangle{midPoint, convexHull[i], convexHull[i+1]}); + + triangulation.emplace_back(Triangle{midPoint, convexHull[convexHull.size()-1], convexHull[0]}); + + return triangulation; +} + +} // end namespace Dumux + +# endif diff --git a/test/common/geometry/CMakeLists.txt b/test/common/geometry/CMakeLists.txt index 41465b6623..2d10a00d2e 100644 --- a/test/common/geometry/CMakeLists.txt +++ b/test/common/geometry/CMakeLists.txt @@ -1,8 +1,11 @@ dune_add_test(SOURCES test_1d3d_intersection.cc) +dune_add_test(SOURCES test_graham_convex_hull.cc) #install sources install(FILES test_1d3d_intersection.cc +test_graham_convex_hull.cc +writetriangulation.hh DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/dumux/test/common/geometry) -set(CMAKE_BUILD_TYPE Release) +set(CMAKE_BUILD_TYPE RelWithDebInfo) diff --git a/test/common/geometry/test_graham_convex_hull.cc b/test/common/geometry/test_graham_convex_hull.cc new file mode 100644 index 0000000000..d08cf7f859 --- /dev/null +++ b/test/common/geometry/test_graham_convex_hull.cc @@ -0,0 +1,125 @@ +// graham convex hull test + triangulation +#include <config.h> + +#include <iostream> +#include <fstream> +#include <string> +#include <random> +#include <vector> +#include <algorithm> + +#include <dune/common/exceptions.hh> +#include <dune/common/fvector.hh> +#include <dune/common/timer.hh> + +#include <dumux/common/math.hh> +#include <dumux/common/geometry/grahamconvexhull.hh> +#include <test/common/geometry/writetriangulation.hh> + +template<class Scalar> +class UniformDistributedRandomNumber +{ +public: + UniformDistributedRandomNumber(Scalar min, Scalar max) + : gen_(r_()), dist_(min, max) {} + + Scalar operator()() + { return dist_(gen_); } + +private: + std::random_device r_; + std::default_random_engine gen_; + std::uniform_real_distribution<Scalar> dist_; +}; + +Dune::FieldVector<double, 3> +randomPointOnPlane(const Dune::FieldVector<double, 3>& origin, + const Dune::FieldVector<double, 3>& normal, + double min = -1.0, double max = 1.0) +{ + using Point = Dune::FieldVector<double, 3>; + + UniformDistributedRandomNumber<double> dice(min, max); + Point p{dice(), dice(), dice()}; + p = Dumux::crossProduct(normal, p); + p += origin; + return p; +} + +void writeCSV(const std::vector<Dune::FieldVector<double, 3>>& p, + const std::string& filename) +{ + std::ofstream fout(filename + ".csv"); + fout << "X, Y, Z\n"; + for (const auto& point : p) + fout << point[0] << ", " << point[1] << ", " << point[2] << '\n'; + fout << std::endl; +} + +void writeVTKPolyData(const std::vector<Dune::FieldVector<double, 3>>& p, + const std::string& filename) +{ + std::ofstream fout(filename + ".vtp"); + fout << "<?xml version=\"1.0\"?>\n" + << "<VTKFile type=\"PolyData\" version=\"0.1\" byte_order=\"LittleEndian\">\n" + << " <PolyData>\n" + << " <Piece NumberOfPoints=\"" << p.size() << "\" NumberOfLines=\"" << p.size() << "\">\n" + << " <Points>\n" + << " <DataArray type=\"Float32\" Name=\"Coordinates\" NumberOfComponents=\"3\" format=\"ascii\">\n"; + + for (const auto& point : p) + fout << point << " "; + fout << '\n'; + + fout << " </DataArray>\n" + << " </Points>\n" + << " <Lines>\n" + << " <DataArray type=\"Int32\" Name=\"connectivity\" NumberOfComponents=\"1\" format=\"ascii\">\n"; + + for (std::size_t i = 0; i < p.size()-1; ++i) + fout << i << " " << i+1 << " "; + fout << p.size()-1 << " 0\n"; + + fout << " </DataArray>\n"; + fout << " <DataArray type=\"Int32\" Name=\"offsets\" NumberOfComponents=\"1\" format=\"ascii\">\n"; + + for (std::size_t i = 1; i <= p.size(); ++i) + fout << i*2 << " "; + fout << '\n'; + + fout << " </DataArray>\n" + << " </Lines>\n" + << " </Piece>\n" + << "</PolyData>\n" + << "</VTKFile>\n"; +} + +int main(int argc, char* argv[]) +{ + using Point = Dune::FieldVector<double, 3>; + // create 100 random points + std::vector<Point> points(50); + // normal vector of the plane + const Point normal({0.0, 1.0, 1.0}); + const Point origin({3.0, 0.0, 0.0}); + for (auto&& p : points) + p = randomPointOnPlane(origin, normal, -10.0, 10.0); + + writeCSV(points, "points"); + + Dune::Timer timer; + auto convexHullPoints = Dumux::grahamConvexHull2d3d(points); + std::cout << "Computed convex hull of " << points.size() << " points in " + << timer.elapsed() << " seconds." << std::endl; + + writeVTKPolyData(convexHullPoints, "convexhull"); + + Dune::Timer timer2; + auto triangles = Dumux::triangulateConvexHull(convexHullPoints); + std::cout << "Computed triangulation of convex hull with " << convexHullPoints.size() + << " points in " << timer2.elapsed() << " seconds." << std::endl; + + Dumux::writeVTKPolyDataTriangle(triangles, "triangulation"); + + return 0; +} diff --git a/test/common/geometry/writetriangulation.hh b/test/common/geometry/writetriangulation.hh new file mode 100644 index 0000000000..4aa8e184a0 --- /dev/null +++ b/test/common/geometry/writetriangulation.hh @@ -0,0 +1,84 @@ +/***************************************************************************** + * See the file COPYING for full copying permissions. * + * * + * This program 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 2 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 <http://www.gnu.org/licenses/>. * + *****************************************************************************/ +/*! + * \file + * \ingroup Common + * \brief A function to write a triangulation to vtk + */ +#ifndef DUMUX_TEST_WRITE_TRIANGULATION_HH +#define DUMUX_TEST_WRITE_TRIANGULATION_HH + +#include <fstream> +#include <string> +#include <iterator> + +namespace Dumux { + +//! TriangleVector has to be a nested vector of triangles in 3d +template<class TriangleVector> +void writeVTKPolyDataTriangle(const TriangleVector& triangles, + const std::string& filename) +{ + std::ofstream fout(filename + ".vtp"); + fout << "<?xml version=\"1.0\"?>\n" + << "<VTKFile type=\"PolyData\" version=\"0.1\" byte_order=\"LittleEndian\">\n" + << " <PolyData>\n" + << " <Piece NumberOfPoints=\"" << triangles.size()*3 << "\" NumberOfLines=\"" << triangles.size()*3 << "\">\n" + << " <Points>\n" + << " <DataArray type=\"Float32\" Name=\"Coordinates\" NumberOfComponents=\"3\" format=\"ascii\">\n"; + + for (const auto& t : triangles) + for (const auto& p : t) + fout << p << " "; + fout << '\n'; + + fout << " </DataArray>\n" + << " </Points>\n" + << " <Lines>\n" + << " <DataArray type=\"Int32\" Name=\"connectivity\" NumberOfComponents=\"1\" format=\"ascii\">\n"; + + int offset = 0; + for (const auto& t : triangles) + { + for (std::size_t i = 0; i < t.size()-1; ++i) + fout << i + offset*3 << " " << i+1 + offset*3 << " "; + fout << t.size()-1 + offset*3 << " " << offset*3 << " "; + ++offset; + } + + fout << " </DataArray>\n"; + fout << " <DataArray type=\"Int32\" Name=\"offsets\" NumberOfComponents=\"1\" format=\"ascii\">\n"; + + offset = 0; + for (const auto& t : triangles) + { + for (std::size_t i = 1; i <= t.size(); ++i) + fout << i*2 + offset*6 << " "; + ++offset; + } + fout << '\n'; + + fout << " </DataArray>\n" + << " </Lines>\n" + << " </Piece>\n" + << "</PolyData>\n" + << "</VTKFile>\n"; +} + +} // end namespace Dumux + +# endif -- GitLab