GithubHelp home page GithubHelp logo

chenzhaiyu / abspy Goto Github PK

View Code? Open in Web Editor NEW
66.0 6.0 10.0 4.06 MB

A Python tool for 3D adaptive binary space partitioning and beyond

Home Page: https://abspy.readthedocs.io/

License: MIT License

Python 100.00%
point-cloud 3d adaptive binary-space-partition reconstruction python

abspy's Introduction


License: MIT PyPI version Build status

Introduction

abspy is a Python tool for 3D adaptive binary space partitioning and beyond: an ambient 3D space is adaptively partitioned to form a linear cell complex with planar primitives, where an adjacency graph is dynamically obtained. The tool is designed primarily to support compact surface reconstruction and other applications as well.


Key features

  • Manipulation of planar primitives from point cloud or reference mesh
  • Linear cell complex creation with adaptive binary space partitioning (a-BSP)
  • Dynamic BSP-tree (NetworkX graph) updated locally upon primitive insertion
  • Support of polygonal surface reconstruction with graph cut
  • Compatible data structure with Easy3D on point cloud, primitive, mesh and cell complex
  • Robust spatial operations underpinned by the rational ring from SageMath's exact kernel

Installation

All-in-one installation

Create a conda environment with the latest abspy release and all its dependencies installed:

git clone https://github.com/chenzhaiyu/abspy && cd abspy
conda env create -f environment.yml && conda activate abspy

Manual installation

Still easy! Create a conda environment and enter it:

conda create --name abspy python=3.10 && conda activate abspy

Install the dependencies:

conda install -c conda-forge networkx numpy tqdm scikit-learn matplotlib colorlog scipy trimesh rtree pyglet sage=10.0 

Alternatively, you can use mamba for faster package parsing installation:

conda install mamba -c conda-forge
mamba install -c conda-forge networkx numpy tqdm scikit-learn matplotlib colorlog scipy trimesh rtree pyglet sage=10.0 

Preferably, the latest abspy release can be found and installed via PyPI:

pip install abspy

Otherwise, you can install the latest version locally:

git clone https://github.com/chenzhaiyu/abspy && cd abspy
pip install .

Quick start

Example 1 - Reconstruction from point cloud

The example loads a point cloud to VertexGroup (.vg), partitions ambient space into a cell complex, creates the adjacency graph, and extracts the object's outer surface.

from abspy import VertexGroup, AdjacencyGraph, CellComplex

# load a point cloud in VertexGroup 
vertex_group = VertexGroup(filepath='points.vg')

# normalise point cloud
vertex_group.normalise_to_centroid_and_scale()

# additional planes to append (e.g., a bounding plane)
additional_planes = [[0, 0, 1, -vertex_group.aabbs[:, 0, 2].min()]]

# initialise cell complex
cell_complex = CellComplex(vertex_group.planes, vertex_group.aabbs, vertex_group.obbs, vertex_group.points_grouped, build_graph=True, additional_planes=additional_planes)

# refine planar primitives
cell_complex.refine_planes()

# prioritise certain planes (e.g., vertical ones)
cell_complex.prioritise_planes(prioritise_verticals=True)

# construct cell complex 
cell_complex.construct()

# print info about cell complex
cell_complex.print_info()

# build adjacency graph from cell complex
adjacency_graph = AdjacencyGraph(cell_complex.graph)

# assign weights (e.g., occupancy by neural network prediction) to graph 
adjacency_graph.assign_weights_to_n_links(cell_complex.cells, attribute='area_overlap', factor=0.001, cache_interfaces=True)
adjacency_graph.assign_weights_to_st_links(...)

# perform graph cut to extract surface
_, _ = adjacency_graph.cut()

# save surface model to an OBJ file
adjacency_graph.save_surface_obj('surface.obj', engine='rendering')

Example 2 - Convex decomposition from mesh

The example loads a mesh to VertexGroupReference, partitions ambient space into a cell complex, identifies cells inside reference mesh, and visualizes the cells.

from abspy import VertexGroupReference
vertex_group_reference = VertexGroupReference(filepath='mesh.obj')

# initialise cell complex
cell_complex = CellComplex(vertex_group_reference.planes, vertex_group_reference.aabbs, vertex_group_reference.obbs, build_graph=True)

# construct cell complex 
cell_complex.construct()

# cells inside reference mesh
cells_in_mesh = cell_complex.cells_in_mesh('mesh.obj', engine='distance')

# save cell complex file
cell_complex.save('complex.cc')

# visualise the inside cells
if len(cells_in_mesh):
    cell_complex.visualise(indices_cells=cells_in_mesh)

