Диаграмма Project Scipy Voronoi из 3d в 2d

Я пытаюсь найти способ рассчитать диаграмму 2d Power в Python. Для этого я хочу использовать тот факт, что двухмерную диаграмму мощности можно интерпретировать как пересечение обычная трехмерная диаграмма Вороного с плоскостью.

С SciPy Voronoi модуль Я могу рассчитать 3d диаграмму Вороного - есть ли возможность пересечь ее плоскостью и преобразовать в 2d диаграмму?


person yellowcap    schedule 10.12.2014    source источник


Ответы (1)


В SciPy еще нет возможности диаграммы мощности.

Преобразование 3D-диаграммы Вороного в 2D-диаграмму мощности, вероятно, будет трудным и, по крайней мере, в Python, медленным.

Чтобы обойти это, я разработал программу на C++, основанную на CGAL, которую затем можно обернуть Python.

Имея в руках функцию Python и код C++, можно создать диаграмму мощности, используя:

polys, bbox = GetPowerDiagram(pts)

И результат выглядит так, если диаграмма мощности обрезана по ограничивающей рамке облака точек:

Обрезанная диаграмма мощности Вороного

и это, если оно не обрезано (но преобразовано в конечное представление):

Конечное представление необрезанной степенной диаграммы Вороного

Приведенный ниже код работал с 12 января 2019 года с использованием GCC 7.3.0, Python 3.6.7, CGAL 4.11 (1041101000). Код также можно получить на Github здесь.

Код Python

#!/usr/bin/env python3
#Power Diagramer
#Author: Richard Barnes (rbarnes.org)
#!/usr/bin/env python3
import plumbum
import random
import shapely
import shapely.wkt
from matplotlib import pyplot as plt
from descartes import PolygonPatch

def GetPowerDiagram(points, ray_length=1000, crop=True):
  """Generates a power diagram of a set of points.

  Arguments:

    points - A list of points of the form `[(x,y,weight), (x,y,weight), ...]`

    ray_length - The power diagram contains infinite rays. The direction vector
                 of those rays will be multiplied by `ray_length` and the ends
                 of the rays connected in order to form a finite representation
                 of the polygon

    crop       - If `True`, then the bounded representation above is cropped to
                 the bounding box of the point cloud
  """
  powerd = plumbum.local["./power_diagramer.exe"]

  #Format output for reading by power_diagramer.exe
  points = [map(str,x) for x in points]
  points = [' '.join(x) for x in points]
  points = '\n'.join(points)
  points = '{raylen}\n{crop}\n{points}'.format(
    raylen = ray_length,
    crop   = 'CROP' if crop else 'NOCROP',
    points = points
  )

  #Run the command
  polygons = (powerd["-"] << points)()

  #Get the output of `power_diagramer.exe`. It is in WKT format, one polygon per
  #line.
  polygons = polygons.split("\n")
  polygons = [x.strip() for x in polygons]
  polygons = [x for x in polygons if len(x)>0]
  polygons = [shapely.wkt.loads(x) for x in polygons]

  #Generate bounding box for ease in plotting
  bbox = [x.bounds for x in polygons]
  minx = min([x[0] for x in bbox])
  miny = min([x[1] for x in bbox])
  maxx = max([x[2] for x in bbox])
  maxy = max([x[3] for x in bbox])

  return polygons, (minx,miny,maxx,maxy)

POINT_COUNT = 100
pts         = []
for i in range(POINT_COUNT):
  x      = random.uniform(0,100) 
  y      = random.uniform(0,100)
  weight = random.uniform(0,10)
  pts.append((x,y,weight))

polys, (minx, miny, maxx, maxy) = GetPowerDiagram(pts, ray_length=1000, crop=True)

fig = plt.figure(1, figsize=(5,5), dpi=90)
ax = fig.add_subplot(111)
ax.set_xlim(minx,maxx)
ax.set_ylim(miny,maxy)
for poly in polys:
  ax.add_patch(PolygonPatch(poly))
plt.show()

Makefile

all:
    #-frounding-math is GCC specific, but required for any CGAL code compiled with
    #GCC
    g++ -O3 power_diagram_lib.cpp -o power_diagramer.exe -Wall -lCGAL -lgmp -lgmpxx -lmpfr -frounding-math

И получившийся исполняемый файл называется power_diagramer.exe и помещается в тот же каталог, что и скрипт Python.

код С++