Please find the usage of abspy at API reference. For the data structure of a .vg/.bvg file, please refer to VertexGroup.

FAQ

  • How can I install abspy on Windows?

For Windows users, you may need to build SageMath from source or install all other dependencies into a pre-built SageMath environment. Otherwise, virtualization with docker may come to the rescue.

  • How can I use abspy for surface reconstruction?

As demonstrated in Example 1, the surface can be addressed by graph cut — in between adjacent cells where one being inside and the other being outside — exactly where the cut is performed. For more information, please refer to Points2Poly which wraps abspy for building surface reconstruction.

adaptive

License

MIT

Citation

If you use abspy in a scientific work, please consider citing the paper:

@article{chen2022points2poly,
  title = {Reconstructing compact building models from point clouds using deep implicit fields},
  journal = {ISPRS Journal of Photogrammetry and Remote Sensing},
  volume = {194},
  pages = {58-73},
  year = {2022},
  issn = {0924-2716},
  doi = {https://doi.org/10.1016/j.isprsjprs.2022.09.017},
  url = {https://www.sciencedirect.com/science/article/pii/S0924271622002611},
  author = {Zhaiyu Chen and Hugo Ledoux and Seyran Khademi and Liangliang Nan}
}

abspy's People

Contributors

chenzhaiyu avatar wangyuqing0424 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

abspy's Issues

Sampling primitives from GT mesh

Providing an option to sample primitives directly from GT meshes. This is for an ablation study where the assumption is made that primitives can be accurately detected.

Unstable `_bbox_intersect()`

Unexpected AssertionError while initial bound should be fine:

AssertionError: intersection failed! check the initial bound

Degenerate cases found, likely due to arithmetic error of bounds (ndarray):

indices_cells = self._bbox_intersect(self.bounds[i], self.planes[i], exhaustive)

One possible fix is to stick to the rational ring, which however might harm the performance.

ValueError: No such backend xxx implemented for given basering (=Integer Ring).

hi, i got a bug when i try to run the 'quick start' code from github

the bug is happened when called the function CellComplex from complex.py in abspy. it seems like the problem is sagemath but i am not sure.

Steps to reproduce the behavior:

  1. Input 'test_points.vg' download with abspy

  2. See error: No such backend (=[[-73955476/123260931, 29460755/49106297], [-30488850/110333459, 57316241/207374152], [-31705399/335534790, 31705409/335534790]]) implemented for given basering (=Integer Ring).

Screenshots
2023-09-25 214727

Additional context
my sage version is 9.0 and the abspy version is 0.2.3

Speeding up `CellComplex.construct()`

The current construct() is slow. Polyhedron.intersection(other) takes most of the time, as profiled with line_profiler:

Timer unit: 1e-06 s

Total time: 27.422 s
File: /Users/zhaiyu/opt/anaconda3/envs/abspy/lib/python3.8/site-packages/abspy/complex.py
Function: construct at line 390

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
   390                                               @profile
   391                                               def construct(self, exhaustive=False):
   392                                                   """
   393                                                   Construct cell complex.
   394                                           
   395                                                   Two-stage primitive-in-cell predicate.
   396                                                   (1) bounding boxes of primitive and existing cells are evaluated
   397                                                   for possible intersection. (2), a strict intersection test is performed.
   398                                           
   399                                                   Generated cells are stored in self.cells.
   400                                                   * query the bounding box intersection.
   401                                                   * optional: intersection test for polygon and edge in each potential cell.
   402                                                   * partition the potential cell into two. rewind if partition fails.
   403                                           
   404                                                   Parameters
   405                                                   ----------
   406                                                   exhaustive: bool
   407                                                       Do exhaustive partitioning if set True
   408                                                   """
   409         1        266.0    266.0      0.0          logger.info('constructing cell complex')
   410         1          4.0      4.0      0.0          tik = time.time()
   411                                           
   412         1      13862.0  13862.0      0.1          pbar = range(len(self.bounds)) if self.quiet else trange(len(self.bounds))
   413        42      16878.0    401.9      0.1          for i in pbar:  # kinetic for each primitive
   414                                                       # bounding box intersection test
   415                                                       # indices of existing cells with potential intersections
   416        41     292416.0   7132.1      1.1              indices_cells = self._intersect(self.bounds[i], self.planes[i], exhaustive)
   417        41        134.0      3.3      0.0              assert len(indices_cells), 'intersection failed! check the initial bound'
   418                                           
   419                                                       # half-spaces defined by inequalities
   420                                                       # no change_ring() here (instead, QQ() in _inequalities) speeds up 10x
   421                                                       # init before the loop could possibly speed up a bit
   422        82      56790.0    692.6      0.2              hspace_positive, hspace_negative = [Polyhedron(ieqs=[inequality]) for inequality in
   423        41      35014.0    854.0      0.1                                                  self._inequalities(self.planes[i])]
   424                                           
   425                                                       # partition the intersected cells and their bounds while doing mesh slice plane
   426        41        153.0      3.7      0.0              indices_parents = []
   427                                           
   428      1013       3274.0      3.2      0.0              for index_cell in indices_cells:
   429       972    1727068.0   1776.8      6.3                  cell_positive = hspace_positive.intersection(self.cells[index_cell])
   430       972    1697056.0   1745.9      6.2                  cell_negative = hspace_negative.intersection(self.cells[index_cell])
   431                                           
   432       972      53076.0     54.6      0.2                  if cell_positive.dim() != 3 or cell_negative.dim() != 3:
   433                                                               # if cell_positive.is_empty() or cell_negative.is_empty():
   434                                                               """
   435                                                               cannot use is_empty() predicate for degenerate cases:
   436                                                                   sage: Polyhedron(vertices=[[0, 1, 2]])
   437                                                                   A 0-dimensional polyhedron in ZZ^3 defined as the convex hull of 1 vertex
   438                                                                   sage: Polyhedron(vertices=[[0, 1, 2]]).is_empty()
   439                                                                   False
   440                                                               """
   441       105        113.0      1.1      0.0                      continue
   442                                           
   443                                                           # incrementally build the adjacency graph
   444       795       1518.0      1.9      0.0                  if self.graph is not None:
   445                                                               # append the two nodes (UID) being partitioned
   446       795       7322.0      9.2      0.0                      self.graph.add_node(self.index_node + 1)
   447       795       3261.0      4.1      0.0                      self.graph.add_node(self.index_node + 2)
   448                                           
   449                                                               # append the edge in between
   450       795       5137.0      6.5      0.0                      self.graph.add_edge(self.index_node + 1, self.index_node + 2)
   451                                           
   452                                                               # get neighbours of the current cell from the graph
   453       795      30508.0     38.4      0.1                      neighbours = self.graph[list(self.graph.nodes)[index_cell]]  # index in the node list
   454                                           
   455       795       1630.0      2.1      0.0                      if neighbours:
   456                                                                   # get the neighbouring cells to the parent
   457       794     119186.0    150.1      0.4                          cells_neighbours = [self.cells[self._index_node_to_cell(n)] for n in neighbours]
   458                                           
   459                                                                   # adjacency test between both created cells and their neighbours
   460                                                                   # todo:
   461                                                                   #   Avoid 3d-3d intersection if possible. Unsliced neighbours connect with only one child
   462                                                                   #   - reduce computation by half - can be further reduced using vertices/faces instead of
   463                                                                   #   polyhedron intersection. Sliced neighbors connect with both children
   464                                           
   465      8090      13708.0      1.7      0.0                          for n, cell in enumerate(cells_neighbours):
   466                                           
   467      7296   13415562.0   1838.8     48.9                              interface_positive = cell_positive.intersection(cell)
   468                                           
   469      7296     218415.0     29.9      0.8                              if interface_positive.dim() == 2:
   470                                                                           # this neighbour can connect with either or both children
   471      5100      69081.0     13.5      0.3                                  self.graph.add_edge(self.index_node + 1, list(neighbours)[n])
   472      5100    9149388.0   1794.0     33.4                                  interface_negative = cell_negative.intersection(cell)
   473      5100     144452.0     28.3      0.5                                  if interface_negative.dim() == 2:
   474      2894      39336.0     13.6      0.1                                      self.graph.add_edge(self.index_node + 2, list(neighbours)[n])
   475                                                                       else:
   476                                                                           # this neighbour must otherwise connect with the other child
   477      2196      30033.0     13.7      0.1                                  self.graph.add_edge(self.index_node + 2, list(neighbours)[n])
   478                                           
   479                                                               # update cell id
   480       795       1257.0      1.6      0.0                      self.index_node += 2
   481                                           
   482       795       1197.0      1.5      0.0                  self.cells.append(cell_positive)
   483       795        849.0      1.1      0.0                  self.cells.append(cell_negative)
   484                                           
   485                                                           # incrementally cache the bounds for created cells
   486       795     137263.0    172.7      0.5                  self.cells_bounds.append(cell_positive.bounding_box())
   487       795     114524.0    144.1      0.4                  self.cells_bounds.append(cell_negative.bounding_box())
   488                                           
   489       795       1246.0      1.6      0.0                  indices_parents.append(index_cell)
   490                                           
   491                                                       # delete the parent cells and their bounds. this does not affect the appended ones
   492       836       1132.0      1.4      0.0              for index_parent in sorted(indices_parents, reverse=True):
   493       795       1029.0      1.3      0.0                  del self.cells[index_parent]
   494       795        959.0      1.2      0.0                  del self.cells_bounds[index_parent]
   495                                           
   496                                                           # remove the parent node (and subsequently its incident edges) in the graph
   497       795        807.0      1.0      0.0                  if self.graph is not None:
   498       795      16774.0     21.1      0.1                      self.graph.remove_node(list(self.graph.nodes)[index_parent])
   499                                           
   500         1          3.0      3.0      0.0          self.constructed = True
   501         1        305.0    305.0      0.0          logger.info('cell complex constructed: {:.2f} s'.format(time.time() - tik))

qiuckstart

Hi, I follow the your steps to install the packages but failed to run the quick start code.
I use your test data ‘test_points.vg’
It shows 'NameError: name 'bounds' is not defined'
1667836011343

then I add ‘from trimesh import bounds’
but it shows ‘TypeError: 'module' object is not subscriptable’
1667836479358

this are my environment
ubuntu 20.04
python=3.9.13
abspy=0.2.1
sage=9.4
others are installed by ‘pip install -r requirements’

Can I get any tips? thank you

TypeError when importing SageMath==9.5

Environment:

  • Linux
  • Python 3.9.13
  • SageMath 9.5 (from conda-forge)

How to reproduce

>>> from sage.all import polytopes, QQ, RR, Polyhedron

Error message

Traceback (most recent call last):
  File "/tmp/pycharm_project_629/tests/test_complex.py", line 4, in <module>
    from abspy import CellComplex
  File "/tmp/pycharm_project_629/abspy/__init__.py", line 1, in <module>
    from .complex import CellComplex
  File "/tmp/pycharm_project_629/abspy/complex.py", line 23, in <module>
    from sage.all import polytopes, QQ, RR, Polyhedron
  File "/workspace/env/miniconda3/envs/abspy/lib/python3.9/site-packages/sage/all.py", line 131, in <module>
    from sage.rings.all      import *
  File "/workspace/env/miniconda3/envs/abspy/lib/python3.9/site-packages/sage/rings/all.py", line 87, in <module>
    from .qqbar import (AlgebraicRealField, AA,
  File "/workspace/env/miniconda3/envs/abspy/lib/python3.9/site-packages/sage/rings/qqbar.py", line 2810, in <module>
    QQxy = QQ['x', 'y']
  File "sage/structure/parent.pyx", line 1276, in sage.structure.parent.Parent.__getitem__ (build/cythonized/sage/structure/parent.c:11472)
  File "/workspace/env/miniconda3/envs/abspy/lib/python3.9/site-packages/sage/categories/rings.py", line 1177, in __getitem__
    return PolynomialRing(self, elts)
  File "/workspace/env/miniconda3/envs/abspy/lib/python3.9/site-packages/sage/rings/polynomial/polynomial_ring_constructor.py", line 647, in PolynomialRing
    return _multi_variate(base_ring, names, **kwds)
  File "/workspace/env/miniconda3/envs/abspy/lib/python3.9/site-packages/sage/rings/polynomial/polynomial_ring_constructor.py", line 775, in _multi_variate
    from sage.rings.polynomial.multi_polynomial_libsingular import MPolynomialRing_libsingular
  File "sage/rings/polynomial/multi_polynomial_libsingular.pyx", line 1, in init sage.rings.polynomial.multi_polynomial_libsingular (build/cythonized/sage/rings/polynomial/multi_polynomial_libsingular.cpp:50113)
  File "sage/libs/singular/singular.pyx", line 1558, in init sage.libs.singular.singular (build/cythonized/sage/libs/singular/singular.cpp:15816)
  File "sage/libs/singular/singular.pyx", line 1525, in sage.libs.singular.singular.init_libsingular (build/cythonized/sage/libs/singular/singular.cpp:12934)
  File "/workspace/env/miniconda3/envs/abspy/lib/python3.9/posixpath.py", line 152, in dirname
    p = os.fspath(p)
TypeError: expected str, bytes or os.PathLike object, not NoneType

Process finished with exit code 1

Export mesh instead of polygon soup

Hi and thanks for making this nice work available!

I would like to use your library to export the cell complex and final surface. If I am not mistaken both can only be exported as polygon soups, ie with each face being a single component and many duplicate vertices.

Do you know if there is an easy way to access common vertices between adjacent cells and even adjacent facets through sage?

Kind regards

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.