//Finds the cropped Voronoi diagram of a set of points and saves it as WKT
//Compile with: g++ -O3 main.cpp -o power_diagramer.exe -Wall -lCGAL -lgmp
//Author: Richard Barnes (rbarnes.org)
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <CGAL/Regular_triangulation_filtered_traits_2.h>
#include <CGAL/Regular_triangulation_adaptation_traits_2.h>
#include <CGAL/Regular_triangulation_adaptation_policies_2.h>
#include <CGAL/Regular_triangulation_2.h>
#include <CGAL/Voronoi_diagram_2.h>
#include <CGAL/Boolean_set_operations_2.h>
#include <CGAL/bounding_box.h>
#include <CGAL/Polygon_2.h>
#include <iostream>
#include <cstdint>
#include <string>
#include <memory>

typedef CGAL::Exact_predicates_exact_constructions_kernel K;
typedef CGAL::Regular_triangulation_2<K> RT;

typedef CGAL::Regular_triangulation_adaptation_traits_2<RT>         AT;
typedef CGAL::Regular_triangulation_degeneracy_removal_policy_2<RT> DRP;
typedef CGAL::Voronoi_diagram_2<RT, AT, DRP> VD;

int main(int argc, char **argv){
  if(argc!=2){
    std::cerr<<"Synax: "<<argv[0]<<" <FILENAME>"<<std::endl;
    std::cerr<<"<FILENAME> may be a file or '-'. The latter reads from stdin."<<std::endl;
    std::cerr<<"File has the format:"<<std::endl;
    std::cerr<<"<RAY_LENGTH>"        <<std::endl;
    std::cerr<<"<CROP/NOCROP>"       <<std::endl;
    std::cerr<<"<X> <Y> <WEIGHT>"    <<std::endl;
    std::cerr<<"<X> <Y> <WEIGHT>"    <<std::endl;
    std::cerr<<"..."                 <<std::endl;
    std::cerr                        <<std::endl;
    std::cerr<<"'RAY_LENGTH' is a multiplier that extends infinite rays"<<std::endl;
    std::cerr<<"'CROP' will crop the power diagram to the bounding box of the input points"<<std::endl;
    return -1;
  }

  std::string filename = argv[1];

  //Output formatting
  std::cout.precision(4);          //Number of digits of decimal precision
  std::cout.setf(std::ios::fixed); //Don't use scientific notation

  //Used to convert otherwise infinite rays into looooong line segments by
  //multiplying the components of the direction vector of a ray by this value.
  int RAY_LENGTH = 1000;

  //Create a pointer to the correct input stream
  std::istream *fin;
  if(filename=="-")
    fin = &std::cin;
  else
    fin = new std::ifstream(filename);

  std::string crop_string;
  bool do_crop = false;

  (*fin)>>RAY_LENGTH;
  (*fin)>>crop_string;
  if(crop_string=="CROP")
    do_crop = true;
  else if(crop_string=="NOCROP")
    do_crop = false;
  else {
    std::cerr<<"Crop value must be 'CROP' or 'NOCROP'!"<<std::endl;
    return -1;
  }

  //Read in points from the command line
  RT::Weighted_point wp;
  std::vector<RT::Weighted_point> wpoints;
  while((*fin)>>wp)
    wpoints.push_back(wp);

  //Clean up the stream pointer
  if(filename!="-")
    delete fin;

  //Create a Regular Triangulation from the points
  RT rt(wpoints.begin(), wpoints.end());
  rt.is_valid();

  //Wrap the triangulation with a Voronoi diagram adaptor. This is necessary to
  //get the Voronoi faces.
  VD vd(rt);

  //CGAL often returns objects that are either segments or rays. This converts
  //these objects into segments. If the object would have resolved into a ray,
  //that ray is intersected with the bounding box defined above and returned as
  //a segment.
  const auto ConvertToSeg = [&](const CGAL::Object seg_obj, bool outgoing) -> K::Segment_2 {
    //One of these will succeed and one will have a NULL pointer
    const K::Segment_2 *dseg = CGAL::object_cast<K::Segment_2>(&seg_obj);
    const K::Ray_2     *dray = CGAL::object_cast<K::Ray_2>(&seg_obj);
    if (dseg) { //Okay, we have a segment
      return *dseg;
    } else {    //Must be a ray
      const auto &source = dray->source();
      const auto dsx     = source.x();
      const auto dsy     = source.y();
      const auto &dir    = dray->direction();
      const auto tpoint  = K::Point_2(dsx+RAY_LENGTH*dir.dx(),dsy+RAY_LENGTH*dir.dy());
      if(outgoing)
        return K::Segment_2(
          dray->source(),
          tpoint
        );
      else
        return K::Segment_2(
          tpoint,
          dray->source()
        );
    }
  };

  //Loop over the faces of the Voronoi diagram in some arbitrary order
  for(VD::Face_iterator fit = vd.faces_begin(); fit!=vd.faces_end();++fit){
    CGAL::Polygon_2<K> pgon;

    //Edge circulators traverse endlessly around a face. Make a note of the
    //starting point so we know when to quit.
    VD::Face::Ccb_halfedge_circulator ec_start = fit->ccb();

    //Find a bounded edge to start on
    for(;ec_start->is_unbounded();ec_start++){}

    //Current location of the edge circulator
    VD::Face::Ccb_halfedge_circulator ec = ec_start;

    //In WKT format each polygon must begin and end with the same point
    K::Point_2 first_point;

    do {
      //A half edge circulator representing a ray doesn't carry direction
      //information. To get it, we take the dual of the dual of the half-edge.
      //The dual of a half-edge circulator is the edge of a Delaunay triangle.
      //The dual of the edge of Delaunay triangle is either a segment or a ray.
      // const CGAL::Object seg_dual = rt.dual(ec->dual());
      const CGAL::Object seg_dual = vd.dual().dual(ec->dual());

      //Convert the segment/ray into a segment
      const auto this_seg = ConvertToSeg(seg_dual, ec->has_target());

      pgon.push_back(this_seg.source());
      if(ec==ec_start)
        first_point = this_seg.source();

      //If the segment has no target, it's a ray. This means that the next
      //segment will also be a ray. We need to connect those two rays with a
      //segment. The following accomplishes this.
      if(!ec->has_target()){
        const CGAL::Object nseg_dual = vd.dual().dual(ec->next()->dual());
        const auto next_seg = ConvertToSeg(nseg_dual, ec->next()->has_target());
        pgon.push_back(next_seg.target());
      }
    } while ( ++ec != ec_start ); //Loop until we get back to the beginning

    if(do_crop){
      //Find the bounding box of the points. This will be used to crop the Voronoi
      //diagram later.
      const CGAL::Bbox_2 bbox = CGAL::bbox_2(wpoints.begin(), wpoints.end());

      //In order to crop the Voronoi diagram, we need to convert the bounding box
      //into a polygon. You'd think there'd be an easy way to do this. But there
      //isn't (or I haven't found it).
      CGAL::Polygon_2<K> bpoly;
      bpoly.push_back(K::Point_2(bbox.xmin(),bbox.ymin()));
      bpoly.push_back(K::Point_2(bbox.xmax(),bbox.ymin()));
      bpoly.push_back(K::Point_2(bbox.xmax(),bbox.ymax()));
      bpoly.push_back(K::Point_2(bbox.xmin(),bbox.ymax()));

      //Perform the intersection. Since CGAL is very general, it believes the
      //result might be multiple polygons with holes.
      std::list<CGAL::Polygon_with_holes_2<K>> isect;
      CGAL::intersection(pgon, bpoly, std::back_inserter(isect));

      //But we know better. The intersection of a convex polygon and a box is
      //always a single polygon without holes. Let's assert this.
      assert(isect.size()==1);

      //And recover the polygon of interest
      auto &poly_w_holes = isect.front();
      pgon               = poly_w_holes.outer_boundary();
    }

    //Print the polygon as a WKT polygon
    std::cout<<"POLYGON ((";
    for(auto v=pgon.vertices_begin();v!=pgon.vertices_end();v++)
      std::cout<<v->x()<<" "<<v->y()<<", ";
    std::cout<<pgon.vertices_begin()->x()<<" "<<pgon.vertices_begin()->y()<<"))\n";
  }

  return 0;
}
person Richard    schedule 06.05.2018
comment
Ответ на вопрос четырехлетней давности с таким обаянием и некоторыми из ваших прошлых элементов портфолио Projects заслуживают моего приветствия и [+1] Оставайтесь с нами, чувак! - person user3666197; 06.05.2018
comment
Я могу просто добавить @user3666197, спасибо за пост, Ричард. Тем временем я решил проблему с проекцией 3D в 2D (но, к сожалению, не могу поделиться кодом). Я уверен, что это будет полезно для других людей в будущем! - person yellowcap; 07.05.2018