├── .gitignore ├── License.txt ├── README.md ├── main.py ├── pcloudpy ├── __init__.py ├── core │ ├── __init__.py │ ├── base.py │ ├── filters │ │ ├── Delaunay2D.py │ │ ├── Delaunay3D.py │ │ ├── DisplayNormals.py │ │ ├── ExtractPolyData.py │ │ ├── NormalsEstimation.py │ │ ├── OrientedNormalEstimation.py │ │ ├── ScreenedPoisson.py │ │ ├── StatisticalOutlierRemovalFilter.py │ │ ├── __init__.py │ │ ├── base.py │ │ ├── vtkPointSetNormalsEstimation.py │ │ └── vtkPointSetOutlierRemoval.py │ ├── grid.py │ ├── interpolation.py │ ├── io │ │ ├── ReaderLAS.py │ │ ├── ReaderPLY.py │ │ ├── ReaderTIFF.py │ │ ├── ReaderVTP.py │ │ ├── ReaderXYZ.py │ │ ├── __init__.py │ │ ├── base.py │ │ └── converters.py │ └── utils │ │ ├── __init__.py │ │ └── vtkhelpers.py ├── display │ └── __init__.py └── gui │ ├── AppObject.py │ ├── MainWindow.py │ ├── MainWindowBase.py │ ├── ManagerLayer.py │ ├── __init__.py │ ├── app.py │ ├── components │ ├── DatasetsWidget.py │ ├── FilterWidget.py │ ├── ObjectInspectorWidget.py │ ├── TabViewWidget.py │ ├── ToolboxesWidget.py │ ├── ViewWidget.py │ ├── __init__.py │ ├── customWidgets.py │ ├── toolboxStandardItem.py │ ├── toolboxTreeWidgetItem.py │ └── widgetGenerator.py │ ├── graphics │ ├── QVTKRenderWindowInteractor.py │ ├── QVTKWidget.py │ ├── QVTKWindow.py │ └── __init__.py │ ├── resources │ ├── about.md │ ├── conf │ │ └── config_toolboxes.yaml │ ├── icons │ │ ├── Toolbox-50.png │ │ ├── branch-closed.png │ │ ├── branch-end.png │ │ ├── branch-more.png │ │ ├── branch-open.png │ │ ├── clean.png │ │ ├── clone.png │ │ ├── pcloudpy-name.png │ │ ├── pcloudpy.png │ │ ├── pqDelete24.png │ │ ├── pqExtractSelection.png │ │ ├── pqEyeball16.png │ │ ├── pqEyeballd16.png │ │ ├── pqFilter32.png │ │ ├── pqResetCamera24.png │ │ ├── pqResetCamera32.png │ │ ├── pqSelect32.png │ │ ├── pqSelectSurfPoints24.png │ │ ├── pqServer16.png │ │ ├── pqXMinus24.png │ │ ├── pqXPlus24.png │ │ ├── pqYMinus24.png │ │ ├── pqYPlus24.png │ │ ├── pqZMinus24.png │ │ ├── pqZPlus24.png │ │ ├── toolbox-icon.png │ │ ├── trash_24.png │ │ ├── trash_red_24.png │ │ └── vline.png │ └── resources.qrc │ ├── resources_rc.py │ ├── shell │ ├── CodeEdit.py │ ├── IPythonConsole.py │ ├── PythonConsole.py │ └── __init__.py │ └── utils │ ├── __init__.py │ └── qhelpers.py ├── requirements.txt ├── resources ├── pcloudpy.png ├── pcloudpy_tunnel.png └── pcloudpy_v0.10.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea 3 | *.iml 4 | out 5 | gen 6 | /build 7 | /dist 8 | .egg-info -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | The BSD 3-Clause License 2 | 3 | Copyright (c) 2015, Miguel Molero-Armenta 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 17 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 20 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 21 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## pcloudpy 2 | 3 | Point Cloud Viewer and Processing Toolkit in Python 4 | 5 | 6 | This toolkit aims at providing an ease interface to display point clouds in many formats and performing diverse filtering processes. 7 | This project goal is to make use of amazing libraries such as numpy, scipy, matplotlib, IPython, VTK, pandas, sklearn, laspy, pyside, pyqode and so on to be used in the display and processing of point clouds. 8 | **pcloudpy** project is highly inspired in Paraview, MeshLab, CloudCompare, FreeCad, PCL. It attempts to offer an alternative to add modules written in python easily. 9 | An IPython Console and a Python Editor are also available in order to interact with the pcloudpy api and graphical user interface. 10 | 11 | 12 | ------- 13 | 14 | The main features of the pcloudpy module are the following: 15 | 16 | - Display dense point clouds 17 | - Selection and Cleaning of point clouds 18 | - Filtering of point clouds (point neighborhood statistics, Outlier removal) 19 | - Extent Extraction from Point Clouds 20 | - Delaunay Triangulations 21 | - Normals Estimation (PCA Eigen Method & Oriented) 22 | - Screened Poisson Surface Reconstruction 23 | - Interaction with pcloudpy modules from an embedded IPython Console 24 | 25 | ------- 26 | 27 | ![](https://github.com/mmolero/pcloudpy/blob/master/resources/pcloudpy_v0.10.png) 28 | 29 | ------ 30 | 31 | Requirements: 32 | 33 | - VTK==5.10.1 34 | 35 | - GDAL>=1.11.0 36 | 37 | - markdown2>=2.3.0 38 | 39 | - laspy>=1.2.5 40 | 41 | - PyYAML>=3.11 42 | 43 | - pyqode.core>=2.6.6 44 | 45 | - pyqode.qt>=2.6.0 46 | 47 | - pyqode.python>=2.6.3 48 | 49 | - pypoisson, see https://github.com/mmolero/pypoisson 50 | 51 | 52 | 53 | ----- 54 | 55 | if you are interesting in collaborating and/or testing this software, please don't hesitate to contact me 56 | 57 | ------ 58 | 59 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main point 3 | """ 4 | #Author: Miguel Molero 5 | # License: BSD 3 clause 6 | 7 | 8 | from pcloudpy import app 9 | 10 | #launch Graphical User Interface 11 | app.run() 12 | -------------------------------------------------------------------------------- /pcloudpy/__init__.py: -------------------------------------------------------------------------------- 1 | from pcloudpy.gui import app 2 | -------------------------------------------------------------------------------- /pcloudpy/core/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pcloudpy/core/base.py: -------------------------------------------------------------------------------- 1 | """Base Class for all Custom Objects, highly based on sklearn Base Class""" 2 | #Author: Miguel Molero 3 | 4 | import inspect 5 | import six 6 | import numpy as np 7 | 8 | def _pprint(params, offset=0, printer=repr): 9 | """Pretty print the dictionary 'params' 10 | Parameters 11 | ---------- 12 | params: dict 13 | The dictionary to pretty print 14 | offset: int 15 | The offset in characters to add at the begin of each line. 16 | printer: 17 | The function to convert entries to strings, typically 18 | the builtin str or repr 19 | """ 20 | # Do a multi-line justified repr: 21 | options = np.get_printoptions() 22 | np.set_printoptions(precision=5, threshold=64, edgeitems=2) 23 | params_list = list() 24 | this_line_length = offset 25 | line_sep = ',\n' + (1 + offset // 2) * ' ' 26 | for i, (k, v) in enumerate(sorted(six.iteritems(params))): 27 | if type(v) is float: 28 | # use str for representing floating point numbers 29 | # this way we get consistent representation across 30 | # architectures and versions. 31 | this_repr = '%s=%s' % (k, str(v)) 32 | else: 33 | # use repr of the rest 34 | this_repr = '%s=%s' % (k, printer(v)) 35 | if len(this_repr) > 500: 36 | this_repr = this_repr[:300] + '...' + this_repr[-100:] 37 | if i > 0: 38 | if (this_line_length + len(this_repr) >= 75 or '\n' in this_repr): 39 | params_list.append(line_sep) 40 | this_line_length = len(line_sep) 41 | else: 42 | params_list.append(', ') 43 | this_line_length += 2 44 | params_list.append(this_repr) 45 | this_line_length += len(this_repr) 46 | 47 | np.set_printoptions(**options) 48 | lines = ''.join(params_list) 49 | # Strip trailing space to avoid nightmare in doctests 50 | lines = '\n'.join(l.rstrip(' ') for l in lines.split('\n')) 51 | return lines 52 | 53 | 54 | ############################################################################### 55 | 56 | class BaseObject(object): 57 | """Base Class for all the Custom Objects in pcloudpy, highly inspired by sklearn Base Class 58 | 59 | Notes 60 | ----- 61 | All Custom Objects should specify all the parameters that can be set 62 | at the class level in their ''__init__'' as explicit keyword arguments 63 | (no ''*args'' pr ''**kwargs). 64 | """ 65 | 66 | @classmethod 67 | def _get_param_names(cls): 68 | 69 | init = cls.__init__ 70 | if init is object.__init__: 71 | # No explicit constructor to introspect 72 | return [] 73 | 74 | args, varargs, kw, default = inspect.getargspec(cls.__init__) 75 | if varargs is not None: 76 | raise RuntimeError("pcloudpy objects should always " 77 | "specify their parameters in the signature" 78 | "of their __init__ (no varargs)." 79 | " %s doesn't follow this convention." 80 | % (cls, )) 81 | 82 | args.pop(0) 83 | args.sort() 84 | return args 85 | 86 | 87 | 88 | def get_params(self, deep=True): 89 | """Get parameters for this object. 90 | 91 | Parameters 92 | ---------- 93 | deep: boolean, optional 94 | If True, will return the parameters for this pcloudpy Object and 95 | contained sub-objects that are pcloudpy Objects. 96 | 97 | Returns 98 | ------- 99 | params : mapping of string to any 100 | Parameter names mapped to their values. 101 | """ 102 | 103 | out = dict() 104 | for key in self._get_param_names(): 105 | 106 | value = getattr(self, key, None) 107 | 108 | if deep and hasattr(value, 'get_params'): 109 | deep_items = value.get_params().items() 110 | out.update((key + '__' + k, val) for k, val in deep_items) 111 | out[key] = value 112 | return out 113 | 114 | 115 | 116 | def set_params(self, **params): 117 | """Set the parameters of this Object. 118 | The method works on simple pcloudpy Objects. 119 | 120 | Returns 121 | ------- 122 | self 123 | """ 124 | 125 | if not params: 126 | # Simple optimisation to gain speed (inspect is slow) 127 | return self 128 | 129 | valid_params = self.get_params(deep=True) 130 | for key, value in six.iteritems(params): 131 | split = key.split('__', 1) 132 | if len(split) > 1: 133 | # nested objects case 134 | name, sub_name = split 135 | if name not in valid_params: 136 | raise ValueError('Invalid parameter %s for object %s' % 137 | (name, self)) 138 | sub_object = valid_params[name] 139 | sub_object.set_params(**{sub_name: value}) 140 | else: 141 | # simple objects case 142 | if key not in valid_params: 143 | raise ValueError('Invalid parameter %s ' 'for object %s' 144 | % (key, self.__class__.__name__)) 145 | setattr(self, key, value) 146 | return self 147 | 148 | def __repr__(self): 149 | class_name = self.__class__.__name__ 150 | return '%s(%s)' % (class_name, _pprint(self.get_params(deep=False), 151 | offset=len(class_name),),) 152 | 153 | -------------------------------------------------------------------------------- /pcloudpy/core/filters/Delaunay2D.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | 4 | from vtk import vtkDelaunay2D, vtkPolyData 5 | from pcloudpy.core.filters.base import FilterBase 6 | 7 | class Delaunay2D(FilterBase): 8 | """ 9 | 2D Delaunay triangulation method 10 | Delaunay2d is a filter that constructs a 2D Delaunay triangulation from a vtkPolyData object 11 | 12 | Parameters 13 | ---------- 14 | PolyData: vtkPolyData 15 | An instance of vtkPolyData. 16 | 17 | alpha: float 18 | Specify alpha (or distance) value to control output of this filter. 19 | For a non-zero alpha value, only edges or triangles contained within a sphere centered at mesh vertices will be output. 20 | Otherwise, only triangles will be output. , optional. (by default = 1.0.) 21 | 22 | tolerance: float 23 | tolerance to control discarding of closely spaced points, optional. (by default = 0.) 24 | 25 | 26 | Returns 27 | ------- 28 | vtkPolyData 29 | 2D Delaunay triangulation 30 | """ 31 | 32 | def __init__(self, alpha, tolerance): 33 | super(Delaunay2D, self).__init__() 34 | self.alpha = alpha 35 | self.tolerance = tolerance 36 | 37 | def set_input(self, input_data): 38 | if isinstance(input_data, vtkPolyData): 39 | super(Delaunay2D, self).set_input(input_data) 40 | return True 41 | else: 42 | return False 43 | 44 | def update(self): 45 | delaunay = vtkDelaunay2D() 46 | delaunay.SetInputData(self.input_) 47 | delaunay.SetTolerance(self.tolerance) 48 | delaunay.SetAlpha(self.alpha) 49 | delaunay.Update() 50 | self.output_ = delaunay.GetOutput() 51 | -------------------------------------------------------------------------------- /pcloudpy/core/filters/Delaunay3D.py: -------------------------------------------------------------------------------- 1 | from vtk import vtkDelaunay3D, vtkGeometryFilter, vtkTriangleFilter, vtkPolyData 2 | 3 | from pcloudpy.core.filters.base import FilterBase 4 | 5 | class Delaunay3D(FilterBase): 6 | """ 7 | 3D Delaunay triangulation method 8 | Delaunay3d is a filter that constructs a 3D Delaunay triangulation from a vtkPolyData object 9 | 10 | Parameters 11 | ---------- 12 | PolyData: vtkPolyData 13 | An instance of vtkPolyData. 14 | 15 | alpha: float 16 | Specify alpha (or distance) value to control output of this filter. 17 | For a non-zero alpha value, only edges or triangles contained within a sphere centered at mesh vertices will be output. 18 | Otherwise, only triangles will be output. , optional. (by default = 1.0.) 19 | 20 | tolerance: float 21 | tolerance to control discarding of closely spaced points, optional. (by default = 0.) 22 | 23 | 24 | Returns 25 | ------- 26 | vtkPolyData 27 | 2D Delaunay triangulation 28 | """ 29 | 30 | def __init__(self, alpha, tolerance): 31 | super(Delaunay3D, self).__init__() 32 | self.alpha = alpha 33 | self.tolerance = tolerance 34 | 35 | def set_input(self, input_data): 36 | if isinstance(input_data, vtkPolyData): 37 | super(Delaunay3D, self).set_input(input_data) 38 | return True 39 | else: 40 | return False 41 | 42 | def update(self): 43 | delaunay = vtkDelaunay3D() 44 | delaunay.SetInputData(self.input_) 45 | delaunay.SetTolerance(self.tolerance) 46 | delaunay.SetAlpha(self.alpha) 47 | delaunay.Update() 48 | 49 | geom = vtkGeometryFilter() 50 | geom.SetInputConnection(delaunay.GetOutputPort() ) 51 | 52 | triangle = vtkTriangleFilter() 53 | triangle.SetInputConnection(geom.GetOutputPort()) 54 | triangle.Update() 55 | self.output_ = triangle.GetOutput() 56 | -------------------------------------------------------------------------------- /pcloudpy/core/filters/DisplayNormals.py: -------------------------------------------------------------------------------- 1 | from vtk import vtkArrowSource, vtkGlyph3D, vtkPolyData 2 | from vtk import vtkRenderer, vtkRenderWindowInteractor, vtkPolyDataMapper, vtkActor, vtkRenderWindow 3 | 4 | 5 | from pcloudpy.core.filters.base import FilterBase 6 | 7 | class DisplayNormals(FilterBase): 8 | 9 | def __init__(self): 10 | super(DisplayNormals, self).__init__() 11 | 12 | def set_input(self, input_data): 13 | if isinstance(input_data, vtkPolyData): 14 | super(DisplayNormals, self).set_input(input_data) 15 | return True 16 | else: 17 | return False 18 | 19 | def update(self): 20 | # Source for the glyph filter 21 | arrow = vtkArrowSource() 22 | arrow.SetTipResolution(8) 23 | arrow.SetTipLength(0.3) 24 | arrow.SetTipRadius(0.1) 25 | 26 | glyph = vtkGlyph3D() 27 | glyph.SetSourceConnection(arrow.GetOutputPort()) 28 | glyph.SetInputData(self.input_) 29 | glyph.SetVectorModeToUseNormal() 30 | glyph.SetScaleFactor(0.1) 31 | #glyph.SetColorModeToColorByVector() 32 | #glyph.SetScaleModeToScaleByVector() 33 | glyph.OrientOn() 34 | glyph.Update() 35 | 36 | self.output_ = glyph.GetOutput() 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /pcloudpy/core/filters/ExtractPolyData.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | """ 4 | from vtk import vtkExtractUnstructuredGrid, vtkGeometryFilter, vtkAppendFilter, vtkPolyData, vtkCleanPolyData 5 | from pcloudpy.core.filters.base import FilterBase 6 | 7 | class ExtractPolyData(FilterBase): 8 | """ 9 | Extent Extraction from a Point Cloud 10 | 11 | Parameters 12 | ---------- 13 | 14 | extent: tuple, shape=6, 15 | Extent = (xmin, xmax, ymin, ymax, zmin, zmax) 16 | 17 | """ 18 | def __init__(self, extent): 19 | super(ExtractPolyData, self).__init__() 20 | self.extent = extent 21 | 22 | def set_input(self, input_data): 23 | if isinstance(input_data, vtkPolyData): 24 | super(ExtractPolyData, self).set_input(input_data) 25 | return True 26 | else: 27 | return False 28 | 29 | def update(self): 30 | """ 31 | 32 | """ 33 | 34 | appendFilter = vtkAppendFilter() 35 | appendFilter.AddInput(self.input_) 36 | appendFilter.Update() 37 | 38 | extractGrid = vtkExtractUnstructuredGrid() 39 | extractGrid.SetInputData(appendFilter.GetOutput()) 40 | extractGrid.SetExtent(self.extent[0], self.extent[1], self.extent[2], self.extent[3], self.extent[4], self.extent[5]) 41 | 42 | geom = vtkGeometryFilter() 43 | geom.SetInputConnection(extractGrid.GetOutputPort() ) 44 | geom.Update() 45 | 46 | clean = vtkCleanPolyData() 47 | clean.PointMergingOn() 48 | clean.SetTolerance(0.01) 49 | clean.SetInput(geom.GetOutput()) 50 | clean.Update() 51 | 52 | self.output_ = clean.GetOutput() 53 | 54 | -------------------------------------------------------------------------------- /pcloudpy/core/filters/NormalsEstimation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class that define a normal estimation method based on PCA Eigen method to fit plane 3 | """ 4 | __all__ = ["NormalsEstimation"] 5 | 6 | import numpy as np 7 | from scipy.linalg import eigh 8 | from sklearn.neighbors import NearestNeighbors 9 | 10 | from pcloudpy.core.filters.base import FilterBase 11 | from ..io.converters import numpy_from_polydata, copy_polydata_add_normals 12 | 13 | 14 | 15 | class NormalsEstimation(FilterBase): 16 | """ 17 | NormalEstimation filter estimates normals of a point cloud using PCA Eigen method to fit plane 18 | 19 | Parameters 20 | ---------- 21 | 22 | number_neighbors: int 23 | number of neighbors to be considered in the normals estimation 24 | 25 | 26 | Attributes 27 | ---------- 28 | input_: vtkPolyData 29 | Input Data to be filtered 30 | 31 | output_: vtkPolyData 32 | Output Data 33 | 34 | """ 35 | 36 | def __init__(self, number_neighbors = 10): 37 | self.number_neighbors = number_neighbors 38 | 39 | def update(self): 40 | 41 | array_with_color = numpy_from_polydata(self.input_) 42 | normals = np.empty_like(array_with_color[:,0:3]) 43 | coord = array_with_color[:,0:3] 44 | 45 | neigh = NearestNeighbors(self.number_neighbors) 46 | neigh.fit(coord) 47 | 48 | for i in range(0,len(coord)): 49 | #Determine the neighbours of point 50 | d = neigh.kneighbors(coord[i]) 51 | #Add coordinates of neighbours , dont include center point to array. Determine coordinate by the index of the neighbours. 52 | y = np.zeros((self.number_neighbors-1,3)) 53 | y = coord[d[1][0][1:self.number_neighbors],0:3] 54 | #Get information content 55 | #Assign information content to each point i.e xyzb 56 | normals[i,0:3] = self.get_normals(y) 57 | 58 | self.output_ = copy_polydata_add_normals(self.input_, normals) 59 | 60 | 61 | def get_normals(self, XYZ): 62 | #The below code uses the PCA Eigen method to fit plane. 63 | #Get the covariance matrix 64 | average = np.sum(XYZ, axis=0)/XYZ.shape[0] 65 | b = np.transpose(XYZ - average) 66 | cov = np.cov(b) 67 | #Get eigen val and vec 68 | e_val,e_vect = eigh(cov, overwrite_a=True, overwrite_b=True) 69 | norm = e_vect[:,0] 70 | return norm -------------------------------------------------------------------------------- /pcloudpy/core/filters/OrientedNormalEstimation.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | """ 5 | Class that define oriented normal estimation method based on PCA Eigen method to fit plane and minimum spanning tree 6 | 7 | """ 8 | __all__ = ["OrientedNormalsEstimation"] 9 | 10 | import numpy as np 11 | from scipy.linalg import eigh 12 | from sklearn.neighbors import NearestNeighbors 13 | import networkx as nx 14 | 15 | from pcloudpy.core.filters.base import FilterBase 16 | from ..io.converters import numpy_from_polydata, copy_polydata_add_normals 17 | 18 | 19 | class OrientedNormalsEstimation(FilterBase): 20 | """ 21 | NormalEstimation filter estimates normals of a point cloud using PCA Eigen method to fit plane 22 | 23 | Parameters 24 | ---------- 25 | 26 | number_neighbors: int 27 | number of neighbors to be considered in the normals estimation 28 | 29 | 30 | Attributes 31 | ---------- 32 | input_: vtkPolyData 33 | Input Data to be filtered 34 | 35 | output_: vtkPolyData 36 | Output Data 37 | 38 | 39 | 40 | """ 41 | 42 | def __init__(self, number_neighbors = 10): 43 | 44 | self.number_neighbors = number_neighbors 45 | 46 | 47 | def update(self): 48 | 49 | array_with_color = numpy_from_polydata(self.input_) 50 | normals = np.empty_like(array_with_color[:,0:3]) 51 | coord = array_with_color[:,0:3] 52 | 53 | neigh = NearestNeighbors(self.number_neighbors) 54 | neigh.fit(coord) 55 | 56 | for i in range(0,len(coord)): 57 | #Determine the neighbours of point 58 | d = neigh.kneighbors(coord[i]) 59 | #Add coordinates of neighbours , dont include center point to array. Determine coordinate by the index of the neighbours. 60 | y = np.zeros((self.number_neighbors-1,3)) 61 | y = coord[d[1][0][1:self.number_neighbors],0:3] 62 | #Get information content 63 | #Assign information content to each point i.e xyzb 64 | normals[i,0:3] = self.get_normals(y) 65 | 66 | #Get the point with highest z value , this will be used as the starting point for my depth search 67 | z_max_point = np.where(coord[:,2]== np.max(coord[:,2])) 68 | z_max_point = int(z_max_point[0]) 69 | 70 | if normals[z_max_point,2] < 0 : #ie normal doesnt point out 71 | normals[z_max_point,:]=-normals[z_max_point,:] 72 | 73 | #Create a graph 74 | G = nx.Graph() 75 | 76 | #Add all points and there neighbours to graph, make the weight equal to the distance between points 77 | for i in range(0,len(coord)): 78 | 79 | d = neigh.kneighbors(coord[i,:3]) 80 | for c in range(1,self.number_neighbors): 81 | p1 = d[1][0][0] 82 | p2 = d[1][0][c] 83 | n1 = normals[d[1][0][0],:] 84 | n2 = normals[d[1][0][c],:] 85 | dot = np.dot(n1,n2) 86 | G.add_edge(p1,p2,weight =1-np.abs(dot)) 87 | 88 | 89 | T = nx.minimum_spanning_tree(G) 90 | 91 | x=[] 92 | for i in nx.dfs_edges(T,z_max_point): 93 | x+=i 94 | 95 | 96 | inds = np.where(np.diff(x))[0] 97 | out = np.split(x,inds[np.diff(inds)==1][1::2]+1) 98 | 99 | for j in range(0,len(out)): 100 | for i in range(0,len(out[j])-1): 101 | n1 = normals[out[j][i],:] 102 | n2 = normals[out[j][i+1],:] 103 | if np.dot(n2,n1)<0: 104 | normals[out[j][i+1],:]=-normals[out[j][i+1],:] 105 | 106 | 107 | self.output_ = copy_polydata_add_normals(self.input_, normals) 108 | 109 | 110 | def get_normals(self, XYZ): 111 | 112 | #The below code uses the PCA Eigen method to fit plane. 113 | #Get the covariance matrix 114 | average = np.sum(XYZ, axis=0)/XYZ.shape[0] 115 | b = np.transpose(XYZ - average) 116 | cov = np.cov(b) 117 | #Get eigen val and vec 118 | e_val,e_vect = eigh(cov, overwrite_a=True, overwrite_b=True) 119 | norm = e_vect[:,0] 120 | return norm -------------------------------------------------------------------------------- /pcloudpy/core/filters/ScreenedPoisson.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from vtk import vtkPolyData 3 | from pcloudpy.core.filters.base import FilterBase 4 | from ..io.converters import get_points_normals_from, get_polydata_from 5 | 6 | from pypoisson import poisson_reconstruction 7 | 8 | 9 | class ScreenedPoisson(FilterBase): 10 | 11 | 12 | def __init__(self, depth=8, full_depth=5, scale=1.1, samples_per_node=1.0, cg_depth=0.0, 13 | enable_polygon_mesh=False, enable_density=False): 14 | 15 | super(ScreenedPoisson, self).__init__() 16 | self.depth= depth 17 | self.full_depth = full_depth 18 | self.scale = scale 19 | self.samples_per_node=samples_per_node 20 | self.cg_depth=cg_depth 21 | self.enable_polygon_mesh=enable_polygon_mesh 22 | self.enable_density = enable_density 23 | 24 | def set_input(self, input_data): 25 | """ 26 | set input data 27 | 28 | Parameters 29 | ---------- 30 | input-data : vtkPolyData 31 | 32 | Returns 33 | ------- 34 | is_valid: bool 35 | Returns True if the input_data is valid for processing 36 | 37 | """ 38 | if isinstance(input_data, vtkPolyData): 39 | super(ScreenedPoisson, self).set_input(input_data) 40 | return True 41 | else: 42 | return False 43 | 44 | 45 | def update(self): 46 | 47 | points, normals = get_points_normals_from(self.input_) 48 | faces, vertices = poisson_reconstruction(points, normals, 49 | depth=self.depth) 50 | 51 | self.output_ = get_polydata_from(vertices, faces) 52 | 53 | 54 | -------------------------------------------------------------------------------- /pcloudpy/core/filters/StatisticalOutlierRemovalFilter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Statistical Outlier Removal Filter uses point neighborhood statistics to filter outlier data. 3 | """ 4 | #Author: Miguel Molero 5 | # License: BSD 3 clause 6 | 7 | 8 | import numpy as np 9 | 10 | from vtk import vtkPoints, vtkCellArray, vtkPolyData 11 | from sklearn.neighbors import KDTree 12 | 13 | from ..utils.vtkhelpers import actor_from_imagedata, actor_from_polydata 14 | from ..io.converters import numpy_from_polydata, polydata_from_numpy 15 | from pcloudpy.core.filters.base import FilterBase 16 | 17 | class StatisticalOutlierRemovalFilter(FilterBase): 18 | """ 19 | 20 | Statistical Outlier Removal Filter uses point neighborhood statistics to filter outlier data. 21 | Statistical Outlier Removal Filter is a python implementation based on the pcl::StatisticalOutlierRemoval from Point Cloud Library 22 | 23 | The algorithm iterates through the entire input twice. 24 | 25 | During the first iteration it will update the average distance that each point has to its nearest k neighbors. 26 | The value of k can be set using mean_k. 27 | Next, the mean and standard deviation of all these distances are computed in order to determine a distance threshold. 28 | The distance threshold will be equal to: mean + std_dev * stddev. 29 | The multiplier for the standard deviation can be set using std_dev. 30 | 31 | During the next iteration the points will be classified as inlier or outlier if their average neighbor distance is below or above this threshold respectively 32 | 33 | 34 | Parameters 35 | ---------- 36 | mean_k : float 37 | The number of points to use for mean distance estimation. 38 | 39 | std_dev: float 40 | The standard deviation multiplier. 41 | The distance threshold will be equal to: mean + stddev_mult * stddev. 42 | Points will be classified as inlier or outlier if their average neighbor distance is below or above this threshold respectively. 43 | 44 | 45 | Notes 46 | ----- 47 | For more information. 48 | 49 | class pcl::StatisticalOutlierRemoval (Radu Bogdan Rusu) 50 | 51 | R. B. Rusu, Z. C. Marton, N. Blodow, M. Dolha, and M. Beetz. 52 | Towards 3D Point Cloud Based Object Maps for Household Environments Robotics and Autonomous Systems Journal (Special Issue on Semantic Knowledge), 2008. 53 | 54 | 55 | """ 56 | 57 | def __init__(self, mean_k, std_dev): 58 | super(StatisticalOutlierRemovalFilter, self).__init__() 59 | self.mean_k = mean_k 60 | self.std_dev = std_dev 61 | 62 | def set_input(self, input_data): 63 | """ 64 | set input data 65 | 66 | Parameters 67 | ---------- 68 | input-data : vtkPolyData 69 | 70 | 71 | Returns 72 | ------- 73 | is_valid: bool 74 | Returns True if the input_data is valid for processing 75 | 76 | """ 77 | if isinstance(input_data, vtkPolyData): 78 | super(StatisticalOutlierRemovalFilter, self).set_input(input_data) 79 | return True 80 | else: 81 | return False 82 | 83 | def set_mean_k(self, value): 84 | self.mean_k = value 85 | 86 | def set_std_dev(self, value): 87 | self.std_dev = value 88 | 89 | def update(self): 90 | """ 91 | Compute filter. 92 | """ 93 | 94 | array_full = numpy_from_polydata(self.input_) 95 | 96 | array = array_full[:,0:3] 97 | color = array_full[:,3:] 98 | 99 | #KDTree object (sklearn) 100 | kDTree = KDTree(array, leaf_size = 5) 101 | dx, idx_knn = kDTree.query(array[:, :], k = self.mean_k + 1) 102 | dx, idx_knn = dx[:,1:], idx_knn[:,1:] 103 | 104 | den = (self.mean_k - 1.0) 105 | if den == 0: 106 | den = 1 107 | distances = np.sum(dx, axis=1)/den 108 | valid_distances = np.shape(distances)[0] 109 | 110 | #Estimate the mean and the standard deviation of the distance vector 111 | sum = np.sum(distances) 112 | sq_sum = np.sum(distances**2) 113 | 114 | mean = sum / float(valid_distances) 115 | variance = (sq_sum - sum * sum / float(valid_distances)) / (float(valid_distances) - 1) 116 | stddev = np.sqrt (variance) 117 | 118 | # a distance that is bigger than this signals an outlier 119 | distance_threshold = mean + self.std_dev * stddev 120 | idx = np.nonzero(distances < distance_threshold) 121 | new_array = np.copy(array[idx]) 122 | new_color = np.copy(color[idx]) 123 | 124 | output = polydata_from_numpy(np.c_[new_array, new_color]) 125 | self.output_ = output 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /pcloudpy/core/filters/__init__.py: -------------------------------------------------------------------------------- 1 | from pcloudpy.core.filters.StatisticalOutlierRemovalFilter import StatisticalOutlierRemovalFilter 2 | from pcloudpy.core.filters.Delaunay2D import Delaunay2D 3 | from pcloudpy.core.filters.Delaunay3D import Delaunay3D 4 | from pcloudpy.core.filters.ExtractPolyData import ExtractPolyData 5 | 6 | from pcloudpy.core.filters.NormalsEstimation import NormalsEstimation 7 | from pcloudpy.core.filters.OrientedNormalEstimation import OrientedNormalsEstimation 8 | 9 | from pcloudpy.core.filters.vtkPointSetNormalsEstimation import vtkPointSetNormalsEstimation 10 | from pcloudpy.core.filters.vtkPointSetOutlierRemoval import vtkPointSetOutlierEstimation 11 | 12 | 13 | from pcloudpy.core.filters.DisplayNormals import DisplayNormals 14 | from pcloudpy.core.filters.ScreenedPoisson import ScreenedPoisson 15 | 16 | 17 | -------------------------------------------------------------------------------- /pcloudpy/core/filters/base.py: -------------------------------------------------------------------------------- 1 | """Base class for all filters.""" 2 | #Author: Miguel Molero 3 | # License: BSD 3 clause 4 | 5 | from vtk import vtkPolyData 6 | 7 | from ..base import BaseObject 8 | 9 | class FilterBase(BaseObject): 10 | """ 11 | Base Class for all Filter Objects 12 | 13 | Attributes 14 | ---------- 15 | input_: vtkPolyData 16 | Input Data to be filtered 17 | 18 | output_: vtkPolyData 19 | Output Data 20 | 21 | """ 22 | 23 | def __init__(self): 24 | 25 | self.input_ = None 26 | self.output_ = None 27 | 28 | def set_input(self, input_data): 29 | 30 | """ 31 | set input data 32 | 33 | Parameters 34 | ---------- 35 | input-data : vtkPolyData 36 | 37 | Returns 38 | ------- 39 | is_valid: bool 40 | Returns True if the input_data is valid for processing 41 | 42 | """ 43 | if isinstance(input_data, vtkPolyData): 44 | self.input_ = input_data 45 | return True 46 | else: 47 | return False 48 | 49 | 50 | def get_output(self): 51 | 52 | if self.output_: 53 | return self.output_ 54 | 55 | def update(self): 56 | pass 57 | 58 | -------------------------------------------------------------------------------- /pcloudpy/core/filters/vtkPointSetNormalsEstimation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class that define a normal estimation method based on vtkPointSetNormalsEstimation by David Doria 3 | """ 4 | __all__ = ["vtkPointSetNormalsEstimation"] 5 | from pcloudpy.core.filters.base import FilterBase 6 | 7 | import numpy as np 8 | from vtk import vtkPolyDataAlgorithm, vtkFloatArray, vtkKdTree, vtkPlane, vtkPolyData, vtkIdList 9 | from scipy.linalg import eigh 10 | 11 | FIXED_NUMBER = 0 12 | RADIUS = 1 13 | 14 | class vtkPointSetNormalsEstimation(FilterBase): 15 | """ 16 | vtkPointSetNormalEstimation filter estimates normals of a point set using a local best fit plane. 17 | 18 | At every point in the point set, vtkPointSetNormalEstimation computes the best 19 | fit plane of the set of points within a specified radius of the point (or a fixed number of neighbors). 20 | The normal of this plane is used as an estimate of the normal of the surface that would go through 21 | the points. 22 | 23 | vtkPointSetNormalEstimation Class is a python implementation based on the version included in PointSetProcessing by 24 | David Doria, see https://github.com/daviddoria/PointSetProcessing 25 | """ 26 | 27 | def __init__(self, number_neighbors = 10, mode = 0, radius = 1): 28 | 29 | self.mode = mode #FIXED_NUMBER 30 | self.number_neighbors = number_neighbors 31 | self.radius = radius 32 | 33 | 34 | def set_mode_to_fixednumber(self): 35 | self.mode = FIXED_NUMBER 36 | 37 | def set_mode_to_radius(self): 38 | self.mode = RADIUS 39 | 40 | def set_input(self, input_data): 41 | """ 42 | set input data 43 | 44 | Parameters 45 | ---------- 46 | input-data : vtkPolyData 47 | 48 | 49 | Returns 50 | ------- 51 | is_valid: bool 52 | Returns True if the input_data is valid for processing 53 | 54 | """ 55 | if isinstance(input_data, vtkPolyData): 56 | super(vtkPointSetNormalsEstimation, self).set_input(input_data) 57 | return True 58 | else: 59 | return False 60 | 61 | 62 | 63 | def update(self): 64 | 65 | normalArray = vtkFloatArray() 66 | normalArray.SetNumberOfComponents( 3 ) 67 | normalArray.SetNumberOfTuples( self.input_.GetNumberOfPoints() ) 68 | normalArray.SetName( "Normals" ) 69 | 70 | kDTree = vtkKdTree() 71 | kDTree.BuildLocatorFromPoints(self.input_.GetPoints()) 72 | 73 | # Estimate the normal at each point. 74 | for pointId in range(0, self.input_.GetNumberOfPoints()): 75 | 76 | point = [0,0,0] 77 | self.input_.GetPoint(pointId, point) 78 | neighborIds = vtkIdList() 79 | 80 | if self.mode == FIXED_NUMBER: 81 | kDTree.FindClosestNPoints(self.number_neighbors, point, neighborIds) 82 | 83 | elif self.mode == RADIUS: 84 | kDTree.FindPointsWithinRadius(self.radius, point, neighborIds) 85 | #If there are not at least 3 points within the specified radius (the current 86 | # #point gets included in the neighbors set), a plane is not defined. Instead, 87 | # #force it to use 3 points. 88 | if neighborIds.GetNumberOfIds() < 3 : 89 | kDTree.FindClosestNPoints(3, point, neighborIds) 90 | 91 | bestPlane = vtkPlane() 92 | self.best_fit_plane(self.input_.GetPoints(), bestPlane, neighborIds) 93 | 94 | normal = bestPlane.GetNormal() 95 | normalArray.SetTuple( pointId, normal ) 96 | 97 | self.output_ = vtkPolyData() 98 | self.output_.ShallowCopy(self.input_) 99 | self.output_.GetPointData().SetNormals(normalArray) 100 | 101 | def best_fit_plane(self, points, bestPlane, idsToUse): 102 | 103 | #Compute the best fit (least squares) plane through a set of points. 104 | dnumPoints = idsToUse.GetNumberOfIds() 105 | 106 | #Find the center of mass of the points 107 | center = self.center_of_mass(points, idsToUse) 108 | 109 | a = np.zeros((3,3)) 110 | for pointId in range(0, dnumPoints): 111 | 112 | x = np.asarray([0,0,0]) 113 | xp = np.asarray([0,0,0]) 114 | points.GetPoint(idsToUse.GetId(pointId), x) 115 | xp = x - center 116 | a[0,:] += xp[0] * xp[:] 117 | a[1,:] += xp[1] * xp[:] 118 | a[2,:] += xp[2] * xp[:] 119 | 120 | #Divide by N-1 121 | a /= (dnumPoints-1) 122 | 123 | eigval, eigvec = eigh(a, overwrite_a=True, overwrite_b=True) 124 | #Jacobi iteration for the solution of eigenvectors/eigenvalues of a 3x3 real symmetric matrix. 125 | #Square 3x3 matrix a; output eigenvalues in w; and output eigenvectors in v. 126 | #Resulting eigenvalues/vectors are sorted in decreasing order; eigenvectors are normalized. 127 | #Set the plane normal to the smallest eigen vector 128 | bestPlane.SetNormal(eigvec[0,0], eigvec[1,0], eigvec[2,0]) 129 | #Set the plane origin to the center of mass 130 | bestPlane.SetOrigin(center[0], center[1], center[2]) 131 | 132 | 133 | def center_of_mass(self, points, idsToUse): 134 | #Compute the center of mass of a set of points. 135 | point = np.asarray([0.0, 0.0, 0.0]) 136 | center = np.asarray([0.0,0.0,0.0]) 137 | for i in range(0, idsToUse.GetNumberOfIds() ): 138 | points.GetPoint(idsToUse.GetId(i), point) 139 | center += point 140 | 141 | numberOfPoints = float(idsToUse.GetNumberOfIds()) 142 | center /= numberOfPoints 143 | return center 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /pcloudpy/core/filters/vtkPointSetOutlierRemoval.py: -------------------------------------------------------------------------------- 1 | 2 | __all__ = ["vtkPointSetOutlierEstimation"] 3 | 4 | import numpy as np 5 | from vtk import vtkPoints, vtkCellArray, vtkPolyData, vtkIdList 6 | from vtk import vtkKdTreePointLocator 7 | from vtk import vtkSphereSource, vtkXMLPolyDataWriter, vtkVertexGlyphFilter 8 | 9 | from sklearn.neighbors import KDTree 10 | from ..utils.vtkhelpers import actor_from_imagedata, actor_from_polydata 11 | from ..io.converters import numpy_from_polydata, polydata_from_numpy 12 | from pcloudpy.core.filters.base import FilterBase 13 | 14 | 15 | class vtkPointSetOutlierEstimation(FilterBase): 16 | """ 17 | Outlier Removal - vtkPointSetOutlierRemoval, 18 | Python implementation based on Point Set Processing for VTK by David Doria 19 | see https://github.com/daviddoria/PointSetProcessing 20 | http://www.vtkjournal.org/browse/publication/708 21 | 22 | We take the simple definition of an outlier to be a point that is farther away from its nearest neighbor than 23 | expected. To implement this definition, for every point p in the point set, we compute the distance from p to 24 | the nearest point to p. We sort these distances and keep points whose nearest point is in a certain percentile 25 | of the entire point set. This parameter is specified by the user as percent_to_remove 26 | 27 | """ 28 | 29 | def __init__(self, percent_to_remove): 30 | 31 | self.percent_to_remove = percent_to_remove 32 | 33 | def set_input(self, input_data): 34 | """ 35 | set input data 36 | 37 | Parameters 38 | ---------- 39 | input-data : vtkPolyData 40 | 41 | 42 | Returns 43 | ------- 44 | is_valid: bool 45 | Returns True if the input_data is valid for processing 46 | 47 | """ 48 | if isinstance(input_data, vtkPolyData): 49 | super(vtkPointSetOutlierEstimation, self).set_input(input_data) 50 | return True 51 | else: 52 | return False 53 | 54 | def set_percent_to_remove(self, value): 55 | self.percent_to_remove = value 56 | 57 | def update(self): 58 | 59 | array_full = numpy_from_polydata(self.input_) 60 | 61 | array = array_full[:,0:3] 62 | color = array_full[:,3:] 63 | 64 | #KDTree object (sklearn) 65 | kDTree = KDTree(array) 66 | dx, _ = kDTree.query(array[:, :], k = 2) 67 | dx = dx[:,1:].ravel() 68 | 69 | Indices = np.argsort(dx, axis=0) 70 | Npts = np.shape(Indices)[0] 71 | numberToKeep = int( (1 - self.percent_to_remove ) * Npts) 72 | 73 | idx = Indices[0:numberToKeep] 74 | 75 | new_array = np.copy(array[idx]) 76 | new_color = np.copy(color[idx]) 77 | array = np.c_[new_array, new_color] 78 | 79 | output = polydata_from_numpy(array) 80 | self.output_ = output 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /pcloudpy/core/grid.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def get_grid_from_extent(xmin, xmax, ymin, ymax, spacing_x=1.0, spacing_y=1.0): 5 | """ 6 | Returns a Regular grid defined for a given extent 7 | 8 | Parameters 9 | ---------- 10 | xmin: float 11 | minimum value of x 12 | 13 | xmax: float 14 | minimum value of x 15 | 16 | ymin:float 17 | minimum value of y 18 | 19 | ymax: float 20 | minimum value of y 21 | 22 | spacing_x: integer 23 | desired size for tile, x-axis (it should be an integer number). Value 1.0 by default. 24 | 25 | spacing_y: integer 26 | desired size for tile, y-axis (it should be an integer number). Value 1.0 by default. 27 | 28 | Returns 29 | ------- 30 | 31 | xi: array, shape = (N1, N2) where N1=len(xi_v) and N2=len(yi_v) 32 | xi is a N2 shaped array with the elements of xi_v repeated to fill the matrix along the first dimension. 33 | 34 | yi: array, shape = (N1, N2) where N1=len(xi_v) and N2=len(yi_v) 35 | yi is a N1 shaped array with the elements of yi_v repeated to fill the matrix along the second dimension. 36 | 37 | xi_v: array 38 | xi_v evenly spaced samples, calculated over the interval [xmin, xmax]. 39 | 40 | yi_v: array 41 | yi_v evenly spaced samples, calculated over the interval [ymin, ymax]. 42 | 43 | """ 44 | 45 | nx = int((xmax-xmin)/spacing_x) 46 | ny = int((ymax-ymin)/spacing_y) 47 | xi_v = np.linspace(xmin, xmax, nx) 48 | yi_v = np.linspace(ymin, ymax, ny) 49 | xi, yi = np.meshgrid(xi_v, yi_v) 50 | 51 | return xi, yi, xi_v, yi_v 52 | 53 | 54 | 55 | def get_division_tiles(xmin, xmax, spacing_tile): 56 | """ 57 | Divides the given interval [xmin, xmax] to tiles for a given spacing_tile 58 | 59 | If spacing_tile is bigger than the distance of the given interval [xmin, xmax], returns the same interval. 60 | If spacing_tile is smaller than the distance of the given interval [xmin, xmax], dividing the interval 61 | into as many tiles as possible with the given size. 62 | 63 | The last tile will have the residuary size. 64 | 65 | Examples: Interval = [0, 10] and size = 3 => result = [ 0, 3, 6, 9, 10] 66 | Interval = [0, 10] and size = 4 => result = [ 0, 4, 8, 10] 67 | 68 | Parameters 69 | ---------- 70 | 71 | xmin: Integer 72 | Interval Init (it should be a integer number) 73 | 74 | xmax: Integer 75 | Interval End (it should be a integer number) 76 | 77 | size: Integer 78 | Desired size between adjacent tiles (it should be an integer number) 79 | 80 | Returns 81 | ------- 82 | 83 | Returns the interval [xmin, xmax] divided into tiles for an given spacing_tile 84 | 85 | xi_v: array 86 | array of points that divides the interval [xmin, xman] into tiles 87 | """ 88 | 89 | num_tiles = int((xmax -xmin)/spacing_tile) 90 | 91 | if (xmax-xmin) < spacing_tile: 92 | xi_v = np.array([xmin,xmax]) 93 | else: 94 | xi_v = np.array([xmin+cont*spacing_tile for cont in range(num_tiles+1) ]) 95 | if xmax > xi_v[-1]: 96 | np.append(xi_v, xmax) 97 | return xi_v 98 | 99 | 100 | def get_splitting_intervals(xmin, xmax, ymin, ymax, spacing_tile, pixel_size): 101 | """ 102 | Divides the given extent [xmin, xmax, ymin, ymax] into tiles with the given size (spacing_tile) 103 | 104 | Parameters 105 | ---------- 106 | 107 | xmin: float 108 | origin of the interval on the x-axis 109 | 110 | xmax: float 111 | end of the interval on the x-axis 112 | 113 | ymin: float 114 | origin of the interval on the y-axis 115 | 116 | ymax: float 117 | end of the interval on the y-axis 118 | 119 | spacing_tile: integer 120 | Given spacing between adjacent tiles (it should be an integer number) 121 | 122 | pixel_size: integer 123 | Number of meters of one pixel in the final raster 124 | (it should be a integer number and pixelOverlap/pixelSize should be an integer number) 125 | 126 | Returns 127 | ------- 128 | 129 | Returns the interval [xmin, xmax, ymin, ymax] divided into tiles with the desired spacing_tile given 130 | 131 | rows: integer 132 | Number of rows in which the original image is divided 133 | 134 | columns: 135 | Number of columns in which the original image is divided 136 | 137 | xi_v: array 138 | array of points that divide the interval [xmin, xmax] into tiles 139 | 140 | yi_v: array 141 | array of points that divide the interval [ymin, ymax] into tiles 142 | 143 | """ 144 | 145 | ixmin, ixmax = _get_integer_intervals(xmin, xmax) 146 | iymin, iymax = _get_integer_intervals(ymin, ymax) 147 | 148 | xi_v = get_division_tiles(ixmin, ixmax, spacing_tile) 149 | columns = len(xi_v)-1 150 | yi_v = get_division_tiles(iymin, iymax, spacing_tile) 151 | rows = len(yi_v)-1 152 | 153 | # 154 | xi_v[0], yi_v[0] = xmin, ymin 155 | xi_v[-1], yi_v[-1] = xmax, ymax 156 | 157 | iniX1, iniX2 = _get_integer_interval_multiple(xi_v[0], xi_v[1], pixel_size) 158 | iniY1, iniY2 = _get_integer_interval_multiple(yi_v[0], yi_v[1], pixel_size) 159 | endX1, endX2 = _get_integer_interval_multiple(xi_v[-2], xi_v[-1], pixel_size) 160 | endY1, endY2 = _get_integer_interval_multiple(yi_v[-2], yi_v[-1], pixel_size) 161 | 162 | xi_v[0], yi_v[0] = iniX1, iniY1 163 | xi_v[-1], yi_v[-1] = endX2, endY2 164 | 165 | return rows,columns, xi_v, yi_v 166 | 167 | 168 | 169 | def _get_integer_intervals(xmin, xmax): 170 | """ 171 | For a given interval [xmin, xmax], returns the minimum interval [iXmin, iXmax] that contains the original one where iXmin and iXmax are Integer numbers. 172 | 173 | Examples: [ 3.45, 5.35] => [ 3, 6] 174 | [-3.45, 5.35] => [-4, 6] 175 | [-3.45, -2.35] => [-4, -2] 176 | 177 | Parameters 178 | ---------- 179 | 180 | xmin: float 181 | origin of the interval 182 | 183 | xmax: float 184 | end of the interval 185 | 186 | Returns 187 | ------- 188 | Returns the interval [iXmin, iXmax] 189 | 190 | iXmin: integer 191 | Minimum value of the integer interval 192 | 193 | iMmax: integer 194 | Maximum value of the integer interval 195 | 196 | """ 197 | 198 | if(xmin<0.0 and xmin!=int(xmin)): 199 | iXmin=int(xmin-1) 200 | else: 201 | iXmin = int(xmin) 202 | 203 | if(xmax==int(xmax)): 204 | iXmax=xmax 205 | else: 206 | iXmax=int(xmax+1) 207 | 208 | return iXmin, iXmax 209 | 210 | 211 | def _get_integer_interval_multiple(xmin, xmax, number): 212 | """ 213 | For a given interval [xmin, xmax], returns the minimum interval [iXmin, iXmax] that contains the original one 214 | where iXmin and iXmax are Integer numbers and the difference (iXmax-iXmin) is a multiple of 'number'. 215 | 216 | Examples: [xmin,xmax]=[ 3.45, 5.35] , number= 3 => [ 3, 6] 217 | [xmin,xmax]=[ 3.45, 5.35] , number= 7 => [-1, 6] 218 | [xmin,xmax]=[ 3.45, 15.35], number= 7 => [ 2, 16] 219 | 220 | Parameters 221 | ---------- 222 | 223 | xmin: float 224 | origin of the interval 225 | 226 | xmax: float 227 | end of the interval 228 | 229 | number: integer 230 | the final interval (iXmin, iXmax) must meet the following rule: the difference (iXmax-iXmin) will be a multiple of this number. 231 | 232 | 233 | Returns 234 | ------- 235 | Returns the interval [iXmin, iXmax] 236 | 237 | iXmin: integer 238 | Minimum value of the integer interval 239 | 240 | iMmax: integer 241 | Maximum value of the integer interval 242 | 243 | """ 244 | 245 | a, b = _get_integer_intervals(xmin, xmax) 246 | while (np.mod(b-a, number) > 0.0): 247 | a -= 1.0 248 | 249 | return a, b 250 | 251 | 252 | def horizontal_mosaicing_list(rasters, row, columns): 253 | 254 | result = rasters[row*columns][0] 255 | for cont in range(1, columns): 256 | result = np.hstack((result, rasters[row*columns + cont][0])) 257 | return result 258 | 259 | 260 | def mosaicing(rasters, rows, columns): 261 | 262 | result = horizontal_mosaicing_list(rasters, 0, columns) 263 | for cont in range(1, rows): 264 | h = horizontal_mosaicing_list(rasters, cont, columns) 265 | result = np.vstack((result,h)) 266 | 267 | return result -------------------------------------------------------------------------------- /pcloudpy/core/interpolation.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | from matplotlib import mlab 4 | from scipy.interpolate import griddata 5 | 6 | 7 | def natural_neighbor(x,y,z,xi,yi): 8 | """ 9 | Natural Neighbor Interpolation Method. 10 | 11 | Natural neighbor interpolation is a method of spatial interpolation, developed by Robin Sibson. 12 | The method is based on Voronoi tessellation of a discrete set of spatial points. 13 | This has advantages over simpler methods of interpolation, such as nearest-neighbor interpolation, 14 | in that it provides a more smooth approximation to the underlying "true" function. 15 | see Radial_basic_function 16 | 17 | zi = natural_neighbor(x,y,z,xi,yi) fits a surface of the form z = f*(*x, y) to the data in the (usually) non uniformly spaced vectors (x, y, z).
18 | griddata() interpolates this surface at the points specified by (xi, yi) to produce zi. xi and yi must describe a regular grid. 19 | 20 | 21 | Parameters 22 | ---------- 23 | 24 | x: array-like, shape= 1D 25 | x-coord [1D array] 26 | 27 | y: array-like, shape= 1D 28 | y-coord [1D array] 29 | 30 | z: array-like, shape= 1D 31 | z-coord [1D array] 32 | 33 | xi: array-like, shape= 2D array 34 | meshgrid for x-coords [2D array] 35 | 36 | yi: array-like, shape= 2D array 37 | meshgrid for y-coords [2D array] 38 | 39 | Returns 40 | ------- 41 | 42 | zi: array-like, shape=2D 43 | zi interpolated-value [2D array] for (xi,yi) 44 | 45 | 46 | """ 47 | zi = mlab.griddata(x,y,z,xi,yi) 48 | return zi 49 | 50 | 51 | def nearest_griddata(x, y, z, xi, yi): 52 | """ 53 | Nearest Neighbor Interpolation Method. 54 | 55 | Nearest-neighbor interpolation (also known as proximal interpolation or, in some contexts, point sampling) is a simple method of multivariate interpolation in one or more dimensions.
56 | 57 | Interpolation is the problem of approximating the value of a function for a non-given point in some space when given the value of that function in points around (neighboring) that point.
58 | The nearest neighbor algorithm selects the value of the nearest point and does not consider the values of neighboring points at all, yielding a piecewise-constant interpolant.
59 | The algorithm is very simple to implement and is commonly used (usually along with mipmapping) in real-time 3D rendering to select color values for a textured surface.
60 | 61 | zi = nearest_griddata(x,y,z,xi,yi) fits a surface of the form z = f*(*x, y) to the data in the (usually) nonuniformly spaced vectors (x, y, z).
62 | griddata() interpolates this surface at the points specified by (xi, yi) to produce zi. xi and yi must describe a regular grid.
63 | 64 | Parameters 65 | ---------- 66 | x: array-like 67 | x-coord [1D array] 68 | y: array-like 69 | y-coord [1D array] 70 | z: array-like 71 | z-coord [1D array] 72 | xi: array-like 73 | meshgrid for x-coords [2D array] see
numpy.meshgrid 74 | yi: array-like 75 | meshgrid for y-coords [2D array] see numpy.meshgrid 76 | 77 | Returns 78 | ------- 79 | Interpolated Zi Coord 80 | 81 | zi: array-like 82 | zi interpolated-value [2D array] for (xi,yi) 83 | """ 84 | zi = griddata(zip(x,y), z, (xi, yi), method='nearest') 85 | return zi 86 | -------------------------------------------------------------------------------- /pcloudpy/core/io/ReaderLAS.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | __all__=['ReaderLAS'] 4 | 5 | 6 | import numpy as np 7 | from laspy.file import File as FileLAS 8 | 9 | from .base import PointsCloudBase 10 | 11 | 12 | class ReaderLAS(PointsCloudBase): 13 | """ 14 | Reader for LAS Files 15 | 16 | example: 17 | reader = ReaderLAS(filename) 18 | reader.update() 19 | 20 | 21 | """ 22 | def __init__(self, filename): 23 | super(ReaderLAS, self).__init__() 24 | self.filename_ = filename 25 | 26 | def _update(self): 27 | 28 | f = FileLAS(self.filename_, mode='r') 29 | points = f.get_points() 30 | name = points.dtype.fields.keys() 31 | 32 | x = f.get_x_scaled() 33 | y = f.get_y_scaled() 34 | z = f.get_z_scaled() 35 | 36 | #Check is the LAS File contains red property 37 | if 'red' in points.dtype.fields[name[0]][0].fields.keys(): 38 | red = np.int32(255.0*f.red/65536.0) 39 | green = np.int32(255.0*f.green/65536.0) 40 | blue = np.int32(255.0*f.blue/65536.0) 41 | self.data_ = np.c_[x, y, z, red, green, blue] 42 | else: 43 | N = f.X.shape[0] 44 | color = 128*np.ones((N,), dtype=np.int32) 45 | self.data_ = np.c_[x, y, z, color, color, color] 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /pcloudpy/core/io/ReaderPLY.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | __all__ =['ReaderPLY', 'WriterPLY'] 4 | 5 | import numpy as np 6 | from vtk import vtkPLYReader, vtkPolyData, vtkPLYWriter 7 | 8 | 9 | from .base import PolyDataBase 10 | from .converters import get_polydata_from 11 | 12 | class ReaderPLY(PolyDataBase): 13 | """ 14 | Reader for PLY Files 15 | 16 | example: 17 | reader = ReaderPLY(filename) 18 | reader.update() 19 | 20 | """ 21 | def __init__(self, filename): 22 | super(ReaderPLY, self).__init__() 23 | self.filename_ = filename 24 | 25 | def _update(self): 26 | reader = vtkPLYReader() 27 | reader.SetFileName(self.filename_) 28 | reader.Update() 29 | self._polydata = reader.GetOutput() 30 | 31 | 32 | class WriterPLY: 33 | """ 34 | Function library for exporting different data into PLY format 35 | 36 | 37 | - from_numpy 38 | - from_polydata 39 | 40 | 41 | """ 42 | 43 | @staticmethod 44 | def from_numpy(points, tr_re, output_filename): 45 | 46 | polydata = get_polydata_from(points, tr_re) 47 | WriterPLY.from_polydata(polydata, output_filename) 48 | 49 | 50 | @staticmethod 51 | def from_polydata(polydata, output_filename): 52 | 53 | writerPLY = vtkPLYWriter() 54 | if not output_filename.endswith(".ply"): 55 | output_filename += ".ply" 56 | 57 | writerPLY.SetFileName(output_filename) 58 | writerPLY.SetInput(polydata) 59 | writerPLY.SetFileTypeToASCII() 60 | writerPLY.Write() 61 | 62 | -------------------------------------------------------------------------------- /pcloudpy/core/io/ReaderTIFF.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | __all__=['ReaderTIFF'] 4 | 5 | import gdal 6 | import numpy as np 7 | 8 | from ..utils.vtkhelpers import numpy_to_image 9 | from .base import ImageDataBase 10 | 11 | 12 | class ReaderTIFF(ImageDataBase): 13 | 14 | def __init__(self, filename): 15 | super(ReaderTIFF, self).__init__() 16 | self.filename_ = filename 17 | 18 | def _update(self): 19 | 20 | dataset = gdal.Open(self.filename_) 21 | band = dataset.GetRasterBand(1) 22 | self.data_ = band.ReadAsArray().astype(np.float32) 23 | vmin = self.data_.min() 24 | data = np.uint8(255*(self.data_-vmin)/ float(self.data_.max()-vmin)) 25 | self._imagedata = numpy_to_image(data) 26 | -------------------------------------------------------------------------------- /pcloudpy/core/io/ReaderVTP.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | __all__=['ReaderVTP'] 4 | 5 | from vtk import vtkXMLPolyDataReader 6 | 7 | from .base import PolyDataBase 8 | 9 | class ReaderVTP(PolyDataBase): 10 | def __init__(self, filename): 11 | super(ReaderVTP, self).__init__() 12 | self.filename_ = filename 13 | 14 | def _update(self): 15 | reader = vtkXMLPolyDataReader() 16 | reader.SetFileName(self.filename_) 17 | reader.Update() 18 | self._polydata = reader.GetOutput() 19 | 20 | -------------------------------------------------------------------------------- /pcloudpy/core/io/ReaderXYZ.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | __all__=['ReaderXYZ'] 4 | 5 | import pandas as pd 6 | from .base import PointsCloudBase 7 | 8 | class ReaderXYZ(PointsCloudBase): 9 | """ 10 | Opens XYZ[RGB] format files. XYZ Coordinates, 11 | Color: 12 | - RGB Color, 13 | - one scalar 14 | - None 15 | """ 16 | def __init__(self, filename): 17 | super(ReaderXYZ, self).__init__() 18 | self.filename_ = filename 19 | 20 | def _update(self): 21 | """ 22 | Reads XYZ[RGB] Format file 23 | """ 24 | df = pd.read_csv(self.filename_, sep=' ') 25 | #export to numpy array 26 | self.data_ = df.to_numpy() 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /pcloudpy/core/io/__init__.py: -------------------------------------------------------------------------------- 1 | from pcloudpy.core.io.ReaderXYZ import ReaderXYZ 2 | from pcloudpy.core.io.ReaderLAS import ReaderLAS 3 | from pcloudpy.core.io.ReaderTIFF import ReaderTIFF 4 | from pcloudpy.core.io.ReaderPLY import ReaderPLY 5 | from pcloudpy.core.io.ReaderVTP import ReaderVTP 6 | 7 | __all__ = ['ReaderXYZ', 'ReaderLAS', 'ReaderTIFF', 'ReaderPLY', 'ReaderVTP'] 8 | 9 | def select_func_from_extension(extension): 10 | d = dict({"xyz": ReaderXYZ, "txt": ReaderXYZ, 11 | "las": ReaderLAS, 12 | "ply": ReaderPLY, 13 | "vtp": ReaderVTP, 14 | "tiff": ReaderTIFF, "tif": ReaderTIFF }) 15 | return d.get(extension) 16 | 17 | -------------------------------------------------------------------------------- /pcloudpy/core/io/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bases Classes for the I/O handling 3 | """ 4 | #Author: Miguel Molero 5 | 6 | from collections import OrderedDict 7 | import copy 8 | import numpy as np 9 | 10 | from vtk import vtkPolyData, vtkImageData 11 | 12 | from ..utils.vtkhelpers import actor_from_imagedata, actor_from_polydata 13 | from ..io.converters import numpy_from_polydata, polydata_from_numpy 14 | 15 | __all__ = ["DataBase", "ImageDataBase", "PolyDataBase", "PointsCloudBase"] 16 | 17 | 18 | class DataType(object): 19 | POLYDATA = "polydata" 20 | POINTCLOUD = "pointcloud" 21 | IMAGEDATA = "imagedata" 22 | 23 | 24 | 25 | class DataBase(object): 26 | """ 27 | Data Base Class 28 | 29 | data_: Array 30 | store array od points of the the point cloud 31 | 32 | props_: OrderedDict 33 | Dictionary used for the props storing 34 | 35 | """ 36 | def __init__(self): 37 | 38 | self.data_ = None 39 | self.props_ = None 40 | self.type_ = None 41 | self._actor = None 42 | 43 | def get_props(self): 44 | if self.props_: 45 | return self.props_ 46 | 47 | def get_data(self): 48 | """ 49 | Gets Container 50 | """ 51 | if self.type_ in (DataType.POLYDATA, DataType.POINTCLOUD): 52 | print("get data") 53 | return self.get_polydata() 54 | elif self.type_ == DataType.IMAGEDATA: 55 | return self.get_imagedata() 56 | 57 | def get_actor(self): 58 | """ 59 | Gets Actor 60 | """ 61 | if self._actor: 62 | return self._actor 63 | 64 | def set_actor(self, actor): 65 | """ 66 | Sets Actor 67 | """ 68 | self._actor = actor 69 | 70 | 71 | 72 | class ImageDataBase(DataBase): 73 | """ 74 | ImageData Base Class 75 | 76 | Attributes 77 | ---------- 78 | props_: OrderedDict 79 | Dictionary used for the props storing 80 | 81 | data_: array 82 | raw data 83 | 84 | 85 | """ 86 | def __init__(self): 87 | super(ImageDataBase, self).__init__() 88 | self.type_ = DataType.IMAGEDATA 89 | 90 | def get_imagedata(self): 91 | """ 92 | Gets ImageData 93 | """ 94 | if self._imagedata: 95 | return self._imagedata 96 | 97 | def set_imagedata(self, imagedata): 98 | """ 99 | Sets ImageData 100 | """ 101 | self._imagedata = imagedata 102 | self._actor = actor_from_imagedata(self._imagedata) 103 | self.update_props() 104 | 105 | def _update(self): 106 | pass 107 | 108 | def update(self): 109 | self._update() 110 | self.set_imagedata(self._imagedata) 111 | self.update_props() 112 | 113 | def clone(self): 114 | """ 115 | Returns a Clone of an ImageDataBase instance 116 | """ 117 | imagedata = vtkImageData() 118 | imagedata.DeepCopy(self._imagedata) 119 | clone = copy.copy(self) 120 | clone.set_imagedata(imagedata) 121 | clone.set_actor(actor_from_imagedata(self._imagedata)) 122 | return clone 123 | 124 | def update_props(self): 125 | 126 | if self._imagedata: 127 | self.props_ = OrderedDict() 128 | self.props_["Number of Points"] = self._imagedata.GetNumberOfPoints() 129 | bounds = self._imagedata.GetBounds() 130 | 131 | self.props_["xmin"] = bounds[0] 132 | self.props_["xmax"] = bounds[1] 133 | self.props_["ymin"] = bounds[2] 134 | self.props_["ymax"] = bounds[3] 135 | self.props_["zmin"] = np.min(self.data_) 136 | self.props_["zmax"] = np.max(self.data_) 137 | 138 | 139 | 140 | class PolyDataBase(DataBase): 141 | """ 142 | PolyData Base Class 143 | 144 | 145 | Attributes 146 | --------- 147 | data_: Array 148 | store array od points of the the point cloud 149 | 150 | props_: OrderedDict 151 | Dictionary used for the props storing 152 | 153 | """ 154 | 155 | def __init__(self): 156 | super(PolyDataBase, self).__init__() 157 | self.type_ = DataType.POLYDATA 158 | 159 | def get_polydata(self): 160 | """ 161 | Gets PolyData (vtkPolyData) 162 | """ 163 | if self._polydata: 164 | return self._polydata 165 | 166 | def set_polydata(self, polydata): 167 | self._polydata = polydata 168 | self._actor = actor_from_polydata(self._polydata) 169 | self.update_props() 170 | 171 | def update(self): 172 | """ 173 | Update Reader 174 | """ 175 | self._update() 176 | self.set_polydata(self._polydata) 177 | self.update_props() 178 | 179 | def _update(self): 180 | pass 181 | 182 | def clone(self): 183 | polydata = vtkPolyData() 184 | polydata.DeepCopy(self._polydata) 185 | 186 | clone = copy.copy(self) 187 | clone.set_polydata(polydata) 188 | clone.set_actor(actor_from_polydata(polydata)) 189 | return clone 190 | 191 | def update_data_from(self, polydata): 192 | 193 | self._data = numpy_from_polydata(polydata) 194 | self._polydata = polydata 195 | self._actor = actor_from_polydata(self._polydata) 196 | self.update_props() 197 | 198 | def update_props(self): 199 | 200 | if self._polydata: 201 | self.props_ = OrderedDict() 202 | self.props_["Number of Points"] = self._polydata.GetNumberOfPoints() 203 | 204 | if isinstance(self._polydata, vtkPolyData): 205 | polys = self._polydata.GetNumberOfPolys() 206 | lines = self._polydata.GetNumberOfLines() 207 | 208 | if polys !=0: 209 | self.props_["Number of Polygons"] = polys 210 | if lines !=0: 211 | self.props_["Number of Lines"] = lines 212 | 213 | verts = self._polydata.GetNumberOfVerts() 214 | if verts !=0: 215 | self.props_["Number of Vertices"] = verts 216 | 217 | strips = self._polydata.GetNumberOfStrips() 218 | if strips !=0: 219 | self.props_["Number of Triangles"] = strips 220 | 221 | bounds = self._polydata.GetBounds() 222 | self.props_["xmin"] = bounds[0] 223 | self.props_["xmax"] = bounds[1] 224 | self.props_["ymin"] = bounds[2] 225 | self.props_["ymax"] = bounds[3] 226 | self.props_["zmin"] = bounds[4] 227 | self.props_["zmax"] = bounds[5] 228 | 229 | 230 | 231 | 232 | class PointsCloudBase(PolyDataBase): 233 | """ 234 | Point Cloud Base Class 235 | 236 | 237 | Attributes 238 | ---------- 239 | data_: Array 240 | store array od points of the the point cloud 241 | 242 | 243 | """ 244 | def __init__(self, *args, **kwargs): 245 | super(PointsCloudBase, self).__init__(*args, **kwargs) 246 | 247 | self.type_ = DataType.POINTCLOUD 248 | 249 | def update(self): 250 | self._update() 251 | self._polydata = polydata_from_numpy(self.data_) 252 | self.set_polydata(self._polydata) 253 | -------------------------------------------------------------------------------- /pcloudpy/core/io/converters.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | import numpy as np 4 | 5 | from vtk import vtkPolyData, vtkPoints, vtkCellArray, vtkTriangle 6 | from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtk, get_numpy_array_type, get_vtk_to_numpy_typemap, \ 7 | numpy_to_vtkIdTypeArray 8 | from vtk import vtkPoints, VTK_UNSIGNED_CHAR 9 | from vtk import vtkCellArray, vtkPolyData 10 | 11 | 12 | def get_polydata_from(points, tr_re): 13 | 14 | numberPoints = len(points) 15 | Points = vtkPoints() 16 | ntype = get_numpy_array_type(Points.GetDataType()) 17 | points_vtk = numpy_to_vtk(np.asarray(points, order='C',dtype=ntype), deep=1) 18 | Points.SetNumberOfPoints(numberPoints) 19 | Points.SetData(points_vtk) 20 | 21 | Triangles = vtkCellArray() 22 | for item in tr_re: 23 | Triangle = vtkTriangle() 24 | Triangle.GetPointIds().SetId(0,item[0]) 25 | Triangle.GetPointIds().SetId(1,item[1]) 26 | Triangle.GetPointIds().SetId(2,item[2]) 27 | Triangles.InsertNextCell(Triangle) 28 | 29 | polydata = vtkPolyData() 30 | polydata.SetPoints(Points) 31 | polydata.SetPolys(Triangles) 32 | 33 | polydata.Modified() 34 | polydata.Update() 35 | 36 | return polydata 37 | 38 | 39 | 40 | def get_points_normals_from(polydata): 41 | 42 | nodes_vtk_array = polydata.GetPoints().GetData() 43 | array = vtk_to_numpy(nodes_vtk_array) 44 | 45 | numberArrays = polydata.GetPointData().GetNumberOfArrays() 46 | for i in range(numberArrays): 47 | if polydata.GetPointData().GetArrayName(i) == "Normals": 48 | array_normals = vtk_to_numpy(polydata.GetPointData().GetScalars("Normals")) 49 | return array, array_normals 50 | else: 51 | raise Exception("No available Normals") 52 | 53 | 54 | def save_to_xyz_normals(polydata, filename): 55 | 56 | array, normals = get_points_normals_from(polydata) 57 | X = np.c_[array, normals] 58 | np.savetxt(filename, X, delimiter=' ', newline='\n') 59 | 60 | 61 | def numpy_from_polydata(polydata): 62 | """ 63 | Converts vtkPolyData to Numpy Array 64 | 65 | Parameters 66 | ---------- 67 | 68 | polydata: vtkPolyData 69 | Input Polydata 70 | 71 | """ 72 | 73 | nodes_vtk_array = polydata.GetPoints().GetData() 74 | nodes_numpy_array = vtk_to_numpy(nodes_vtk_array) 75 | 76 | if polydata.GetPointData().GetScalars(): 77 | nodes_numpy_array_colors = vtk_to_numpy(polydata.GetPointData().GetScalars("colors")) 78 | return np.c_[nodes_numpy_array, nodes_numpy_array_colors] 79 | else: 80 | return nodes_numpy_array 81 | 82 | 83 | def polydata_from_numpy(coords): 84 | """ 85 | Converts Numpy Array to vtkPolyData 86 | 87 | Parameters 88 | ---------- 89 | coords: array, shape= [number of points, point features] 90 | array containing the point cloud in which each point has three coordinares [x, y, z] 91 | and none, one or three values corresponding to colors. 92 | 93 | Returns: 94 | -------- 95 | PolyData: vtkPolyData 96 | concrete dataset represents vertices, lines, polygons, and triangle strips 97 | 98 | """ 99 | 100 | Npts, Ndim = np.shape(coords) 101 | 102 | Points = vtkPoints() 103 | ntype = get_numpy_array_type(Points.GetDataType()) 104 | coords_vtk = numpy_to_vtk(np.asarray(coords[:,0:3], order='C',dtype=ntype), deep=1) 105 | Points.SetNumberOfPoints(Npts) 106 | Points.SetData(coords_vtk) 107 | 108 | Cells = vtkCellArray() 109 | ids = np.arange(0,Npts, dtype=np.int64).reshape(-1,1) 110 | IDS = np.concatenate([np.ones(Npts, dtype=np.int64).reshape(-1,1), ids],axis=1) 111 | ids_vtk = numpy_to_vtkIdTypeArray(IDS, deep=True) 112 | 113 | Cells.SetNumberOfCells(Npts) 114 | Cells.SetCells(Npts,ids_vtk) 115 | 116 | if Ndim == 4: 117 | color = [128]*len(coords) 118 | color = np.c_[color, color, color] 119 | elif Ndim == 6: 120 | color = coords[:,3:] 121 | else: 122 | color = [[128, 128, 128]]*len(coords) 123 | 124 | color_vtk = numpy_to_vtk( 125 | np.ascontiguousarray(color, dtype=get_vtk_to_numpy_typemap()[VTK_UNSIGNED_CHAR]), 126 | deep=True) 127 | 128 | color_vtk.SetName("colors") 129 | 130 | PolyData = vtkPolyData() 131 | PolyData.SetPoints(Points) 132 | PolyData.SetVerts(Cells) 133 | PolyData.GetPointData().SetScalars(color_vtk) 134 | return PolyData 135 | 136 | 137 | def copy_polydata_add_normals(polydata, normals): 138 | 139 | Points = vtkPoints() 140 | ntype = get_numpy_array_type(Points.GetDataType()) 141 | normals_vtk = numpy_to_vtk(np.asarray(normals[:,0:3], order='C',dtype=ntype), deep=1) 142 | normals_vtk.SetName("Normals") 143 | 144 | output = vtkPolyData() 145 | output.ShallowCopy(polydata) 146 | output.GetPointData().SetNormals(normals_vtk) 147 | return output -------------------------------------------------------------------------------- /pcloudpy/core/utils/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Miguel' 2 | -------------------------------------------------------------------------------- /pcloudpy/core/utils/vtkhelpers.py: -------------------------------------------------------------------------------- 1 | """helper methods 2 | """ 3 | #Author: Miguel Molero 4 | 5 | import numpy as np 6 | from vtk.util.numpy_support import numpy_to_vtk 7 | from vtk import vtkPolyDataMapper, vtkActor 8 | from vtk import vtkRenderer, vtkRenderWindow, vtkRenderWindowInteractor 9 | from vtk import vtkImageActor, vtkImageData, vtkDataSetMapper 10 | 11 | 12 | def actor_from_unstructuredgrid(data): 13 | mapper = vtkDataSetMapper() 14 | mapper.SetInputData(data) 15 | actor = vtkActor() 16 | actor.SetMapper(mapper) 17 | return actor 18 | 19 | 20 | def actor_from_polydata(PolyData): 21 | """ 22 | Returns the VTK Actor from vtkPolyData Structure 23 | """ 24 | mapper = vtkPolyDataMapper() 25 | mapper.SetInputData(PolyData) 26 | actor = vtkActor() 27 | actor.SetMapper(mapper) 28 | #actor.GetProperty().SetPointSize(2) 29 | return actor 30 | 31 | def actor_from_imagedata(imagedata): 32 | actor = vtkImageActor() 33 | actor.SetInputData(imagedata) 34 | actor.InterpolateOff() 35 | return actor 36 | 37 | 38 | 39 | def display_from_actor(actor): 40 | renderer = vtkRenderer() 41 | renderWindow = vtkRenderWindow() 42 | renderWindow.AddRenderer(renderer) 43 | 44 | renderer.AddActor(actor) 45 | # enable user interface interactor 46 | iren = vtkRenderWindowInteractor() 47 | iren.SetRenderWindow(renderWindow) 48 | iren.Initialize() 49 | renderWindow.Render() 50 | iren.Start() 51 | 52 | 53 | def numpy_to_image(numpy_array): 54 | """ 55 | @brief Convert a numpy 2D or 3D array to a vtkImageData object 56 | @param numpy_array 2D or 3D numpy array containing image data 57 | @return vtkImageData with the numpy_array content 58 | """ 59 | 60 | shape = numpy_array.shape 61 | if len(shape) < 2: 62 | raise Exception('numpy array must have dimensionality of at least 2') 63 | 64 | h, w = shape[0], shape[1] 65 | c = 1 66 | if len(shape) == 3: 67 | c = shape[2] 68 | 69 | # Reshape 2D image to 1D array suitable for conversion to a 70 | # vtkArray with numpy_support.numpy_to_vtk() 71 | linear_array = np.reshape(numpy_array, (w*h, c)) 72 | vtk_array = numpy_to_vtk(linear_array) 73 | 74 | image = vtkImageData() 75 | image.SetDimensions(w, h, 1) 76 | image.AllocateScalars() 77 | image.GetPointData().GetScalars().DeepCopy(vtk_array) 78 | 79 | return image -------------------------------------------------------------------------------- /pcloudpy/display/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pcloudpy/gui/AppObject.py: -------------------------------------------------------------------------------- 1 | 2 | __all__ = ['AppObject'] 3 | 4 | 5 | from PyQt5.QtCore import * 6 | from PyQt5.QtGui import * 7 | from PyQt5.QtWidgets import * 8 | from PyQt5.QtCore import pyqtSignal as Signal 9 | 10 | from pcloudpy.gui.ManagerLayer import Layer 11 | 12 | class AppObject(object): 13 | """ 14 | Application Object 15 | 16 | 17 | 18 | """ 19 | def __init__(self): 20 | 21 | super(AppObject, self).__init__() 22 | self._current_view = None 23 | self._current_layer = None 24 | 25 | class QObj(QObject): 26 | def __init__(self): 27 | super(QObj, self).__init__() 28 | layerAdded = Signal() 29 | 30 | self._QObj = QObj() 31 | 32 | 33 | def currentView(self): 34 | """ 35 | Gets the Current View 36 | 37 | 38 | 39 | 40 | """ 41 | return self._current_view 42 | 43 | def setCurrentView(self, view): 44 | """ 45 | 46 | """ 47 | self._current_view = view 48 | 49 | def getLayers(self): 50 | """ 51 | 52 | """ 53 | if self._current_view: 54 | return self._current_view.manager_layer.layers() 55 | 56 | def addObject(self, obj, name): 57 | """ 58 | 59 | """ 60 | layer = Layer(name) 61 | layer.set_current_view(self._current_view) 62 | layer.set_container(obj) 63 | 64 | self._current_layer = layer 65 | self._current_view.add_layer(layer) 66 | self._current_view.update_render() 67 | 68 | self._QObj.layerAdded.emit() -------------------------------------------------------------------------------- /pcloudpy/gui/MainWindow.py: -------------------------------------------------------------------------------- 1 | """ 2 | MainWindow Class 3 | """ 4 | #Author: Miguel Molero 5 | # License: BSD 3 clause 6 | 7 | import os 8 | from collections import OrderedDict 9 | 10 | from PyQt5.QtCore import * 11 | from PyQt5.QtGui import * 12 | from PyQt5.QtWidgets import * 13 | from PyQt5.QtCore import pyqtSignal as Signal 14 | 15 | from pcloudpy.gui.components.ViewWidget import ViewWidget 16 | from pcloudpy.gui.ManagerLayer import Layer, ManagerLayer 17 | 18 | from pcloudpy.gui.MainWindowBase import MainWindowBase 19 | 20 | from ..core import io as IO 21 | from ..core import filters as Filters 22 | 23 | class MainWindow(MainWindowBase): 24 | """ 25 | MainWindow Class. 26 | """ 27 | def __init__(self, parent = None): 28 | super(MainWindow, self).__init__(parent) 29 | self.manager_layer = ManagerLayer() 30 | self._num_views = 1 31 | self.setMouseTracking(True) 32 | self.current_view = self.tab_view.currentWidget() 33 | self.App.setCurrentView(self.current_view) 34 | 35 | def setup_connections(self): 36 | super(MainWindow, self).setup_connections() 37 | 38 | #Tab View Widget 39 | self.tab_view.tab.plusClicked.connect(self.add_new_view) 40 | self.tab_view.tab.currentChanged.connect(self.current_tab) 41 | 42 | #Toolboxes Widget 43 | self.toolboxes_widget.currentItemClicked.connect(self.manager_toolbox) 44 | self.toolboxes_widget.currentItemSelected.connect(self.toolbox_item_selected) 45 | 46 | #Datasets Widget 47 | self.datasets_widget.currentItemChanged.connect(self.manager_datasets) 48 | self.datasets_widget.itemDeleted.connect(self.delete_dataset) 49 | self.datasets_widget.tree.clicked.connect(self.set_current_dataset) 50 | self.datasets_widget.tree.selectionModel().selectionChanged.connect(self.select_item) 51 | self.datasets_widget.itemCloneRequested.connect(self.clone_dataset) 52 | self.datasets_widget.filterRequested.connect(self.apply_filter) 53 | 54 | #FilterWidget 55 | #Put in enable state "Apply Filter" 56 | self.filter_widget.filterRequested.connect(self.get_filter_parms) 57 | 58 | view = self.tab_view.currentWidget() 59 | view.layersModified.connect(self.change_on_dataset) 60 | 61 | #AppObject 62 | self.App._QObj.layerAdded.connect(self.add_layer_from_app) 63 | 64 | def current_tab(self, index): 65 | self.current_view = self.tab_view.widget(index) 66 | self.datasets_widget.init_tree(self.current_view.model) 67 | self.object_inspector_widget.properties_tree.clear() 68 | self.App.setCurrentView(self.current_view) 69 | 70 | def add_new_view(self): 71 | self._num_views +=1 72 | view = ViewWidget() 73 | view.layersModified.connect(self.change_on_dataset) 74 | self.tab_view.addTab(view,"Layout#%s"%self._num_views) 75 | self.tab_view.setCurrentIndex(self._num_views) 76 | 77 | def set_current_dataset(self, index): 78 | if index.row()>0: 79 | current_item = index.model().itemFromIndex(index) 80 | layer = current_item.get_current_view().manager_layer[index.row()-1] 81 | props = layer.get_container().get_props() 82 | self.object_inspector_widget.update(props) 83 | 84 | def select_item(self, new_item, _): 85 | items = new_item.indexes() 86 | if len(items)!=0: 87 | index = new_item.indexes()[0] 88 | self.set_current_dataset(index) 89 | 90 | def delete_dataset(self, index): 91 | layer = self.datasets_widget.current_item.get_current_view().manager_layer.pop(index) 92 | layer.get_current_view().remove_actor(layer.get_container().get_actor()) 93 | del layer 94 | 95 | def clone_dataset(self, index): 96 | layer = self.datasets_widget.current_item.get_current_view().manager_layer[index] 97 | current_view = layer.get_current_view() 98 | 99 | layer_clone = layer.copy() 100 | current_view.add_layer(layer_clone) 101 | 102 | current_view.update_render() 103 | self.datasets_widget.add_dataset(layer_clone, current_view) 104 | 105 | def change_on_dataset(self): 106 | pass 107 | 108 | def manager_datasets(self, index): 109 | view = self.tab_view.currentWidget() 110 | layer = view.manager_layer[index] 111 | if self.datasets_widget.current_item.checkState() == Qt.CheckState.Checked: 112 | layer.get_container().get_actor().SetVisibility(1) 113 | else: 114 | layer.get_container().get_actor().SetVisibility(0) 115 | layer.get_current_view().update_render() 116 | 117 | def manager_toolbox(self): 118 | """ 119 | Method that manages the components of the toolboxes 120 | 121 | - reader 122 | - filter 123 | - display 124 | 125 | """ 126 | 127 | item = self.toolboxes_widget.get_current_item() 128 | d = OrderedDict(item.metadata()) 129 | # 130 | if not "type" in d: 131 | return 132 | 133 | if d['type'] == 'reader': 134 | self.filter_widget.remove_filter() 135 | self.filter_widget.set_filter(d['func'], None, d['text']) 136 | 137 | filenames, _ = QFileDialog.getOpenFileNames(self, d['message'], self.dir_path, d['format']) 138 | if filenames: 139 | for filename in filenames: 140 | func = getattr(IO, d['func']) 141 | self._get_datasets(func, filename) 142 | 143 | elif d['type'] == 'filter': 144 | self.filter_widget.set_filter(d['func'], d['parms'], d['text']) 145 | 146 | elif d['type'] == 'display': 147 | self.filter_widget.set_filter(d['func'], None, d['text'], only_apply=True) 148 | 149 | def toolbox_item_selected(self): 150 | """ 151 | Update the selected item in the toolbox manager 152 | 153 | """ 154 | item = self.toolboxes_widget.get_current_item() 155 | d = OrderedDict(item.metadata()) 156 | 157 | if not "type" in d: 158 | return 159 | 160 | if d['type'] == 'reader': 161 | self.filter_widget.set_filter(d['func'], None, d['text']) 162 | 163 | elif d['type'] == 'filter': 164 | self.filter_widget.set_filter(d['func'], d['parms'], d['text']) 165 | 166 | elif d['type'] == 'display': 167 | self.filter_widget.set_filter(d['func'], None, d['text'], only_apply=True) 168 | 169 | 170 | def file_open(self): 171 | filters = "*.xyz;;*.txt;;*.las;;*.tif *.tiff;;*.vtp" 172 | filenames, _ = QFileDialog.getOpenFileNames(self, "Open Dataset[s]", self.dir_path, filters) 173 | if filenames: 174 | for filename in filenames: 175 | func = IO.select_func_from_extension(QFileInfo(filename).suffix()) 176 | if func: 177 | self._get_datasets(func,filename) 178 | 179 | def get_filter_parms(self): 180 | self.func, self.parms = self.filter_widget.get_parms() 181 | self.datasets_widget.set_enable_filter() 182 | 183 | def apply_filter(self, index): 184 | self.setCursor(Qt.WaitCursor) 185 | QApplication.processEvents() 186 | QApplication.processEvents() 187 | layer = self.datasets_widget.current_item.get_current_view().manager_layer[index] 188 | current_view = layer.get_current_view() 189 | layer_clone = layer.copy() 190 | name = layer_clone.get_name().replace("clone","") 191 | name += "_%s"%self.func 192 | layer_clone.set_name(name) 193 | 194 | #apply filter 195 | func = getattr(Filters, self.func) 196 | filter = func(**self.parms) 197 | 198 | data = layer_clone.get_container().get_data() 199 | 200 | if filter.set_input(data): 201 | try: 202 | self.setCursor(Qt.WaitCursor) 203 | QApplication.processEvents() 204 | filter.update() 205 | except Exception as e: 206 | q = QMessageBox(QMessageBox.Critical, "Error Message", str(e)) 207 | q.setStandardButtons(QMessageBox.Ok) 208 | q.exec_() 209 | self.setCursor(Qt.ArrowCursor) 210 | QApplication.processEvents() 211 | return 212 | finally: 213 | self.setCursor(Qt.ArrowCursor) 214 | QApplication.processEvents() 215 | 216 | else: 217 | msg = "No suitable Filter for the chosen dataset" 218 | q = QMessageBox(QMessageBox.Critical, "Error Message", msg) 219 | q.setStandardButtons(QMessageBox.Ok) 220 | q.exec_() 221 | self.setCursor(Qt.ArrowCursor) 222 | QApplication.processEvents() 223 | return 224 | ### 225 | 226 | #Update Actors 227 | layer_clone.get_container().set_polydata(filter.get_output()) 228 | current_view.add_layer(layer_clone) 229 | current_view.update_render() 230 | self.datasets_widget.add_dataset(layer_clone, current_view) 231 | 232 | 233 | def add_layer_from_app(self): 234 | 235 | layer = self.App._current_layer 236 | current_view = self.App._current_view 237 | self.datasets_widget.add_dataset(layer, current_view) 238 | props = layer.get_container().get_props() 239 | self.object_inspector_widget.update(props) 240 | 241 | def _get_datasets(self, func, filename): 242 | self.setCursor(Qt.WaitCursor) 243 | QApplication.processEvents() 244 | 245 | name = os.path.basename(filename) 246 | pcls = func(filename) 247 | pcls.update() 248 | 249 | current_view = self.tab_view.currentWidget() 250 | layer = Layer(name) 251 | layer.set_current_view(current_view) 252 | layer.set_container(pcls) 253 | 254 | current_view.add_layer(layer) 255 | current_view.update_render() 256 | 257 | self.App.setCurrentView(current_view) 258 | self.datasets_widget.add_dataset(layer, current_view) 259 | props = layer.get_container().get_props() 260 | self.object_inspector_widget.update(props) 261 | 262 | self.setCursor(Qt.ArrowCursor) 263 | QApplication.processEvents() 264 | 265 | -------------------------------------------------------------------------------- /pcloudpy/gui/MainWindowBase.py: -------------------------------------------------------------------------------- 1 | """ 2 | Template MainWindowBase.py 3 | """ 4 | #Author: Miguel Molero 5 | 6 | 7 | import sys 8 | import os 9 | from PyQt5.QtCore import * 10 | from PyQt5.QtGui import * 11 | from PyQt5.QtWidgets import * 12 | from PyQt5.QtCore import pyqtSignal as Signal 13 | 14 | import markdown2 15 | import yaml 16 | import pprint 17 | 18 | #own components 19 | from pcloudpy.gui.resources_rc import * 20 | #from pcloudpy.gui.graphics.QVTKWidget import QVTKWidget 21 | 22 | from pcloudpy.gui.AppObject import AppObject 23 | from pcloudpy.gui.utils.qhelpers import * 24 | 25 | from pcloudpy.gui.components.ViewWidget import ViewWidget 26 | from pcloudpy.gui.components.TabViewWidget import TabViewWidget 27 | from pcloudpy.gui.components.ToolboxesWidget import ToolBoxesWidget 28 | from pcloudpy.gui.components.DatasetsWidget import DatasetsWidget 29 | from pcloudpy.gui.components.ObjectInspectorWidget import ObjectInspectorWidget 30 | from pcloudpy.gui.components.FilterWidget import FilterWidget 31 | 32 | #from shell.PythonConsole import PythonConsole 33 | #from shell.IPythonConsole import IPythonConsole 34 | #from shell.CodeEdit import CodeEdit 35 | 36 | NAME = "pcloudpy" 37 | 38 | class Info(object): 39 | version = "0.10" 40 | date = "27-10-2015" 41 | class MainWindowBase(QMainWindow): 42 | """ 43 | Base Class for the MainWindow Object. This class should inherit its attributes and methods to a MainWindow Class 44 | """ 45 | def __init__(self, parent = None): 46 | super(MainWindowBase, self).__init__(parent) 47 | self.setLocale((QLocale(QLocale.English, QLocale.UnitedStates))) 48 | self._software_name = NAME 49 | 50 | self.App = AppObject() 51 | 52 | self.init() 53 | self.create_menus() 54 | self.create_toolbars() 55 | self.setup_docks() 56 | self.setup_graphicsview() 57 | self.setup_statusbar() 58 | self.setup_connections() 59 | self.init_settings() 60 | 61 | self.init_toolboxes() 62 | 63 | QTimer.singleShot(0,self.load_initial_file) 64 | 65 | @property 66 | def software_name(self): 67 | return self._software_name 68 | 69 | @software_name.setter 70 | def software_name(self, name): 71 | self._software_name = name 72 | 73 | def init(self): 74 | self.Info = Info() 75 | self.dirty = False 76 | self.reset = False 77 | self.filename = None 78 | self.recent_files = [] 79 | self.dir_path = os.getcwd() 80 | 81 | self.setGeometry(100,100,900,600) 82 | self.setMinimumSize(400,400) 83 | self.setMaximumSize(2000,1500) 84 | self.setWindowFlags(self.windowFlags()) 85 | self.setWindowTitle(self.software_name) 86 | 87 | #Put here your init code 88 | 89 | def set_title(self, fname=None): 90 | title = os.path.basename(fname) 91 | self.setWindowTitle("%s:%s"%(self.softwareName,title)) 92 | 93 | def load_initial_file(self): 94 | settings = QSettings() 95 | fname = settings.value("LastFile") 96 | if fname and QFile.exists(fname): 97 | self.load_file(fname) 98 | 99 | def load_file(self, fname=None): 100 | if fname is None: 101 | action = self.sender() 102 | if isinstance(action, QAction): 103 | fname = action.data() 104 | if not self.ok_to_Continue(): 105 | return 106 | else: 107 | return 108 | 109 | if fname: 110 | self.filename = None 111 | self.add_recent_file(fname) 112 | self.filename = fname 113 | self.dirty = False 114 | self.set_title(fname) 115 | #Add More actions 116 | # 117 | # 118 | 119 | def add_recent_file(self, fname): 120 | 121 | if fname is None: 122 | return 123 | if not self.recentFiles.count(fname): 124 | self.recentFiles.insert(0,fname) 125 | while len(self.recentFiles)>9: 126 | self.recentFiles.pop() 127 | 128 | def create_menus(self): 129 | 130 | self.menubar = self.menuBar() 131 | 132 | file_menu = self.menubar.addMenu(self.tr('&File')) 133 | help_menu = self.menubar.addMenu(self.tr("&Help")) 134 | 135 | file_open_action = createAction(self, "&Open Dataset[s]", self.file_open) 136 | file_open_action.setIcon(self.style().standardIcon(QStyle.SP_DirIcon)) 137 | help_about_action = createAction(self, "&About %s"%self._software_name, self.help_about, icon="pcloudpy.png") 138 | addActions(file_menu, (file_open_action,)) 139 | addActions(help_menu, (help_about_action,)) 140 | 141 | def setup_connections(self): 142 | #Main Window 143 | self.workspaceLineEdit.textEdited.connect(self.editWorkSpace) 144 | #self.code_edit.codeRequested.connect(self.console_widget.execute_code) 145 | 146 | def setup_docks(self): 147 | #Toolboxes 148 | self.toolboxes_widget = ToolBoxesWidget() 149 | self.toolboxes_dockwidget = QDockWidget(self.tr("Toolboxes")) 150 | self.toolboxes_dockwidget.setObjectName("Toolboxes-Dock") 151 | self.toolboxes_dockwidget.setWidget(self.toolboxes_widget) 152 | self.toolboxes_dockwidget.setAllowedAreas(Qt.RightDockWidgetArea) 153 | self.addDockWidget(Qt.RightDockWidgetArea, self.toolboxes_dockwidget) 154 | 155 | #Datasets 156 | self.datasets_widget = DatasetsWidget() 157 | self.datasets_dockwidget = QDockWidget(self.tr("Datasets")) 158 | self.datasets_dockwidget.setObjectName("Datasets-Dock") 159 | self.datasets_dockwidget.setWidget(self.datasets_widget) 160 | self.datasets_dockwidget.setAllowedAreas(Qt.LeftDockWidgetArea) 161 | self.addDockWidget(Qt.LeftDockWidgetArea, self.datasets_dockwidget) 162 | 163 | #Object Inspector 164 | self.object_inspector_widget = ObjectInspectorWidget() 165 | self.object_inspector_dockwidget = QDockWidget(self.tr("Object Inspector")) 166 | self.object_inspector_dockwidget.setObjectName("Object-Inspector-Dock") 167 | self.object_inspector_dockwidget.setWidget(self.object_inspector_widget) 168 | self.object_inspector_dockwidget.setAllowedAreas(Qt.LeftDockWidgetArea) 169 | self.addDockWidget(Qt.LeftDockWidgetArea, self.object_inspector_dockwidget) 170 | 171 | #Filter Widget 172 | self.filter_widget = FilterWidget() 173 | self.filter_widget_dockwidget = QDockWidget(self.tr("Filter Setup")) 174 | self.filter_widget_dockwidget.setObjectName("Filter-Setup-Dock") 175 | self.filter_widget_dockwidget.setWidget(self.filter_widget) 176 | self.filter_widget_dockwidget.setAllowedAreas(Qt.RightDockWidgetArea) 177 | self.addDockWidget(Qt.RightDockWidgetArea, self.filter_widget_dockwidget) 178 | 179 | #Console 180 | self.tab_console = QTabWidget() 181 | #self.console_widget = IPythonConsole(self, self.App) 182 | #self.code_edit = CodeEdit() 183 | 184 | #self.tab_console.addTab(self.console_widget, "Console") 185 | #self.tab_console.addTab(self.code_edit, "Editor") 186 | 187 | #self.console_widget_dockwidget = QDockWidget(self.tr("IPython")) 188 | #self.console_widget_dockwidget.setObjectName("Console-Dock") 189 | #self.console_widget_dockwidget.setWidget(self.tab_console) 190 | #self.console_widget_dockwidget.setAllowedAreas(Qt.BottomDockWidgetArea) 191 | #self.addDockWidget(Qt.BottomDockWidgetArea, self.console_widget_dockwidget) 192 | 193 | 194 | def create_toolbars(self): 195 | 196 | self.actionOpen_WorkSpace = createAction(self,"Set Workspace", self.setWorkSpace) 197 | self.actionOpen_WorkSpace.setIcon(self.style().standardIcon(QStyle.SP_DirIcon)) 198 | 199 | self.first_toolbar = QToolBar(self) 200 | self.first_toolbar.setObjectName("Workspace Toolbar") 201 | self.first_toolbar.setAllowedAreas(Qt.TopToolBarArea | Qt.BottomToolBarArea) 202 | 203 | self.workspaceLineEdit = QLineEdit() 204 | self.workspaceLineEdit.setMinimumWidth(200) 205 | 206 | self.first_toolbar.addWidget(QLabel("Workspace Dir")) 207 | self.first_toolbar.addWidget(self.workspaceLineEdit) 208 | self.first_toolbar.addAction(self.actionOpen_WorkSpace) 209 | 210 | self.addToolBar(self.first_toolbar) 211 | 212 | if self.dir_path is None: 213 | self.dir_path = os.getcwd() 214 | self.workspaceLineEdit.setText(self.dir_path) 215 | self.addToolBarBreak() 216 | 217 | def setup_graphicsview(self): 218 | 219 | self.tab_view = TabViewWidget(self) 220 | view = ViewWidget() 221 | self.tab_view.addTab(view, "Layout #1") 222 | self.setCentralWidget(self.tab_view) 223 | # 224 | self.datasets_widget.init_tree(view.model) 225 | 226 | def setup_statusbar(self): 227 | self.status = self.statusBar() 228 | self.status.setSizeGripEnabled(False) 229 | 230 | #Add more action 231 | 232 | def setWorkSpace(self): 233 | dir = QFileDialog.getExistingDirectory(None, self.tr("Set Workspace directory"), self.dir_path, QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) 234 | if dir: 235 | self.dir_path = dir 236 | self.workspaceLineEdit.setText(self.dir_path) 237 | 238 | def editWorkSpace(self): 239 | if os.path.isdir(self.workspaceLineEdit.text()): 240 | self.dir_path = self.workspaceLineEdit.text() 241 | 242 | def init_settings(self): 243 | 244 | settings = QSettings() 245 | self.recentFiles = settings.value("RecentFiles") 246 | size = settings.value("MainWindow/Size",QSize(900,600)) 247 | position = settings.value("MainWindow/Position",QPoint(50,50)) 248 | self.restoreState(settings.value("MainWindow/State")) 249 | self.dir_path = settings.value("DirPath") 250 | #Retrives more options 251 | 252 | if self.recentFiles is None: 253 | self.recentFiles = [] 254 | 255 | self.resize(size) 256 | self.move(position) 257 | 258 | #Add more actions 259 | self.workspaceLineEdit.setText(self.dir_path) 260 | 261 | def reset_settings(self): 262 | 263 | settings = QSettings() 264 | settings.clear() 265 | self.reset = True 266 | self.close() 267 | 268 | def init_toolboxes(self): 269 | 270 | if hasattr(sys, 'frozen'): 271 | #http://stackoverflow.com/questions/14750997/load-txt-file-from-resources-in-python 272 | fd = QFile(":/config_toolboxes.yaml") 273 | if fd.open(QIODevice.ReadOnly | QFile.Text): 274 | text = QTextStream(fd).readAll() 275 | fd.close() 276 | data = yaml.load(text) 277 | else: 278 | path = os.path.dirname(os.path.realpath(__file__)) 279 | with open(os.path.join(path,'resources', 'conf', 'config_toolboxes.yaml'), 'r') as f: 280 | # use safe_load instead load 281 | data = yaml.safe_load(f) 282 | 283 | #pp = pprint.PrettyPrinter() 284 | #pp.pprint(data) 285 | self.toolboxes_widget.init_tree(data) 286 | 287 | 288 | def ok_to_continue(self): 289 | 290 | if self.dirty: 291 | reply = QMessageBox.question(self, 292 | "%s - Unsaved Changes"%self.softwareName, 293 | "Save unsaved changes?", 294 | QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel) 295 | 296 | if reply == QMessageBox.Cancel: 297 | return False 298 | elif reply == QMessageBox.Yes: 299 | self.file_save() 300 | 301 | return True 302 | 303 | def file_new(self): 304 | pass 305 | 306 | def file_open(self): 307 | pass 308 | 309 | def file_saveAs(self): 310 | pass 311 | 312 | def file_save(self): 313 | pass 314 | 315 | def help_about(self): 316 | 317 | message = read_file(":/about.md").format(self.Info.version, self.Info.date) 318 | html = markdown2.markdown(str(message)) 319 | 320 | QMessageBox.about(self, "About %s"%NAME, html) 321 | 322 | def closeEvent(self, event): 323 | 324 | if self.reset: 325 | return 326 | 327 | if self.ok_to_continue(): 328 | settings = QSettings() 329 | filename = self.filename if self.filename is not None else None 330 | settings.setValue("LastFile", filename) 331 | recentFiles = self.recentFiles if self.recentFiles else None 332 | settings.setValue("RecentFiles", recentFiles) 333 | settings.setValue("MainWindow/Size", self.size()) 334 | settings.setValue("MainWindow/Position", self.pos()) 335 | settings.setValue("MainWindow/State", self.saveState()) 336 | settings.setValue("DirPath", self.dir_path) 337 | #Set more options 338 | 339 | else: 340 | event.ignore() 341 | 342 | 343 | if __name__=='__main__': 344 | import sys 345 | app = QApplication(sys.argv) 346 | win = MainWindowBase() 347 | win.show() 348 | app.exec_() 349 | 350 | 351 | -------------------------------------------------------------------------------- /pcloudpy/gui/ManagerLayer.py: -------------------------------------------------------------------------------- 1 | """ 2 | ManagerLayer Class 3 | """ 4 | #Author: Miguel Molero 5 | 6 | import copy 7 | 8 | class Layer(object): 9 | """ 10 | Layer Class 11 | 12 | Attributes 13 | ---------- 14 | 15 | _container: pcloudpy.core.io.base.DataBase 16 | 17 | _current_view: pcloudpy.gui.components.ViewWidget 18 | 19 | _is_visible: Bool 20 | 21 | _name: String 22 | 23 | 24 | """ 25 | def __init__(self, name): 26 | 27 | self._name = name 28 | self._is_visible = True 29 | self._current_view = None 30 | self._container = None 31 | 32 | def set_name(self, name): 33 | self._name = name 34 | 35 | def get_name(self): 36 | return self._name 37 | 38 | def set_current_view(self, view): 39 | self._current_view = view 40 | 41 | def get_current_view(self): 42 | return self._current_view 43 | 44 | def get_visibility(self): 45 | return self._is_visible 46 | 47 | def set_visibility(self, state): 48 | self._is_visible = state 49 | 50 | def set_container(self, container): 51 | self._container = container 52 | 53 | def get_container(self): 54 | if self._container: 55 | return self._container 56 | 57 | def copy(self): 58 | layer = copy.copy(self) 59 | layer.set_name(self._name + "_clone") 60 | layer.set_container(self._container.clone()) 61 | return layer 62 | 63 | 64 | class ManagerLayer(object): 65 | 66 | def __init__(self): 67 | self._layers = list() 68 | 69 | def layers(self): 70 | return self._layers 71 | 72 | def append(self, layer): 73 | self._layers.append(layer) 74 | 75 | def pop(self, index=-1): 76 | return self._layers.pop(index) 77 | 78 | def remove(self, layer): 79 | self._layers.remove(layer) 80 | 81 | def __add__(self, layer): 82 | self._layers.append(layer) 83 | return self 84 | 85 | def __getitem__(self, item): 86 | if item < len(self._layers): 87 | return self._layers[item] 88 | raise Exception("Empty List") 89 | 90 | 91 | if __name__== '__main__': 92 | 93 | manager = ManagerLayer() 94 | 95 | layer1 = Layer("layer") 96 | print(isinstance(layer1, Layer)) 97 | layer2 = Layer("layer") 98 | 99 | manager += layer1 100 | manager += layer2 101 | 102 | print(manager._layers) 103 | 104 | print(manager.pop(1)) 105 | print(manager._layers) 106 | -------------------------------------------------------------------------------- /pcloudpy/gui/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pcloudpy/gui/app.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | import sys 4 | from PyQt5.QtCore import * 5 | from PyQt5.QtGui import * 6 | from PyQt5.QtWidgets import * 7 | 8 | import time 9 | from pcloudpy.gui.resources_rc import * 10 | 11 | sys.setrecursionlimit(140000) 12 | 13 | def run(): 14 | app = QApplication(sys.argv) 15 | app.setOrganizationName("pcloudpy") 16 | app.setApplicationName("pcloudpy") 17 | app.setWindowIcon(QIcon(":/pcloudpy.png")) 18 | 19 | splash_pix = QPixmap(":/pcloudpy-name.png") 20 | splash = QSplashScreen(splash_pix, Qt.WindowStaysOnTopHint) 21 | splash.setMask(splash_pix.mask()) 22 | splash.show() 23 | 24 | from pcloudpy.gui.MainWindow import MainWindow 25 | app.processEvents() 26 | 27 | win = MainWindow() 28 | win.show() 29 | splash.finish(win) 30 | app.exec_() 31 | 32 | 33 | if __name__ == "__main__": 34 | run() -------------------------------------------------------------------------------- /pcloudpy/gui/components/DatasetsWidget.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | from PyQt5.QtCore import * 4 | from PyQt5.QtGui import * 5 | from PyQt5.QtWidgets import * 6 | from PyQt5.QtCore import pyqtSignal as Signal 7 | 8 | from pcloudpy.gui.utils.qhelpers import * 9 | 10 | 11 | class StandardItem(QStandardItem): 12 | def __init__(self, *args, **kwargs): 13 | super(StandardItem, self).__init__(*args, **kwargs) 14 | 15 | self._current_view = None 16 | self._layer = None 17 | 18 | def set_current_view(self, view): 19 | self._current_view = view 20 | 21 | def get_current_view(self): 22 | return self._current_view 23 | 24 | def set_layer(self, layer): 25 | self._layer = layer 26 | 27 | def get_layer(self): 28 | return self._layer 29 | 30 | class DatasetsWidget(QWidget): 31 | def __init__(self, parent = None): 32 | super(DatasetsWidget, self).__init__(parent) 33 | 34 | self._index = None 35 | self._enable_filter = False 36 | self.current_item = None 37 | layout = QVBoxLayout() 38 | self.tree = QTreeView() 39 | 40 | self.tree.setStyleSheet(""" 41 | QTreeView::indicator:unchecked {image: url(:/pqEyeballd16.png);} 42 | QTreeView::indicator:checked {image: url(:/pqEyeball16.png);}""") 43 | 44 | self.tree.setHeaderHidden(True) 45 | 46 | layout.addWidget(self.tree) 47 | self.setLayout(layout) 48 | 49 | self.tree.clicked.connect(self.clickedItem) 50 | self.tree.setContextMenuPolicy(Qt.CustomContextMenu) 51 | self.tree.customContextMenuRequested.connect(self.treeContextMenu) 52 | 53 | self.clone_dataset_action = QAction(QIcon(":/clone.png"), "Clone Dataset", self) 54 | self.clone_dataset_action.setStatusTip("Clone Dataset") 55 | self.clone_dataset_action.setToolTip("Clone Dataset") 56 | self.clone_dataset_action.triggered.connect(self.clone_dataset) 57 | 58 | self.delete_dataset_action = QAction(QIcon(":/trash_24.png"), "Delete Dataset", self) 59 | self.delete_dataset_action.setStatusTip("Delete Dataset") 60 | self.delete_dataset_action.setToolTip("Delete Dataset") 61 | self.delete_dataset_action.triggered.connect( self.delete_dataset) 62 | 63 | self.apply_filter_action = QAction("Apply Filter to Dataset", self) 64 | self.apply_filter_action.setStatusTip("Apply Filter to Dataset") 65 | self.apply_filter_action.setToolTip("Apply Filter to Dataset") 66 | self.apply_filter_action.triggered.connect(self.apply_filter) 67 | 68 | #Own Signals 69 | currentItemChanged = Signal(int) 70 | itemDeleted = Signal(int) 71 | itemCloneRequested = Signal(int) 72 | filterRequested = Signal(int) 73 | 74 | def init_tree(self, model): 75 | self.tree.setModel(model) 76 | 77 | def set_enable_filter(self): 78 | self._enable_filter = True 79 | 80 | def treeContextMenu(self, pos): 81 | self._index = self.tree.indexAt(pos) 82 | if self._index.row()>0: 83 | menu = QMenu() 84 | addActions(menu, self.delete_dataset_action) 85 | addActions(menu, self.clone_dataset_action) 86 | menu.addSeparator() 87 | if self._enable_filter: 88 | addActions(menu, self.apply_filter_action) 89 | menu.exec_(self.tree.mapToGlobal(pos)) 90 | 91 | def clickedItem(self, index): 92 | if index.row()>0: 93 | self.current_item = index.model().itemFromIndex(index) 94 | self.currentItemChanged.emit(index.row()-1) 95 | 96 | def add_dataset(self, layer, view): 97 | 98 | model = self.tree.model() 99 | root = model.invisibleRootItem() 100 | 101 | item = StandardItem(layer.get_name()) 102 | item.setIcon(QIcon(":/Toolbox-50.png")) 103 | 104 | item.set_current_view(view) 105 | item.set_layer(layer=layer) 106 | 107 | item.setCheckable(True) 108 | item.setCheckState(Qt.Checked) 109 | root.appendRow(item) 110 | 111 | def delete_dataset(self): 112 | if self._index.row()>0: 113 | self.current_item = self._index.model().itemFromIndex(self._index) 114 | model = self.tree.model() 115 | model.removeRow(self._index.row(), self._index.parent()) 116 | self.itemDeleted.emit(self._index.row()-1) 117 | 118 | def clone_dataset(self): 119 | if self._index.row()>0: 120 | self.current_item = self._index.model().itemFromIndex(self._index) 121 | self.itemCloneRequested.emit(self._index.row()-1) 122 | 123 | def apply_filter(self): 124 | if self._index.row()>0: 125 | self.current_item = self._index.model().itemFromIndex(self._index) 126 | self.filterRequested.emit(self._index.row()-1) -------------------------------------------------------------------------------- /pcloudpy/gui/components/FilterWidget.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | from PyQt5.QtCore import * 4 | from PyQt5.QtGui import * 5 | from PyQt5.QtWidgets import * 6 | from PyQt5.QtCore import pyqtSignal as Signal 7 | 8 | 9 | from .widgetGenerator import widget_generator 10 | 11 | class FilterWidget(QScrollArea): 12 | def __init__(self, *args, **kwargs): 13 | super(FilterWidget, self).__init__(*args, **kwargs) 14 | 15 | filterRequested = Signal() 16 | 17 | def set_filter(self, func, parms, text, only_apply=False): 18 | self.remove_filter() 19 | widget = widget_generator(func=func, parms=parms, text=text, only_apply=only_apply) 20 | apply = widget.findChild(QPushButton, "apply") 21 | if apply: 22 | apply.pressed.connect(self.apply_widget) 23 | self.setWidget(widget) 24 | 25 | def remove_filter(self): 26 | widget = self.takeWidget() 27 | del widget 28 | 29 | def apply_widget(self): 30 | print("apply") 31 | self.filterRequested.emit() 32 | 33 | def get_parms(self): 34 | widget = self.widget() 35 | return widget.get_parms() -------------------------------------------------------------------------------- /pcloudpy/gui/components/ObjectInspectorWidget.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | from PyQt5.QtCore import * 4 | from PyQt5.QtGui import * 5 | from PyQt5.QtWidgets import * 6 | 7 | class ObjectInspectorWidget(QWidget): 8 | def __init__(self, parent = None): 9 | super(ObjectInspectorWidget, self).__init__(parent) 10 | 11 | layout = QVBoxLayout() 12 | 13 | self.tab = QTabWidget() 14 | self.properties_tree = QTreeWidget() 15 | self.properties_tree.setHeaderLabels(["",""]) 16 | self.properties_tree.setAlternatingRowColors(True) 17 | self.properties_tree.setColumnCount(2) 18 | self.properties_tree.header().resizeSection(0, 200) 19 | 20 | self.tab.addTab(self.properties_tree, "Properties") 21 | 22 | layout.addWidget(self.tab) 23 | self.setLayout(layout) 24 | 25 | self.setGeometry(0,0,100, 400) 26 | 27 | def update(self, props): 28 | 29 | self.properties_tree.clear() 30 | data_tree = QTreeWidgetItem(self.properties_tree) 31 | data_tree.setText(0,"Data") 32 | #data_tree.setFont(0,QFont(c.FONT_NAME, c.FONT_SIZE_1, QFont.Bold)) 33 | 34 | labels = props.keys() 35 | values = props.values() 36 | self.populateTree(data_tree, labels, values) 37 | 38 | def populateTree(self, parent,labels,values): 39 | for i,j in zip(labels,values): 40 | if j is None: 41 | continue 42 | item = QTreeWidgetItem(parent) 43 | item.setText(0,i) 44 | #item.setFont(0,QFont(c.FONT_NAME, c.FONT_SIZE_2, QFont.Normal)) 45 | if isinstance(j,bool): 46 | if j is True: 47 | item.setText(1, c.MARK) 48 | else: 49 | item.setText(1, c.CROSS) 50 | else: 51 | item.setText(1,str(j)) 52 | 53 | #item.setFont(1,QFont(c.FONT_NAME, c.FONT_SIZE_3, QFont.Normal)) 54 | self.properties_tree.expandItem(parent) -------------------------------------------------------------------------------- /pcloudpy/gui/components/TabViewWidget.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from PyQt5.QtCore import * 4 | from PyQt5.QtGui import * 5 | from PyQt5.QtWidgets import * 6 | from PyQt5.QtCore import pyqtSignal as Signal 7 | 8 | 9 | class TabBarPlus(QTabBar): 10 | """Tab bar that has a plus button floating to the right of the tabs.""" 11 | 12 | plusClicked = Signal() 13 | 14 | def __init__(self): 15 | super(TabBarPlus, self).__init__() 16 | 17 | # Plus Button 18 | self.plusButton = QPushButton("+") 19 | self.plusButton.setParent(self) 20 | self.plusButton.setMaximumSize(30, 25) # Small Fixed size 21 | self.plusButton.setMinimumSize(30, 25) # Small Fixed size 22 | self.plusButton.clicked.connect(self.plusClicked.emit) 23 | self.movePlusButton() # Move to the correct location 24 | 25 | def sizeHint(self): 26 | """Return the size of the TabBar with increased width for the plus button.""" 27 | sizeHint = QTabBar.sizeHint(self) 28 | width = sizeHint.width() 29 | height = sizeHint.height() 30 | return QSize(width+30, height) 31 | 32 | def resizeEvent(self, event): 33 | """Resize the widget and make sure the plus button is in the correct location.""" 34 | super(TabBarPlus, self).resizeEvent(event) 35 | self.movePlusButton() 36 | 37 | def tabLayoutChange(self): 38 | """This virtual handler is called whenever the tab layout changes. 39 | If anything changes make sure the plus button is in the correct location. 40 | """ 41 | super(TabBarPlus, self).tabLayoutChange() 42 | 43 | self.movePlusButton() 44 | # end tabLayoutChange 45 | 46 | def movePlusButton(self): 47 | """Move the plus button to the correct location.""" 48 | # Find the width of all of the tabs 49 | size = 0 50 | for i in range(self.count()): 51 | size += self.tabRect(i).width() 52 | 53 | # Set the plus button location in a visible area 54 | h = self.geometry().top() 55 | w = self.width() 56 | if size > w: # Show just to the left of the scroll buttons 57 | self.plusButton.move(w-54, h) 58 | else: 59 | self.plusButton.move(size, h) 60 | 61 | class TabViewWidget(QTabWidget): 62 | 63 | def __init__(self, parent = None): 64 | super(TabViewWidget, self).__init__(parent) 65 | 66 | # Tab Bar 67 | self.tab = TabBarPlus() 68 | self.setTabBar(self.tab) 69 | 70 | # Properties 71 | self.setMovable(True) 72 | self.setTabsClosable(True) 73 | 74 | self.tabCloseRequested.connect(self.remove_tab) 75 | 76 | tabDeleted = Signal(int) 77 | 78 | def remove_tab(self, index): 79 | if index !=0: 80 | widget = self.widget(index) 81 | super(TabViewWidget, self).removeTab(index) 82 | del widget 83 | 84 | 85 | -------------------------------------------------------------------------------- /pcloudpy/gui/components/ToolboxesWidget.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | from PyQt5.QtCore import * 4 | from PyQt5.QtGui import * 5 | from PyQt5.QtWidgets import * 6 | from PyQt5.QtCore import pyqtSignal as Signal 7 | 8 | from ..resources_rc import * 9 | from .toolboxTreeWidgetItem import ToolBoxTreeWidgetItem 10 | from .toolboxStandardItem import ToolboxStandardItem 11 | 12 | 13 | class ToolBoxesWidget(QWidget): 14 | def __init__(self, parent = None): 15 | super(ToolBoxesWidget, self).__init__(parent) 16 | 17 | self._current_item = None 18 | 19 | layout = QVBoxLayout() 20 | self.tree = QTreeView() 21 | self.tree.setHeaderHidden(True) 22 | self.tree.setObjectName("Toolbox-Tree") 23 | layout.addWidget(self.tree) 24 | self.setLayout(layout) 25 | 26 | self.tree.clicked.connect(self.click_item) 27 | 28 | currentItemClicked = Signal() 29 | currentItemSelected = Signal() 30 | 31 | def init_tree(self, data): 32 | 33 | model = QStandardItemModel() 34 | root = model.invisibleRootItem() 35 | 36 | for key in data.keys(): 37 | item = QStandardItem(key) 38 | item.setIcon(QIcon(":/toolbox-icon.png")) 39 | item.setFlags(item.flags() & ~Qt.ItemIsEditable) 40 | root.appendRow(item) 41 | 42 | for child in data[key]: 43 | item_child = ToolboxStandardItem() 44 | item_child.setText(child) 45 | print(child) 46 | d = data[key][child] 47 | item_child.setEnabled(int(d['enabled'])) 48 | item_child.setMetadata(d) 49 | item.appendRow(item_child) 50 | 51 | self.tree.setModel(model) 52 | 53 | ##self.connect(self.tree.selectionModel(), SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), self.select_item) 54 | self.tree.selectionModel().selectionChanged.connect(self.select_item) 55 | 56 | 57 | def get_current_item(self): 58 | if self._current_item: 59 | return self._current_item 60 | 61 | def click_item(self, index): 62 | self._current_item = index.model().itemFromIndex(index) 63 | if isinstance(self._current_item, ToolboxStandardItem): 64 | if self._current_item.isEnabled(): 65 | self.currentItemClicked.emit() 66 | 67 | def select_item(self, new_item, _): 68 | items = new_item.indexes() 69 | if len(items)!=0: 70 | index = new_item.indexes()[0] 71 | #self.click_item(index) 72 | self._current_item = index.model().itemFromIndex(index) 73 | if isinstance(self._current_item, ToolboxStandardItem): 74 | if self._current_item.isEnabled(): 75 | self.currentItemSelected.emit() 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /pcloudpy/gui/components/ViewWidget.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | import numpy as np 4 | 5 | 6 | from PyQt5.QtCore import * 7 | from PyQt5.QtGui import * 8 | from PyQt5.QtWidgets import * 9 | 10 | from PyQt5.QtCore import pyqtSignal as Signal 11 | 12 | 13 | from vtk import vtkInteractorStyleImage, vtkInteractorStyleTrackballCamera, vtkActor, vtkImageActor 14 | from vtk import vtkAreaPicker, vtkContourWidget, vtkOrientedGlyphContourRepresentation, vtkImplicitSelectionLoop 15 | from vtk import vtkCleanPolyData, vtkImplicitBoolean, vtkDataSetSurfaceFilter, vtkExtractGeometry, vtkExtractPolyDataGeometry 16 | 17 | from ..graphics.QVTKWidget import QVTKWidget 18 | 19 | from ..utils.qhelpers import * 20 | from ..ManagerLayer import ManagerLayer 21 | 22 | from pcloudpy.core.utils.vtkhelpers import actor_from_polydata 23 | 24 | 25 | class QVTKWidgetKeyEvents(QVTKWidget): 26 | def __init__(self, parent = None): 27 | super(QVTKWidgetKeyEvents, self).__init__(parent) 28 | 29 | keyEventRequested = Signal(int) 30 | 31 | def keyPressEvent(self, ev): 32 | super(QVTKWidgetKeyEvents, self).keyPressEvent(ev) 33 | self.keyEventRequested.emit(ev.key()) 34 | 35 | 36 | class ViewWidget(QWidget): 37 | def __init__(self, parent=None): 38 | super(ViewWidget,self).__init__(parent) 39 | 40 | self.setAttribute(Qt.WA_DeleteOnClose) 41 | 42 | self._is_extract = False 43 | self._contour_widget = None 44 | self.manager_layer = ManagerLayer() 45 | self._current_layer = None 46 | 47 | layout = QVBoxLayout() 48 | layout.setContentsMargins(0,0,0,0) 49 | 50 | self.toolbar = QToolBar() 51 | self.toolbar.setStyleSheet(""" 52 | QToolBar { border: 0px } 53 | """) 54 | self.toolbar.setIconSize(QSize(16,16)) 55 | 56 | self.vtkWidget = QVTKWidgetKeyEvents(self) 57 | self.vtkWidget.keyEventRequested.connect(self.key_press_event) 58 | 59 | layout.addWidget(self.toolbar) 60 | layout.addWidget(self.vtkWidget) 61 | self.setLayout(layout) 62 | 63 | self.setup_toolbar() 64 | self.init_model() 65 | 66 | layersModified = Signal() 67 | 68 | def setup_toolbar(self): 69 | 70 | self.reset_view_action = QAction(QIcon(":/pqResetCamera32.png"), "Reset View/Camera", self) 71 | self.reset_view_action.setStatusTip("Reset View/Camera") 72 | self.reset_view_action.setToolTip("Reset View/Camera") 73 | self.reset_view_action.triggered.connect(self.vtkWidget.reset_view) 74 | 75 | self.set_view_direction_to_mx_action = QAction(QIcon(":/pqXMinus24.png"), "View Direction -x", self) 76 | self.set_view_direction_to_mx_action.setStatusTip("View Direction -x") 77 | self.set_view_direction_to_mx_action.setToolTip("View Direction -x") 78 | self.set_view_direction_to_mx_action.triggered.connect(self.vtkWidget.viewMX) 79 | 80 | self.set_view_direction_to_my_action = QAction(QIcon(":/pqYMinus24.png"), "View Direction -y", self) 81 | self.set_view_direction_to_my_action.setStatusTip("View Direction -y") 82 | self.set_view_direction_to_my_action.setToolTip("View Direction -y") 83 | self.set_view_direction_to_my_action.triggered.connect(self.vtkWidget.viewMY) 84 | 85 | self.set_view_direction_to_mz_action = QAction(QIcon(":/pqZMinus24.png"), "View Direction -z", self) 86 | self.set_view_direction_to_mz_action.setStatusTip("View Direction -z") 87 | self.set_view_direction_to_mz_action.setToolTip("View Direction -z") 88 | self.set_view_direction_to_mz_action.triggered.connect(self.vtkWidget.viewMZ) 89 | 90 | self.set_view_direction_to_px_action = QAction(QIcon(":/pqXPlus24.png"), "View Direction +x", self) 91 | self.set_view_direction_to_px_action.setStatusTip("View Direction +x") 92 | self.set_view_direction_to_px_action.setToolTip("View Direction +x") 93 | self.set_view_direction_to_px_action.triggered.connect(self.vtkWidget.viewPX) 94 | 95 | self.set_view_direction_to_py_action = QAction(QIcon(":/pqYPlus24.png"), "View Direction +y", self) 96 | self.set_view_direction_to_py_action.setStatusTip("View Direction +y") 97 | self.set_view_direction_to_py_action.setToolTip("View Direction +y") 98 | self.set_view_direction_to_py_action.triggered.connect(self.vtkWidget.viewPY) 99 | 100 | self.set_view_direction_to_pz_action = QAction(QIcon(":/pqZPlus24.png"), "View Direction +z", self) 101 | self.set_view_direction_to_pz_action.setStatusTip("View Direction +z") 102 | self.set_view_direction_to_pz_action.setToolTip("View Direction +z") 103 | self.set_view_direction_to_pz_action.triggered.connect(self.vtkWidget.viewPZ) 104 | 105 | self.selection_action = QAction(QIcon(":/pqSelectSurfPoints24.png"), "Select Points", self) 106 | self.selection_action.setStatusTip("Select Points") 107 | self.selection_action.setToolTip("Select Points") 108 | self.selection_action.triggered.connect(self.select_points) 109 | 110 | self.extract_action = QAction(QIcon(":/pqExtractSelection.png"), "Extract Selected Points", self) 111 | self.extract_action.setStatusTip("Extract Selected Points") 112 | self.extract_action.setToolTip("Extract Selected Points") 113 | self.extract_action.triggered.connect(self.extract_points) 114 | 115 | self.clean_action = createAction(self, "Clean Selected Points", self.clean_points, icon="clean.png") 116 | 117 | self.clean_action = QAction(QIcon(":/clean.png"), "Clean Selected Points", self) 118 | self.clean_action .setStatusTip("Clean Selected Points") 119 | self.clean_action .setToolTip("Clean Selected Points") 120 | self.clean_action .triggered.connect(self.clean_points) 121 | 122 | 123 | addActions(self.toolbar, self.reset_view_action) 124 | addActions(self.toolbar, self.set_view_direction_to_mx_action) 125 | addActions(self.toolbar, self.set_view_direction_to_my_action) 126 | addActions(self.toolbar, self.set_view_direction_to_mz_action) 127 | addActions(self.toolbar, self.set_view_direction_to_px_action) 128 | addActions(self.toolbar, self.set_view_direction_to_py_action) 129 | addActions(self.toolbar, self.set_view_direction_to_pz_action) 130 | self.toolbar.addSeparator() 131 | addActions(self.toolbar, self.selection_action) 132 | addActions(self.toolbar, self.extract_action) 133 | addActions(self.toolbar, self.clean_action) 134 | 135 | self.extract_action.setEnabled(False) 136 | self.clean_action.setEnabled(False) 137 | 138 | def init_model(self): 139 | self.model = QStandardItemModel() 140 | root = self.model.invisibleRootItem() 141 | item = QStandardItem("build:") 142 | item.setIcon(QIcon(":/pqServer16.png")) 143 | item.setFlags(item.flags() & ~Qt.ItemIsEditable) 144 | root.appendRow(item) 145 | 146 | def set_interactor_style_image(self): 147 | self.vtkWidget.set_interactor_style(vtkInteractorStyleImage()) 148 | 149 | def set_interactor_style_trackball(self): 150 | self.vtkWidget.set_interactor_style(vtkInteractorStyleTrackballCamera()) 151 | 152 | def add_layer(self, layer): 153 | self.manager_layer += layer 154 | self.add_actor(layer.get_container().get_actor()) 155 | self._current_layer = layer 156 | 157 | def set_current_layer(self, layer): 158 | self._current_layer =layer 159 | 160 | def add_actor(self, actor): 161 | if isinstance(actor, vtkActor): 162 | self.vtkWidget.renderer.AddActor(actor) 163 | self.set_interactor_style_trackball() 164 | 165 | elif isinstance(actor, vtkImageActor): 166 | self.vtkWidget.renderer.AddActor2D(actor) 167 | self.set_interactor_style_image() 168 | 169 | def remove_actor(self, actor): 170 | self.vtkWidget.renderer.RemoveActor(actor) 171 | self.update_render() 172 | 173 | def update_render(self): 174 | self.vtkWidget.reset_view() 175 | self.vtkWidget.Render() 176 | self.vtkWidget.show() 177 | 178 | def select_points(self): 179 | self._is_extract = not self._is_extract 180 | if self._is_extract: 181 | 182 | self.setCursor(QCursor(Qt.CrossCursor)) 183 | 184 | self._contour_widget = vtkContourWidget() 185 | self._contour_widget.SetInteractor(self.vtkWidget.get_interactor()) 186 | self._contour_representation = vtkOrientedGlyphContourRepresentation() 187 | 188 | self._contour_widget.SetRepresentation(self._contour_representation) 189 | self._contour_representation.GetLinesProperty().SetColor(1,1,0) 190 | self._contour_representation.GetLinesProperty().SetLineWidth(2.0) 191 | self._contour_representation.GetLinesProperty().SetPointSize(10.0) 192 | self._contour_representation.SetAlwaysOnTop(1) 193 | self._contour_widget.EnabledOn() 194 | 195 | else: 196 | self.vtkWidget.get_interactor().GetRenderWindow().SetCurrentCursor(0) 197 | self._contour_widget.CloseLoop() 198 | self._contour_widget.ProcessEventsOff() 199 | self.vtkWidget.get_interactor().GetRenderWindow().Render() 200 | 201 | self.extract_action.setEnabled(True) 202 | self.clean_action.setEnabled(True) 203 | 204 | def _extract_polygon(self, PolyData, is_clean): 205 | 206 | self._contour_widget.EnabledOff() 207 | print("Init Extracting") 208 | 209 | self.setCursor(QCursor(Qt.WaitCursor)) 210 | QApplication.processEvents() 211 | 212 | polydata_rep = self._contour_representation.GetContourRepresentationAsPolyData() 213 | planes = self.get_frustrum() 214 | normal = planes.GetNormals() 215 | 216 | nor = np.array([0,0,0]) 217 | normal.GetTuple(5, nor) 218 | 219 | #progressBar.setValue(10) 220 | #QApplication.processEvents() 221 | 222 | selection = vtkImplicitSelectionLoop() 223 | selection.SetLoop(polydata_rep.GetPoints()) 224 | selection.SetNormal(nor[0], nor[1], nor[2]) 225 | 226 | #progressBar.setValue(20) 227 | #QApplication.processEvents() 228 | 229 | tip = vtkImplicitBoolean() 230 | tip.AddFunction(selection) 231 | tip.AddFunction(planes) 232 | tip.SetOperationTypeToIntersection() 233 | tip.Modified() 234 | 235 | #progressBar.setValue(40) 236 | #QApplication.processEvents() 237 | 238 | if is_clean: 239 | extractGeometry = vtkExtractPolyDataGeometry() 240 | else: 241 | extractGeometry = vtkExtractGeometry() 242 | 243 | extractGeometry.SetInputData(PolyData) 244 | extractGeometry.SetImplicitFunction(tip) 245 | 246 | if is_clean: 247 | extractGeometry.ExtractInsideOff() 248 | extractGeometry.Update() 249 | 250 | if is_clean: 251 | clean = vtkCleanPolyData() 252 | clean.SetInputConnection(extractGeometry.GetOutputPort()) 253 | clean.Update() 254 | #progressBar.setValue(80) 255 | #QApplication.processEvents() 256 | 257 | filter = vtkDataSetSurfaceFilter() 258 | if is_clean: 259 | filter.SetInputConnection(clean.GetOutputPort()) 260 | else: 261 | filter.SetInputConnection(extractGeometry.GetOutputPort()) 262 | filter.Update() 263 | 264 | #progressBar.setValue(90) 265 | #QApplication.processEvents() 266 | 267 | self.setCursor(QCursor(Qt.ArrowCursor)) 268 | QApplication.processEvents() 269 | self.extract_action.setEnabled(False) 270 | self.clean_action.setEnabled(False) 271 | 272 | print("End Extracting") 273 | return filter.GetOutput() 274 | 275 | def extract_points(self): 276 | self._apply_extraction(is_clean=False) 277 | 278 | def clean_points(self): 279 | self._apply_extraction(is_clean=True) 280 | 281 | def _apply_extraction(self, is_clean): 282 | if self._is_extract: 283 | self._is_extract = False 284 | self.vtkWidget.get_interactor().GetRenderWindow().SetCurrentCursor(0) 285 | self._contour_widget.CloseLoop() 286 | self._contour_widget.ProcessEventsOff() 287 | self.vtkWidget.get_interactor().GetRenderWindow().Render() 288 | 289 | #for idx, layer in enumerate(self.manager_layer.layers()): 290 | layer = self._current_layer 291 | actor = layer.get_container().get_actor() 292 | 293 | if actor.GetVisibility(): 294 | polydata = self._extract_polygon(actor.GetMapper().GetInput(), is_clean=is_clean) 295 | self.vtkWidget.renderer.RemoveActor(actor) 296 | actor = actor_from_polydata(polydata) 297 | layer.get_container().update_data_from(polydata) 298 | layer.get_container().set_actor(actor) 299 | #todo 300 | #update data -> numpy_from_polydata 301 | 302 | self.add_actor(actor) 303 | 304 | self.update_render() 305 | 306 | def get_frustrum(self): 307 | 308 | render = self._contour_representation.GetRenderer() 309 | numberNodes = self._contour_representation.GetNumberOfNodes() 310 | 311 | V = list() 312 | for i in range(numberNodes): 313 | v = np.array([0,0]) 314 | self._contour_representation.GetNthNodeDisplayPosition(i, v) 315 | V.append(v) 316 | 317 | xmin = np.min(np.array(V)[:,0]) 318 | ymin = np.min(np.array(V)[:,1]) 319 | xmax = np.max(np.array(V)[:,0]) 320 | ymax = np.max(np.array(V)[:,1]) 321 | 322 | p1 = np.array([xmin, ymax]) 323 | p2 = np.array([xmax, ymin]) 324 | 325 | picker = vtkAreaPicker() 326 | picker.AreaPick( p1[0], p1[1], p2[0], p2[1], render) 327 | 328 | planes = picker.GetFrustum() 329 | return planes 330 | 331 | def key_press_event(self, key): 332 | if key == Qt.Key_Escape: 333 | if isinstance(self._contour_widget, vtkContourWidget): 334 | self.vtkWidget.get_interactor().GetRenderWindow().SetCurrentCursor(0) 335 | self._contour_widget.ProcessEventsOff() 336 | self._contour_widget.SetEnabled(0) 337 | self.vtkWidget.get_interactor().GetRenderWindow().Render() -------------------------------------------------------------------------------- /pcloudpy/gui/components/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Miguel' 2 | -------------------------------------------------------------------------------- /pcloudpy/gui/components/customWidgets.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtGui import * 3 | from PyQt5.QtWidgets import * 4 | from PyQt5.QtCore import pyqtSignal as Signal 5 | 6 | class LineEdit(QLineEdit): 7 | def __init__(self, parent=None): 8 | super(LineEdit, self).__init__(parent) 9 | self.setMaximumWidth(60) 10 | self.setText(str(0)) 11 | 12 | class SpinBox(QSpinBox): 13 | def __init__(self, parent=None): 14 | super(SpinBox, self).__init__(parent) 15 | 16 | def set_values(self, vmin, vmax, step, value): 17 | self.setRange(int(vmin), int(vmax)) 18 | self.setSingleStep(int(step)) 19 | self.setValue(int(value)) 20 | 21 | def get_values(self): 22 | return self.value() 23 | 24 | 25 | class DoubleSpinBox(QDoubleSpinBox): 26 | def __init__(self, parent=None): 27 | super(DoubleSpinBox, self).__init__(parent) 28 | 29 | def set_values(self, vmin, vmax, step, value): 30 | self.setRange(vmin, vmax) 31 | self.setSingleStep(step) 32 | self.setValue(value) 33 | 34 | def get_values(self): 35 | return self.value() 36 | 37 | 38 | class CheckBox(QCheckBox): 39 | def __init__(self, parent=None): 40 | super(CheckBox, self).__init__(parent) 41 | 42 | def set_values(self, value): 43 | self.setChecked(value) 44 | 45 | def get_values(self): 46 | return self.isChecked() 47 | 48 | class Extent(QWidget): 49 | 50 | def __init__(self, parent=None): 51 | super(Extent, self).__init__(parent) 52 | 53 | self.xmin_lineedit = LineEdit() 54 | self.xmax_lineedit = LineEdit() 55 | self.ymin_lineedit = LineEdit() 56 | self.ymax_lineedit = LineEdit() 57 | self.zmin_lineedit = LineEdit() 58 | self.zmax_lineedit = LineEdit() 59 | 60 | grid = QGridLayout() 61 | grid.addWidget(QLabel("xmin"), 0,0) 62 | grid.addWidget(self.xmin_lineedit, 0,1) 63 | grid.addWidget(QLabel("xmax"), 0,2) 64 | grid.addWidget(self.xmax_lineedit, 0,3) 65 | grid.addWidget(QLabel("ymin"), 1,0) 66 | grid.addWidget(self.ymin_lineedit, 1,1) 67 | grid.addWidget(QLabel("ymax"), 1,2) 68 | grid.addWidget(self.ymax_lineedit, 1,3) 69 | grid.addWidget(QLabel("zmin"), 2,0) 70 | grid.addWidget(self.zmin_lineedit, 2,1) 71 | grid.addWidget(QLabel("zmax"),2,2 ) 72 | grid.addWidget(self.zmax_lineedit, 2,3) 73 | 74 | self.setLayout(grid) 75 | 76 | def get_extent(self): 77 | 78 | return (float(self.xmin_lineedit.text()), float(self.xmax_lineedit.text()), 79 | float(self.ymin_lineedit.text()), float(self.ymax_lineedit.text()), 80 | float(self.zmin_lineedit.text()), float(self.zmax_lineedit.text())) -------------------------------------------------------------------------------- /pcloudpy/gui/components/toolboxStandardItem.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | from PyQt5.QtCore import * 4 | from PyQt5.QtGui import * 5 | from PyQt5.QtWidgets import * 6 | 7 | class ToolboxStandardItem(QStandardItem): 8 | def __init__(self, *args, **kwargs): 9 | super(ToolboxStandardItem, self).__init__(*args, **kwargs) 10 | 11 | self._metadata = None 12 | self.setFlags(self.flags() & ~Qt.ItemIsEditable) 13 | self.setIcon(QIcon(":/Toolbox-50.png")) 14 | self._objectName = None 15 | self._metadata = None 16 | 17 | def setText(self, text): 18 | super(ToolboxStandardItem, self).setText(text) 19 | self.setObjectName(text.replace(" ","-")) 20 | 21 | def setMetadata(self, metadata): 22 | self._metadata = metadata 23 | 24 | def metadata(self): 25 | return self._metadata 26 | 27 | def setObjectName(self, name): 28 | self._objectName = name 29 | 30 | def objectName(self): 31 | return self._objectName 32 | 33 | -------------------------------------------------------------------------------- /pcloudpy/gui/components/toolboxTreeWidgetItem.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | 3 | from PyQt5.QtCore import * 4 | from PyQt5.QtGui import * 5 | from PyQt5.QtWidgets import * 6 | 7 | class ToolBoxTreeWidgetItem(QTreeWidgetItem): 8 | def __init__(self, parent=None, type=QTreeWidgetItem.UserType + 2): 9 | super(ToolBoxTreeWidgetItem, self).__init__(parent, type) 10 | 11 | self.setIcon(0,QIcon(":/Toolbox-50.png")) 12 | self._objectName = None 13 | self._metadata = None 14 | 15 | def setText(self, column, text): 16 | super(ToolBoxTreeWidgetItem, self).setText(column, text) 17 | self.setObjectName(text.replace(" ","-")) 18 | 19 | def setMetadata(self, metadata): 20 | self._metadata = metadata 21 | 22 | def metadata(self): 23 | return self._metadata 24 | 25 | 26 | def setObjectName(self, name): 27 | self._objectName = name 28 | 29 | def objectName(self): 30 | return self._objectName 31 | -------------------------------------------------------------------------------- /pcloudpy/gui/components/widgetGenerator.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | import markdown2 3 | 4 | from PyQt5.QtCore import * 5 | from PyQt5.QtGui import * 6 | from PyQt5.QtWidgets import * 7 | from PyQt5.QtCore import pyqtSignal as Signal 8 | 9 | from pcloudpy.gui.components import customWidgets 10 | 11 | 12 | def widget_generator(func, parms, text="", only_apply=False): 13 | 14 | class TemplateWidget(QWidget): 15 | def __init__(self, parent=None, func=None, parms=None, text="", only_apply=False): 16 | super(TemplateWidget, self).__init__(parent) 17 | self.parms = None 18 | self.func = func 19 | self.only_apply = only_apply 20 | 21 | if only_apply: 22 | self.init_apply(text) 23 | else: 24 | if parms is not None: 25 | self.init_params(parms, text) 26 | else: 27 | self.init_text(text) 28 | 29 | def init_apply(self, text): 30 | grid = QGridLayout() 31 | 32 | self.apply_button = QPushButton("Apply") 33 | self.apply_button.setObjectName("apply") 34 | self.apply_button.setFixedSize(60,60) 35 | grid.addWidget(self.apply_button, 0, 0) 36 | 37 | html = markdown2.markdown(str(text)) 38 | textEdit = QTextEdit(html) 39 | textEdit.setMinimumWidth(350) 40 | textEdit.setMinimumHeight(350) 41 | textEdit.setReadOnly(True) 42 | grid.addWidget(textEdit,1, 0, 1, 5) 43 | 44 | self.setLayout(grid) 45 | 46 | 47 | def init_params(self, parms, text): 48 | self.parms = dict(parms) 49 | 50 | grid = QGridLayout() 51 | index = 0 52 | for (k,v) in parms.items(): 53 | 54 | if v['type'] == "Extent": 55 | item = customWidgets.Extent() 56 | item.setObjectName(k) 57 | grid.addWidget(item, index, 0, 1, 1) 58 | else: 59 | item = getattr(customWidgets, v['type'])() 60 | item.setObjectName(k) 61 | item.set_values(*map(float,v['values'].strip().split(','))) 62 | item.setToolTip(v.get('tooltip', "")) 63 | 64 | grid.addWidget(QLabel(k), index, 0, 1, 1) 65 | grid.addWidget(item, index, 1, 1, 1) 66 | 67 | index += 1 68 | 69 | self.apply_button = QPushButton("Apply") 70 | self.apply_button.setObjectName("apply") 71 | self.apply_button.setFixedSize(60,60) 72 | 73 | grid.addWidget(self.apply_button, 0, 3, index, 3) 74 | 75 | html = markdown2.markdown(str(text)) 76 | textEdit = QTextEdit(html) 77 | textEdit.setMinimumWidth(350) 78 | textEdit.setMinimumHeight(350) 79 | textEdit.setReadOnly(True) 80 | grid.addWidget(textEdit,index, 0, 1, 5) 81 | 82 | self.setLayout(grid) 83 | 84 | def init_text(self, text): 85 | 86 | html = markdown2.markdown(str(text)) 87 | 88 | textEdit = QTextEdit(html) 89 | textEdit.setMinimumWidth(350) 90 | textEdit.setMinimumHeight(350) 91 | textEdit.setReadOnly(True) 92 | 93 | vBox = QVBoxLayout() 94 | vBox.addWidget(textEdit) 95 | 96 | self.setLayout(vBox) 97 | 98 | def get_parms(self): 99 | 100 | if self.only_apply: 101 | return self.func, dict() 102 | 103 | if self.parms: 104 | d = dict() 105 | for (k,v) in self.parms.items(): 106 | if v['type'] =="Extent": 107 | d[k] = self.findChild(customWidgets.Extent, k).get_extent() 108 | else: 109 | item = self.findChild(getattr(customWidgets, v['type']), k) 110 | d[k] = item.get_values() 111 | return self.func, d 112 | 113 | return TemplateWidget(None, func, parms, text=text, only_apply=only_apply) 114 | -------------------------------------------------------------------------------- /pcloudpy/gui/graphics/QVTKRenderWindowInteractor.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple VTK widget for PyQt or PySide. 3 | See http://www.trolltech.com for Qt documentation, 4 | http://www.riverbankcomputing.co.uk for PyQt, and 5 | http://pyside.github.io for PySide. 6 | 7 | This class is based on the vtkGenericRenderWindowInteractor and is 8 | therefore fairly powerful. It should also play nicely with the 9 | vtk3DWidget code. 10 | 11 | Created by Prabhu Ramachandran, May 2002 12 | Based on David Gobbi's QVTKRenderWidget.py 13 | 14 | Changes by Gerard Vermeulen Feb. 2003 15 | Win32 support. 16 | 17 | Changes by Gerard Vermeulen, May 2003 18 | Bug fixes and better integration with the Qt framework. 19 | 20 | Changes by Phil Thompson, Nov. 2006 21 | Ported to PyQt v4. 22 | Added support for wheel events. 23 | 24 | Changes by Phil Thompson, Oct. 2007 25 | Bug fixes. 26 | 27 | Changes by Phil Thompson, Mar. 2008 28 | Added cursor support. 29 | 30 | Changes by Rodrigo Mologni, Sep. 2013 (Credit to Daniele Esposti) 31 | Bug fix to PySide: Converts PyCObject to void pointer. 32 | 33 | Changes by Greg Schussman, Aug. 2014 34 | The keyPressEvent function now passes keysym instead of None. 35 | 36 | Changes by Alex Tsui, Apr. 2015 37 | Port from PyQt4 to PyQt5. 38 | 39 | Changes by Fabian Wenzel, Jan. 2016 40 | Support for Python3 41 | """ 42 | 43 | # Check whether a specific PyQt implementation was chosen 44 | try: 45 | import vtk.qt 46 | PyQtImpl = vtk.qt.PyQtImpl 47 | except ImportError: 48 | pass 49 | 50 | # Check whether a specific QVTKRenderWindowInteractor base 51 | # class was chosen, can be set to "QGLWidget" 52 | QVTKRWIBase = "QWidget" 53 | try: 54 | import vtk.qt 55 | QVTKRWIBase = vtk.qt.QVTKRWIBase 56 | except ImportError: 57 | pass 58 | 59 | if PyQtImpl is None: 60 | # Autodetect the PyQt implementation to use 61 | try: 62 | import PyQt5 63 | PyQtImpl = "PyQt5" 64 | except ImportError: 65 | try: 66 | import PyQt4 67 | PyQtImpl = "PyQt4" 68 | except ImportError: 69 | try: 70 | import PySide 71 | PyQtImpl = "PySide" 72 | except ImportError: 73 | raise ImportError("Cannot load either PyQt or PySide") 74 | 75 | if PyQtImpl == "PyQt5": 76 | if QVTKRWIBase == "QGLWidget": 77 | from PyQt5.QtOpenGL import QGLWidget 78 | from PyQt5.QtWidgets import QWidget 79 | from PyQt5.QtWidgets import QSizePolicy 80 | from PyQt5.QtWidgets import QApplication 81 | from PyQt5.QtCore import Qt 82 | from PyQt5.QtCore import QTimer 83 | from PyQt5.QtCore import QObject 84 | from PyQt5.QtCore import QSize 85 | from PyQt5.QtCore import QEvent 86 | elif PyQtImpl == "PyQt4": 87 | if QVTKRWIBase == "QGLWidget": 88 | from PyQt4.QtOpenGL import QGLWidget 89 | from PyQt4.QtGui import QWidget 90 | from PyQt4.QtGui import QSizePolicy 91 | from PyQt4.QtGui import QApplication 92 | from PyQt4.QtCore import Qt 93 | from PyQt4.QtCore import QTimer 94 | from PyQt4.QtCore import QObject 95 | from PyQt4.QtCore import QSize 96 | from PyQt4.QtCore import QEvent 97 | elif PyQtImpl == "PySide": 98 | if QVTKRWIBase == "QGLWidget": 99 | from PySide.QtOpenGL import QGLWidget 100 | from PySide.QtGui import QWidget 101 | from PySide.QtGui import QSizePolicy 102 | from PySide.QtGui import QApplication 103 | from PySide.QtCore import Qt 104 | from PySide.QtCore import QTimer 105 | from PySide.QtCore import QObject 106 | from PySide.QtCore import QSize 107 | from PySide.QtCore import QEvent 108 | else: 109 | raise ImportError("Unknown PyQt implementation " + repr(PyQtImpl)) 110 | 111 | # Define types for base class, based on string 112 | if QVTKRWIBase == "QWidget": 113 | QVTKRWIBaseClass = QWidget 114 | elif QVTKRWIBase == "QGLWidget": 115 | QVTKRWIBaseClass = QGLWidget 116 | else: 117 | raise ImportError("Unknown base class for QVTKRenderWindowInteractor " + QVTKRWIBase) 118 | 119 | class QVTKRenderWindowInteractor(QVTKRWIBaseClass): 120 | 121 | """ A QVTKRenderWindowInteractor for Python and Qt. Uses a 122 | vtkGenericRenderWindowInteractor to handle the interactions. Use 123 | GetRenderWindow() to get the vtkRenderWindow. Create with the 124 | keyword stereo=1 in order to generate a stereo-capable window. 125 | 126 | The user interface is summarized in vtkInteractorStyle.h: 127 | 128 | - Keypress j / Keypress t: toggle between joystick (position 129 | sensitive) and trackball (motion sensitive) styles. In joystick 130 | style, motion occurs continuously as long as a mouse button is 131 | pressed. In trackball style, motion occurs when the mouse button 132 | is pressed and the mouse pointer moves. 133 | 134 | - Keypress c / Keypress o: toggle between camera and object 135 | (actor) modes. In camera mode, mouse events affect the camera 136 | position and focal point. In object mode, mouse events affect 137 | the actor that is under the mouse pointer. 138 | 139 | - Button 1: rotate the camera around its focal point (if camera 140 | mode) or rotate the actor around its origin (if actor mode). The 141 | rotation is in the direction defined from the center of the 142 | renderer's viewport towards the mouse position. In joystick mode, 143 | the magnitude of the rotation is determined by the distance the 144 | mouse is from the center of the render window. 145 | 146 | - Button 2: pan the camera (if camera mode) or translate the actor 147 | (if object mode). In joystick mode, the direction of pan or 148 | translation is from the center of the viewport towards the mouse 149 | position. In trackball mode, the direction of motion is the 150 | direction the mouse moves. (Note: with 2-button mice, pan is 151 | defined as -Button 1.) 152 | 153 | - Button 3: zoom the camera (if camera mode) or scale the actor 154 | (if object mode). Zoom in/increase scale if the mouse position is 155 | in the top half of the viewport; zoom out/decrease scale if the 156 | mouse position is in the bottom half. In joystick mode, the amount 157 | of zoom is controlled by the distance of the mouse pointer from 158 | the horizontal centerline of the window. 159 | 160 | - Keypress 3: toggle the render window into and out of stereo 161 | mode. By default, red-blue stereo pairs are created. Some systems 162 | support Crystal Eyes LCD stereo glasses; you have to invoke 163 | SetStereoTypeToCrystalEyes() on the rendering window. Note: to 164 | use stereo you also need to pass a stereo=1 keyword argument to 165 | the constructor. 166 | 167 | - Keypress e: exit the application. 168 | 169 | - Keypress f: fly to the picked point 170 | 171 | - Keypress p: perform a pick operation. The render window interactor 172 | has an internal instance of vtkCellPicker that it uses to pick. 173 | 174 | - Keypress r: reset the camera view along the current view 175 | direction. Centers the actors and moves the camera so that all actors 176 | are visible. 177 | 178 | - Keypress s: modify the representation of all actors so that they 179 | are surfaces. 180 | 181 | - Keypress u: invoke the user-defined function. Typically, this 182 | keypress will bring up an interactor that you can type commands in. 183 | 184 | - Keypress w: modify the representation of all actors so that they 185 | are wireframe. 186 | """ 187 | 188 | # Map between VTK and Qt cursors. 189 | _CURSOR_MAP = { 190 | 0: Qt.ArrowCursor, # VTK_CURSOR_DEFAULT 191 | 1: Qt.ArrowCursor, # VTK_CURSOR_ARROW 192 | 2: Qt.SizeBDiagCursor, # VTK_CURSOR_SIZENE 193 | 3: Qt.SizeFDiagCursor, # VTK_CURSOR_SIZENWSE 194 | 4: Qt.SizeBDiagCursor, # VTK_CURSOR_SIZESW 195 | 5: Qt.SizeFDiagCursor, # VTK_CURSOR_SIZESE 196 | 6: Qt.SizeVerCursor, # VTK_CURSOR_SIZENS 197 | 7: Qt.SizeHorCursor, # VTK_CURSOR_SIZEWE 198 | 8: Qt.SizeAllCursor, # VTK_CURSOR_SIZEALL 199 | 9: Qt.PointingHandCursor, # VTK_CURSOR_HAND 200 | 10: Qt.CrossCursor, # VTK_CURSOR_CROSSHAIR 201 | } 202 | 203 | def __init__(self, parent=None, **kw): 204 | # the current button 205 | self._ActiveButton = Qt.NoButton 206 | 207 | # private attributes 208 | self.__saveX = 0 209 | self.__saveY = 0 210 | self.__saveModifiers = Qt.NoModifier 211 | self.__saveButtons = Qt.NoButton 212 | self.__wheelDelta = 0 213 | 214 | # do special handling of some keywords: 215 | # stereo, rw 216 | 217 | try: 218 | stereo = bool(kw['stereo']) 219 | except KeyError: 220 | stereo = False 221 | 222 | try: 223 | rw = kw['rw'] 224 | except KeyError: 225 | rw = None 226 | 227 | # create base qt-level widget 228 | if QVTKRWIBase == "QWidget": 229 | if "wflags" in kw: 230 | wflags = kw['wflags'] 231 | else: 232 | wflags = Qt.WindowFlags() 233 | QWidget.__init__(self, parent, wflags | Qt.MSWindowsOwnDC) 234 | elif QVTKRWIBase == "QGLWidget": 235 | QGLWidget.__init__(self, parent) 236 | 237 | if rw: # user-supplied render window 238 | self._RenderWindow = rw 239 | else: 240 | self._RenderWindow = vtk.vtkRenderWindow() 241 | 242 | WId = self.winId() 243 | 244 | # Python2 245 | if type(WId).__name__ == 'PyCObject': 246 | from ctypes import pythonapi, c_void_p, py_object 247 | 248 | pythonapi.PyCObject_AsVoidPtr.restype = c_void_p 249 | pythonapi.PyCObject_AsVoidPtr.argtypes = [py_object] 250 | 251 | WId = pythonapi.PyCObject_AsVoidPtr(WId) 252 | 253 | # Python3 254 | elif type(WId).__name__ == 'PyCapsule': 255 | from ctypes import pythonapi, c_void_p, py_object, c_char_p 256 | 257 | pythonapi.PyCapsule_GetName.restype = c_char_p 258 | pythonapi.PyCapsule_GetName.argtypes = [py_object] 259 | 260 | name = pythonapi.PyCapsule_GetName(WId) 261 | 262 | pythonapi.PyCapsule_GetPointer.restype = c_void_p 263 | pythonapi.PyCapsule_GetPointer.argtypes = [py_object, c_char_p] 264 | 265 | WId = pythonapi.PyCapsule_GetPointer(WId, name) 266 | 267 | self._RenderWindow.SetWindowInfo(str(int(WId))) 268 | 269 | if stereo: # stereo mode 270 | self._RenderWindow.StereoCapableWindowOn() 271 | self._RenderWindow.SetStereoTypeToCrystalEyes() 272 | 273 | try: 274 | self._Iren = kw['iren'] 275 | except KeyError: 276 | self._Iren = vtk.vtkGenericRenderWindowInteractor() 277 | self._Iren.SetRenderWindow(self._RenderWindow) 278 | 279 | # do all the necessary qt setup 280 | self.setAttribute(Qt.WA_OpaquePaintEvent) 281 | self.setAttribute(Qt.WA_PaintOnScreen) 282 | self.setMouseTracking(True) # get all mouse events 283 | self.setFocusPolicy(Qt.WheelFocus) 284 | self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) 285 | 286 | self._Timer = QTimer(self) 287 | self._Timer.timeout.connect(self.TimerEvent) 288 | 289 | self._Iren.AddObserver('CreateTimerEvent', self.CreateTimer) 290 | self._Iren.AddObserver('DestroyTimerEvent', self.DestroyTimer) 291 | self._Iren.GetRenderWindow().AddObserver('CursorChangedEvent', 292 | self.CursorChangedEvent) 293 | 294 | #Create a hidden child widget and connect its destroyed signal to its 295 | #parent ``Finalize`` slot. The hidden children will be destroyed before 296 | #its parent thus allowing cleanup of VTK elements. 297 | self._hidden = QWidget(self) 298 | self._hidden.hide() 299 | self._hidden.destroyed.connect(self.Finalize) 300 | 301 | def __getattr__(self, attr): 302 | """Makes the object behave like a vtkGenericRenderWindowInteractor""" 303 | if attr == '__vtk__': 304 | return lambda t=self._Iren: t 305 | elif hasattr(self._Iren, attr): 306 | return getattr(self._Iren, attr) 307 | else: 308 | raise AttributeError(self.__class__.__name__ + 309 | " has no attribute named " + attr) 310 | 311 | def Finalize(self): 312 | ''' 313 | Call internal cleanup method on VTK objects 314 | ''' 315 | self._RenderWindow.Finalize() 316 | 317 | def CreateTimer(self, obj, evt): 318 | self._Timer.start(10) 319 | 320 | def DestroyTimer(self, obj, evt): 321 | self._Timer.stop() 322 | return 1 323 | 324 | def TimerEvent(self): 325 | self._Iren.TimerEvent() 326 | 327 | def CursorChangedEvent(self, obj, evt): 328 | """Called when the CursorChangedEvent fires on the render window.""" 329 | # This indirection is needed since when the event fires, the current 330 | # cursor is not yet set so we defer this by which time the current 331 | # cursor should have been set. 332 | QTimer.singleShot(0, self.ShowCursor) 333 | 334 | def HideCursor(self): 335 | """Hides the cursor.""" 336 | self.setCursor(Qt.BlankCursor) 337 | 338 | def ShowCursor(self): 339 | """Shows the cursor.""" 340 | vtk_cursor = self._Iren.GetRenderWindow().GetCurrentCursor() 341 | qt_cursor = self._CURSOR_MAP.get(vtk_cursor, Qt.ArrowCursor) 342 | self.setCursor(qt_cursor) 343 | 344 | def closeEvent(self, evt): 345 | self.Finalize() 346 | 347 | def sizeHint(self): 348 | return QSize(400, 400) 349 | 350 | def paintEngine(self): 351 | return None 352 | 353 | def paintEvent(self, ev): 354 | self._Iren.Render() 355 | 356 | def resizeEvent(self, ev): 357 | w = self.width() 358 | h = self.height() 359 | vtk.vtkRenderWindow.SetSize(self._RenderWindow, w, h) 360 | self._Iren.SetSize(w, h) 361 | self._Iren.ConfigureEvent() 362 | self.update() 363 | 364 | def _GetCtrlShift(self, ev): 365 | ctrl = shift = False 366 | 367 | if hasattr(ev, 'modifiers'): 368 | if ev.modifiers() & Qt.ShiftModifier: 369 | shift = True 370 | if ev.modifiers() & Qt.ControlModifier: 371 | ctrl = True 372 | else: 373 | if self.__saveModifiers & Qt.ShiftModifier: 374 | shift = True 375 | if self.__saveModifiers & Qt.ControlModifier: 376 | ctrl = True 377 | 378 | return ctrl, shift 379 | 380 | def enterEvent(self, ev): 381 | ctrl, shift = self._GetCtrlShift(ev) 382 | self._Iren.SetEventInformationFlipY(self.__saveX, self.__saveY, 383 | ctrl, shift, chr(0), 0, None) 384 | self._Iren.EnterEvent() 385 | 386 | def leaveEvent(self, ev): 387 | ctrl, shift = self._GetCtrlShift(ev) 388 | self._Iren.SetEventInformationFlipY(self.__saveX, self.__saveY, 389 | ctrl, shift, chr(0), 0, None) 390 | self._Iren.LeaveEvent() 391 | 392 | def mousePressEvent(self, ev): 393 | ctrl, shift = self._GetCtrlShift(ev) 394 | repeat = 0 395 | if ev.type() == QEvent.MouseButtonDblClick: 396 | repeat = 1 397 | self._Iren.SetEventInformationFlipY(ev.x(), ev.y(), 398 | ctrl, shift, chr(0), repeat, None) 399 | 400 | self._ActiveButton = ev.button() 401 | 402 | if self._ActiveButton == Qt.LeftButton: 403 | self._Iren.LeftButtonPressEvent() 404 | elif self._ActiveButton == Qt.RightButton: 405 | self._Iren.RightButtonPressEvent() 406 | elif self._ActiveButton == Qt.MidButton: 407 | self._Iren.MiddleButtonPressEvent() 408 | 409 | def mouseReleaseEvent(self, ev): 410 | ctrl, shift = self._GetCtrlShift(ev) 411 | self._Iren.SetEventInformationFlipY(ev.x(), ev.y(), 412 | ctrl, shift, chr(0), 0, None) 413 | 414 | if self._ActiveButton == Qt.LeftButton: 415 | self._Iren.LeftButtonReleaseEvent() 416 | elif self._ActiveButton == Qt.RightButton: 417 | self._Iren.RightButtonReleaseEvent() 418 | elif self._ActiveButton == Qt.MidButton: 419 | self._Iren.MiddleButtonReleaseEvent() 420 | 421 | def mouseMoveEvent(self, ev): 422 | self.__saveModifiers = ev.modifiers() 423 | self.__saveButtons = ev.buttons() 424 | self.__saveX = ev.x() 425 | self.__saveY = ev.y() 426 | 427 | ctrl, shift = self._GetCtrlShift(ev) 428 | self._Iren.SetEventInformationFlipY(ev.x(), ev.y(), 429 | ctrl, shift, chr(0), 0, None) 430 | self._Iren.MouseMoveEvent() 431 | 432 | def keyPressEvent(self, ev): 433 | ctrl, shift = self._GetCtrlShift(ev) 434 | if ev.key() < 256: 435 | key = str(ev.text()) 436 | else: 437 | key = chr(0) 438 | 439 | keySym = _qt_key_to_key_sym(ev.key()) 440 | if shift and len(keySym) == 1 and keySym.isalpha(): 441 | keySym = keySym.upper() 442 | 443 | self._Iren.SetEventInformationFlipY(self.__saveX, self.__saveY, 444 | ctrl, shift, key, 0, keySym) 445 | self._Iren.KeyPressEvent() 446 | self._Iren.CharEvent() 447 | 448 | def keyReleaseEvent(self, ev): 449 | ctrl, shift = self._GetCtrlShift(ev) 450 | if ev.key() < 256: 451 | key = chr(ev.key()) 452 | else: 453 | key = chr(0) 454 | 455 | self._Iren.SetEventInformationFlipY(self.__saveX, self.__saveY, 456 | ctrl, shift, key, 0, None) 457 | self._Iren.KeyReleaseEvent() 458 | 459 | def wheelEvent(self, ev): 460 | if hasattr(ev, 'delta'): 461 | self.__wheelDelta += ev.delta() 462 | else: 463 | self.__wheelDelta += ev.angleDelta().y() 464 | 465 | if self.__wheelDelta >= 120: 466 | self._Iren.MouseWheelForwardEvent() 467 | self.__wheelDelta = 0 468 | elif self.__wheelDelta <= -120: 469 | self._Iren.MouseWheelBackwardEvent() 470 | self.__wheelDelta = 0 471 | 472 | def GetRenderWindow(self): 473 | return self._RenderWindow 474 | 475 | def Render(self): 476 | self.update() 477 | 478 | -------------------------------------------------------------------------------- /pcloudpy/gui/graphics/QVTKWidget.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic Window handling custom operations from QVTKRenderWindowInteractor 3 | """ 4 | #Author: Miguel Molero 5 | 6 | from PyQt5.QtCore import * 7 | from PyQt5.QtGui import * 8 | from PyQt5.QtWidgets import * 9 | 10 | import numpy as np 11 | from vtk import vtkWindowToImageFilter, vtkPNGWriter 12 | from vtk import vtkPoints, vtkCellArray, vtkUnsignedCharArray, vtkPolyData, vtkPolyDataMapper, vtkActor 13 | from vtk import vtkRenderer, vtkRenderWindow, vtkRenderWindowInteractor,vtkGenericRenderWindowInteractor, vtkInteractorStyleTrackballCamera 14 | from vtk import vtkCellPicker, vtkAreaPicker, vtkCleanPolyData, vtkDataSetSurfaceFilter 15 | from vtk import vtkExtractPolyDataGeometry, vtkExtractGeometry, vtkImplicitSelectionLoop, vtkImplicitBoolean 16 | from vtk import vtkOrientedGlyphContourRepresentation, vtkContourWidget, vtkOrientationMarkerWidget, vtkAxesActor 17 | 18 | from .QVTKRenderWindowInteractor import QVTKRenderWindowInteractor 19 | 20 | class QVTKWidget(QVTKRenderWindowInteractor): 21 | 22 | def __init__(self, parent=None): 23 | super(QVTKWidget, self).__init__(parent) 24 | self._enable_axis = False 25 | self._Iren.SetInteractorStyle (vtkInteractorStyleTrackballCamera ()) 26 | 27 | 28 | self.renderer = vtkRenderer() 29 | self.renderer.GradientBackgroundOn() 30 | self.renderer.SetBackground2(255.0,255.0,255.0) 31 | self.renderer.SetBackground(37/255.0, 85/255.0,152/255.0) 32 | 33 | self.GetRenderWindow().AddRenderer(self.renderer) 34 | self.Initialize() 35 | self.Start() 36 | self.add_axes() 37 | 38 | def keyPressEvent(self, ev): 39 | QVTKRenderWindowInteractor.keyPressEvent(self, ev) 40 | if ev.key() < 256: 41 | key = str(ev.text()) 42 | else: 43 | # Has modifiers, but an ASCII key code. 44 | #key = chr(ev.key()) 45 | key = chr(0) 46 | 47 | def add_axes(self): 48 | self.axis_widget = vtkOrientationMarkerWidget() 49 | axes = vtkAxesActor() 50 | axes.SetShaftTypeToLine() 51 | axes.SetTotalLength(0.5, 0.5, 0.5) 52 | self.axis_widget.SetOutlineColor(0.9300,0.5700,0.1300) 53 | self.axis_widget.SetOrientationMarker(axes) 54 | self.axis_widget.SetInteractor(self._Iren) 55 | self.axis_widget.SetViewport(0.80, 0.0, 1.0,0.25) 56 | 57 | self._enable_axis = True 58 | self.axis_widget.SetEnabled(self._enable_axis) 59 | self.axis_widget.InteractiveOff() 60 | 61 | def camera_control(self, roll, pitch, yaw): 62 | 63 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().Elevation(pitch) 64 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().Roll(roll) 65 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().Azimuth(yaw) 66 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().OrthogonalizeViewUp() 67 | 68 | self.Render() 69 | self.show() 70 | 71 | def viewMX(self): 72 | 73 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetPosition(0, 0, 0) 74 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetFocalPoint(-1, 0, 0) 75 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetViewUp(0, 0, 1) 76 | 77 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().ResetCamera() 78 | self.Render() 79 | self.show() 80 | 81 | def viewMY(self): 82 | 83 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetPosition(0, 0, 0) 84 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetFocalPoint(0, -1, 0) 85 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetViewUp(0, 0, 1) 86 | 87 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().ResetCamera() 88 | self.Render() 89 | self.show() 90 | 91 | def viewMZ(self): 92 | 93 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetPosition(0, 0, 0) 94 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetFocalPoint(0, 0, -1) 95 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetViewUp(0, 1, 0) 96 | 97 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().ResetCamera() 98 | self.Render() 99 | self.show() 100 | 101 | def viewPX(self): 102 | 103 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetPosition(0, 0, 0) 104 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetFocalPoint(1, 0, 0) 105 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetViewUp(0, 0, 1) 106 | 107 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().ResetCamera() 108 | self.Render() 109 | self.show() 110 | 111 | def viewPY(self): 112 | 113 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetPosition(0, 0, 0) 114 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetFocalPoint(0, 1, 0) 115 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetViewUp(0, 0, 1) 116 | 117 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().ResetCamera() 118 | self.Render() 119 | self.show() 120 | 121 | def viewPZ(self): 122 | 123 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetPosition(0, 0, 0) 124 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetFocalPoint(0, 0, 1) 125 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().GetActiveCamera().SetViewUp(0, 1, 0) 126 | 127 | self._Iren.GetRenderWindow().GetRenderers().GetFirstRenderer().ResetCamera() 128 | self.Render() 129 | self.show() 130 | 131 | def reset_view(self): 132 | 133 | self.renderer.ResetCamera() 134 | self.Render() 135 | self.show() 136 | 137 | def set_interactor_style(self, style): 138 | self.renderer.ResetCamera() 139 | self._Iren.SetInteractorStyle(style) 140 | 141 | def get_interactor(self): 142 | return self._Iren 143 | 144 | 145 | 146 | if __name__ == "__main__": 147 | 148 | from vtk import vtkConeSource 149 | 150 | app = QtGui.QApplication(['QVTKWidget']) 151 | 152 | renderer = vtkRenderer() 153 | renderer.GradientBackgroundOn() 154 | renderer.SetBackground2(0.0,0.0,0.0) 155 | renderer.SetBackground(128/255.0, 128/255.0,128/255.0) 156 | 157 | vtkWidget = QVTKWidget() 158 | vtkWidget.GetRenderWindow().AddRenderer(renderer) 159 | 160 | vtkWidget.Initialize() 161 | vtkWidget.Start() 162 | vtkWidget.add_axes() 163 | 164 | cone = vtkConeSource() 165 | cone.SetResolution(8) 166 | coneMapper = vtkPolyDataMapper() 167 | coneMapper.SetInput(cone.GetOutput()) 168 | coneActor = vtkActor() 169 | coneActor.SetMapper(coneMapper) 170 | renderer.AddActor(coneActor) 171 | 172 | # show the widget 173 | vtkWidget.show() 174 | # start event processing 175 | app.exec_() -------------------------------------------------------------------------------- /pcloudpy/gui/graphics/QVTKWindow.py: -------------------------------------------------------------------------------- 1 | #Author: Miguel Molero 2 | from PyQt5.QtCore import * 3 | from PyQt5.QtGui import * 4 | from PyQt5.QtWidgets import * 5 | 6 | from pcloudpy.gui.graphics.QVTKWidget import QVTKWidget 7 | 8 | class QVTKMainWindow(QMainWindow): 9 | def __init__(self, parent = None): 10 | super(QVTKMainWindow, self).__init__(parent) 11 | self.vtkWidget = QVTKWidget(self) 12 | self.setCentralWidget(self.vtkWidget) 13 | self.setWindowTitle("QVTKMainWindow") 14 | self.setGeometry(50,50, 800,800) 15 | 16 | 17 | if __name__ == "__main__": 18 | 19 | from vtk import vtkConeSource 20 | from vtk import vtkPolyDataMapper, vtkActor 21 | 22 | app = QApplication(['QVTKWindow']) 23 | 24 | win = QVTKMainWindow() 25 | 26 | cone = vtkConeSource() 27 | cone.SetResolution(8) 28 | coneMapper = vtkPolyDataMapper() 29 | coneMapper.SetInput(cone.GetOutput()) 30 | coneActor = vtkActor() 31 | coneActor.SetMapper(coneMapper) 32 | 33 | win.vtkWidget.renderer.AddActor(coneActor) 34 | # show the widget 35 | win.show() 36 | # start event processing 37 | app.exec_() -------------------------------------------------------------------------------- /pcloudpy/gui/graphics/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pcloudpy/gui/resources/about.md: -------------------------------------------------------------------------------- 1 | **pcloudpy** v{0}, date: {1} 2 | 3 | Point Cloud Viewer and Processing Toolkit in Python 4 | 5 | core developer: miguel.molero@gmail.com 6 | 7 | see mmolero.github.io -------------------------------------------------------------------------------- /pcloudpy/gui/resources/conf/config_toolboxes.yaml: -------------------------------------------------------------------------------- 1 | Import Data: 2 | 3 | XYZ File: 4 | type: reader 5 | name: XYZ File 6 | message: Open XYZ File 7 | format: "*.xyz;;*.txt" 8 | func: ReaderXYZ 9 | enabled: 1 10 | text : "**Open XYZ File**" 11 | 12 | LAS File: 13 | type: reader 14 | name: LAS File 15 | message: Open LAS File 16 | format: "*.las" 17 | func: ReaderLAS 18 | enabled: 1 19 | text : "**Open LAS File**" 20 | 21 | VTK File: 22 | type: reader 23 | name: VTK File 24 | format: .vtp 25 | message: Open VTP File 26 | format: "*.vtp" 27 | func: ReaderVTP 28 | enabled: 1 29 | text : "Open VTP File" 30 | 31 | PLY File: 32 | type: reader 33 | name: PLY File 34 | format: "*.ply" 35 | message: Open PLY File 36 | func: ReaderPLY 37 | enabled: 1 38 | text : "Open PLY File" 39 | 40 | TIFF File: 41 | type: reader 42 | name: TIFF File 43 | format: "*.tif;; *.tiff" 44 | message: Open TIFF File 45 | func: ReaderTIFF 46 | enabled: 1 47 | text : "Open TIFF File" 48 | 49 | Filters: 50 | 51 | Statistical Outlier Removal Filter: 52 | type: filter 53 | enabled: 1 54 | name: Statistical-Outlier-Removal-Filter 55 | parms: 56 | mean_k: 57 | type: "SpinBox" 58 | values: "1, 200, 5, 1" 59 | tooltip: | 60 | Set the number of nearest neighbors to use for mean distance estimation. 61 | 62 | std_dev: 63 | type: "DoubleSpinBox" 64 | values: "0, 20, 1, 1" 65 | tooltip: | 66 | Set the standard deviation multiplier for the distance threshold calculation. 67 | The distance threshold will be equal to: mean + std_dev * std. 68 | Points will be classified as inlier or outlier if their average neighbor distance is below or above this threshold respectively. 69 | 70 | 71 | text : | 72 | 73 | **Statistical Outlier Removal Filter** uses point neighborhood statistics to filter outlier data. 74 | The algorithm iterates through the entire input twice: 75 | 76 | - During the first iteration it will update the average distance that each point has to its nearest k neighbors. The value of k can be set using mean-k. 77 | Next, the mean and standard deviation of all these distances are computed in order to determine a distance threshold. 78 | The distance threshold will be equal to: mean + std_dev * std. The multiplier for the standard deviation can be set using std_dev_mult. 79 | 80 | - During the next iteration the points will be classified as inlier or outlier if their average neighbor distance is below or above this threshold respectively 81 | 82 | **mean_k**: 83 | Set the number of nearest neighbors to use for mean distance estimation. 84 | 85 | **std_dev**: 86 | Set the standard deviation multiplier for the distance threshold calculation. 87 | The distance threshold will be equal to: mean + std_dev_mult * std. 88 | Points will be classified as inlier or outlier if their average neighbor distance is below or above this threshold respectively. 89 | 90 | 91 | For more information. 92 | 93 | Statistical Outlier Removal Filter is a python implementation based on the pcl::StatisticalOutlierRemoval from Point Cloud Library 94 | class pcl::StatisticalOutlierRemoval (Radu Bogdan Rusu) 95 | 96 | R. B. Rusu, Z. C. Marton, N. Blodow, M. Dolha, and M. Beetz. 97 | Towards 3D Point Cloud Based Object Maps for Household Environments Robotics and Autonomous Systems Journal (Special Issue on Semantic Knowledge), 2008. 98 | 99 | func: StatisticalOutlierRemovalFilter 100 | 101 | 102 | Extract Extent Filter: 103 | type: filter 104 | enabled: 1 105 | name: Extract-Extent-Filter 106 | parms: 107 | extent: 108 | type: "Extent" 109 | text : | 110 | **Extract Extent Filter** uses a given extent to retrieve the points inside it. 111 | 112 | func: ExtractPolyData 113 | 114 | 115 | 116 | Delaunay 2D: 117 | type: filter 118 | name: Delaunay2D 119 | enabled: 1 120 | parms: 121 | alpha: 122 | type: "DoubleSpinBox" 123 | values: "0, 10000, 1, 1" 124 | tooltip: | 125 | Specify alpha (or distance) value to control output of this filter. 126 | For a non-zero alpha value, only edges or triangles contained within a sphere centered at mesh vertices will be output. 127 | Otherwise, only triangles will be output. 128 | tolerance: 129 | type: "DoubleSpinBox" 130 | values: "0.01, 10, 0.01, 0.01" 131 | tooltip: | 132 | Specify a tolerance to control discarding of closely spaced points. 133 | This tolerance is specified as a fraction of the diagonal length of the bounding box of the points. 134 | 135 | text : | 136 | **Delaunay2D** is a filter that constructs a 2D Delaunay triangulation from a list of input points. 137 | see vtkDelaunay2D Documentation. 138 | 139 | The output of the filter is a polygonal dataset. Usually the output is a triangle mesh, 140 | but if a non-zero alpha distance value is specified (called the "alpha" value), then only triangles, edges, and vertices laying within the alpha radius are output. 141 | In other words, non-zero alpha values may result in arbitrary combinations of triangles, lines, and vertices. 142 | (The notion of alpha value is derived from Edelsbrunner's work on "alpha shapes".) 143 | 144 | **alpha**: 145 | Specify alpha (or distance) value to control output of this filter. 146 | For a non-zero alpha value, only edges or triangles contained within a sphere centered at mesh vertices will be output. 147 | Otherwise, only triangles will be output. 148 | 149 | **tolerance**: 150 | Specify a tolerance to control discarding of closely spaced points. This tolerance is specified as a fraction of the diagonal length of the bounding box of the points 151 | 152 | The 2D Delaunay triangulation is defined as the triangulation that satisfies the Delaunay criterion for n-dimensional simplexes 153 | (in this case n=2 and the simplexes are triangles). 154 | This criterion states that a circumsphere of each simplex in a triangulation contains only the n+1 defining points of the simplex. 155 | (See "The Visualization Toolkit" text for more information.) 156 | In two dimensions, this translates into an optimal triangulation. 157 | That is, the maximum interior angle of any triangle is less than or equal to that of any possible triangulation. 158 | 159 | The Delaunay triangulation can be numerically sensitive in some cases. 160 | To prevent problems, try to avoid injecting points that will result in triangles with bad aspect ratios (1000:1 or greater). 161 | In practice this means inserting points that are "widely dispersed", and enables smooth transition of triangle sizes throughout the mesh. 162 | (You may even want to add extra points to create a better point distribution.) 163 | If numerical problems are present, you will see a warning message to this effect at the end of the triangulation process. 164 | 165 | 166 | func: Delaunay2D 167 | 168 | Delaunay 3D: 169 | type: filter 170 | name: Delaunay3D 171 | enabled: 1 172 | 173 | parms : 174 | alpha: 175 | type: "DoubleSpinBox" 176 | values: "0, 10000, 1, 1" 177 | tooltip: | 178 | Specify alpha (or distance) value to control output of this filter. 179 | For a non-zero alpha value, only verts, edges, faces, or tetra contained within the circumsphere (of radius alpha) will be output. 180 | Otherwise, only tetrahedra will be output. 181 | tolerance: 182 | type: "DoubleSpinBox" 183 | values: "0.01, 10, 0.01, 0.01" 184 | tooltip: | 185 | Specify a tolerance to control discarding of closely spaced points. 186 | This tolerance is specified as a fraction of the diagonal length of the bounding box of the points 187 | 188 | 189 | text : | 190 | **Delaunay3D** is a filter that constructs a 3D Delaunay triangulation from a list of input points. 191 | 192 | The output of the filter is an unstructured grid dataset. 193 | Usually the output is a tetrahedral mesh, but if a non-zero alpha distance value is specified (called the "alpha" value), 194 | then only tetrahedra, triangles, edges, and vertices laying within the alpha radius are output. 195 | In other words, non-zero alpha values may result in arbitrary combinations of tetrahedra, triangles, lines, and vertices. 196 | (The notion of alpha value is derived from Edelsbrunner's work on "alpha shapes".) 197 | Note that a modification to alpha shapes enables output of combinations of tetrahedra, triangles, lines, and/or verts 198 | (see the boolean ivars AlphaTets, AlphaTris, AlphaLines, AlphaVerts). 199 | 200 | **alpha**: 201 | Specify alpha (or distance) value to control output of this filter. 202 | For a non-zero alpha value, only verts, edges, faces, or tetra contained within the circumsphere (of radius alpha) will be output. 203 | Otherwise, only tetrahedra will be output. 204 | 205 | **tolerance**: 206 | Specify a tolerance to control discarding of closely spaced points. This tolerance is specified as a fraction of the diagonal length of the bounding box of the points 207 | 208 | The 3D Delaunay triangulation is defined as the triangulation that satisfies the Delaunay criterion for n-dimensional simplexes 209 | (in this case n=3 and the simplexes are tetrahedra). 210 | This criterion states that a circumsphere of each simplex in a triangulation contains only the n+1 defining points of the simplex. 211 | (See "The Visualization Toolkit" for more information.) 212 | While in two dimensions this translates into an "optimal" triangulation, this is not true in 3D, since a measurement for optimality in 3D is not agreed on. 213 | 214 | 215 | 216 | func: Delaunay3D 217 | 218 | Point Set Normals Estimation (vtkPointSetNormalsEstimation): 219 | type: filter 220 | name: vtkPointSetNormalsEstimation 221 | enabled: 1 222 | parms: 223 | number_neighbors: 224 | type: "SpinBox" 225 | values: "1, 100, 1, 1" 226 | 227 | text: | 228 | **vtkPointSetNormalEstimation** filter estimates normals of a point set using a local best fit plane. 229 | 230 | At every point in the point set, vtkPointSetNormalEstimation computes the best 231 | fit plane of the set of points within a specified radius of the point (or a fixed number of neighbors). 232 | 233 | The normal of this plane is used as an estimate of the normal of the surface that would go through 234 | the points. 235 | 236 | vtkPointSetNormalEstimation Class is a python implementation based on the version included in PointSetProcessing by 237 | David Doria, see (https://github.com/daviddoria/PointSetProcessing) 238 | 239 | func: vtkPointSetNormalsEstimation 240 | 241 | Normals Estimation: 242 | 243 | type: filter 244 | name: NormalsEstimation 245 | enabled: 1 246 | parms: 247 | number_neighbors: 248 | type: "SpinBox" 249 | values: "1,100,1,1" 250 | 251 | text: | 252 | **Normals Estimation** filter estimates normals of a point cloud using PCA Eigen method to fit plane 253 | 254 | func: NormalsEstimation 255 | 256 | Oriented Normals Estimation: 257 | 258 | type: filter 259 | name: OrientedNormalsEstimation 260 | enabled: 1 261 | parms: 262 | number_neighbors: 263 | type: "SpinBox" 264 | values: "1,100,1,1" 265 | 266 | text: | 267 | ** Oriented Normals Estimation** filter estimates normals of a point cloud using PCA Eigen method to fit plane and minimum spanning tree 268 | 269 | func: OrientedNormalsEstimation 270 | 271 | 272 | Point Set Outlier Estimation: 273 | type: filter 274 | name: vtkPointSetOutlierEstimation 275 | enabled: 1 276 | parms: 277 | percent_to_remove: 278 | type: "DoubleSpinBox" 279 | values: "0,1,0.1,0.1" 280 | tooltip: | 281 | We sort these distances and keep points whose nearest point is in a certain percentile 282 | of the entire point set. 283 | 284 | 285 | text: | 286 | Outlier Removal - vtkPointSetOutlierRemoval, 287 | Python implementation based on Point Set Processing for VTK by David Doria 288 | see: 289 | https://github.com/daviddoria/PointSetProcessing 290 | http://www.vtkjournal.org/browse/publication/708 291 | 292 | We take the simple definition of an outlier to be a point that is farther away from its nearest neighbor than 293 | expected. To implement this definition, for every point p in the point set, we compute the distance from p to 294 | the nearest point to p. We sort these distances and keep points whose nearest point is in a certain percentile 295 | of the entire point set. 296 | This parameter is specified by the user as **percent_to_remove** 297 | 298 | func: vtkPointSetOutlierEstimation 299 | 300 | 301 | 302 | Normals Visualization: 303 | type: display 304 | name: Normals Visualization 305 | enabled: 1 306 | text: Normal Vector Visualization. First, Normal estimation must be done 307 | func: DisplayNormals 308 | 309 | 310 | Surface Reconstruction: 311 | 312 | Screened Poisson Reconstruction: 313 | type: filter 314 | name: ScreenedPoisson 315 | enabled: 1 316 | func: ScreenedPoisson 317 | parms: 318 | depth: 319 | type: "SpinBox" 320 | values: "2,16,1,8" 321 | tooltip: "" 322 | 323 | full_depth: 324 | type: "SpinBox" 325 | values: "2,16,1,5" 326 | tooltip: "" 327 | 328 | scale: 329 | type: "DoubleSpinBox" 330 | values: "0.1, 2.0, 0.1, 1.1" 331 | tooltip: "" 332 | 333 | samples_per_node: 334 | type: "SpinBox" 335 | values: "1,20,1,1" 336 | tooltip: "" 337 | 338 | cg_depth: 339 | type: "SpinBox" 340 | values: "0, 16, 1, 0" 341 | tooltip: "" 342 | 343 | enable_polygon_mesh: 344 | type: "CheckBox" 345 | values: "0" 346 | tooltip: "" 347 | 348 | enable_density: 349 | type: "CheckBox" 350 | values: "0" 351 | tooltip: "" 352 | 353 | text: | 354 | **Screened Poisson Surface Reconstruction** (Version 6.13) 355 | 356 | Requires Point Cloud with oriented normals 357 | 358 | more information, see: 359 | 360 | http://www.cs.jhu.edu/~misha/Code/PoissonRecon/Version6.13/ 361 | https://github.com/mmolero/pypoisson 362 | 363 | **depth**: 364 | This integer is the maximum depth of the tree that will be used for surface reconstruction. 365 | Running at depth d corresponds to solving on a voxel grid whose resolution is no larger than 2^d x 2^d x 2^d. 366 | Note that since the reconstructor adapts the octree to the sampling density, the specified reconstruction depth is only an upper bound. 367 | The default value for this parameter is 8. 368 | 369 | **full_depth**: 370 | This integer specifies the depth beyond depth the octree will be adapted. 371 | At coarser depths, the octree will be complete, containing all 2^d x 2^d x 2^d nodes. 372 | The default value for this parameter is 5. 373 | 374 | **scale**: 375 | This floating point value specifies the ratio between the diameter of the cube used for reconstruction and the diameter of the samples' bounding cube. 376 | The default value is 1.1. 377 | 378 | **samples per node**: 379 | This floating point value specifies the minimum number of sample points that should fall within an octree node as the octree construction is adapted to sampling density. 380 | For noise-free samples, small values in the range [1.0 - 5.0] can be used. 381 | For more noisy samples, larger values in the range [15.0 - 20.0] may be needed to provide a smoother, noise-reduced, reconstruction. 382 | The default value is 1.0. 383 | 384 | **cg depth**: 385 | This integer is the depth up to which a conjugate-gradients solver will be used to solve the linear system. 386 | Beyond this depth Gauss-Seidel relaxation will be used. 387 | The default value for this parameter is 0. 388 | 389 | **enable polygon mesh**: 390 | Enabling this flag tells the reconstructor to output a polygon mesh (rather than triangulating the results of Marching Cubes). 391 | The default value for this parameter is False. 392 | 393 | **enable density**: 394 | Enabling this flag tells the reconstructor to output the estimated depth values of the iso-surface vertices 395 | The default value for this parameter is False. 396 | 397 | 398 | 399 | 400 | -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/Toolbox-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/Toolbox-50.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/branch-closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/branch-closed.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/branch-end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/branch-end.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/branch-more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/branch-more.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/branch-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/branch-open.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/clean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/clean.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/clone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/clone.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pcloudpy-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pcloudpy-name.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pcloudpy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pcloudpy.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqDelete24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqDelete24.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqExtractSelection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqExtractSelection.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqEyeball16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqEyeball16.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqEyeballd16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqEyeballd16.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqFilter32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqFilter32.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqResetCamera24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqResetCamera24.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqResetCamera32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqResetCamera32.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqSelect32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqSelect32.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqSelectSurfPoints24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqSelectSurfPoints24.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqServer16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqServer16.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqXMinus24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqXMinus24.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqXPlus24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqXPlus24.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqYMinus24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqYMinus24.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqYPlus24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqYPlus24.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqZMinus24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqZMinus24.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/pqZPlus24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/pqZPlus24.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/toolbox-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/toolbox-icon.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/trash_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/trash_24.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/trash_red_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/trash_red_24.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/icons/vline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/pcloudpy/gui/resources/icons/vline.png -------------------------------------------------------------------------------- /pcloudpy/gui/resources/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/pcloudpy.png 4 | icons/pcloudpy-name.png 5 | icons/vline.png 6 | icons/branch-more.png 7 | icons/branch-open.png 8 | icons/branch-closed.png 9 | icons/branch-end.png 10 | icons/Toolbox-50.png 11 | icons/toolbox-icon.png 12 | icons/pqServer16.png 13 | icons/pqEyeball16.png 14 | icons/pqEyeballd16.png 15 | icons/pqXMinus24.png 16 | icons/pqYMinus24.png 17 | icons/pqZMinus24.png 18 | icons/pqXPlus24.png 19 | icons/pqYPlus24.png 20 | icons/pqZPlus24.png 21 | icons/pqDelete24.png 22 | icons/pqFilter32.png 23 | icons/pqResetCamera32.png 24 | icons/clean.png 25 | icons/pqSelectSurfPoints24.png 26 | icons/pqExtractSelection.png 27 | icons/trash_red_24.png 28 | icons/trash_24.png 29 | icons/clone.png 30 | conf/config_toolboxes.yaml 31 | about.md 32 | 33 | 34 | -------------------------------------------------------------------------------- /pcloudpy/gui/shell/CodeEdit.py: -------------------------------------------------------------------------------- 1 | """ 2 | A basic example that show you how to create a basic python code editor widget, 3 | from scratch. 4 | 5 | Editor features: 6 | - syntax highlighting 7 | - code completion (using jedi) 8 | - code folding 9 | - auto indentation 10 | - auto complete 11 | - comments mode (ctrl+/) 12 | - calltips mode 13 | - linters (pyflakes and pep8) modes + display panel 14 | - line number panel 15 | - builtin search and replace panel 16 | """ 17 | import logging 18 | logging.basicConfig() 19 | 20 | import sys 21 | from pyqode.qt import QtWidgets 22 | from pyqode.python.backend import server 23 | from pyqode.core import api, modes, panels 24 | from pyqode.python import modes as pymodes, panels as pypanels, widgets 25 | 26 | from PySide.QtGui import * 27 | from PySide.QtCore import * 28 | 29 | from .. import resources_rc 30 | from ..utils.qhelpers import * 31 | 32 | 33 | class CodeEdit(QWidget): 34 | def __init__(self, parent=None, dir_path=None): 35 | super(CodeEdit, self).__init__(parent) 36 | 37 | self.dir_path = dir_path 38 | 39 | layout = QVBoxLayout() 40 | 41 | self.toolbar = QToolBar() 42 | file_open_action = createAction(self, "&Open Script (.py)", self.file_open) 43 | file_open_action.setIcon(self.style().standardIcon(QStyle.SP_DialogOpenButton)) 44 | 45 | file_save_action = createAction(self, "&Save Script (.py)", self.file_save) 46 | file_save_action.setIcon(self.style().standardIcon(QStyle.SP_DialogSaveButton)) 47 | 48 | file_run_action = createAction(self, "&Run Script (.py)", self.file_run) 49 | file_run_action.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight)) 50 | 51 | self.toolbar.addAction(file_open_action) 52 | self.toolbar.addAction(file_save_action) 53 | self.toolbar.addAction(file_run_action) 54 | 55 | self.python_editor = MyPythonCodeEdit() 56 | 57 | layout.addWidget(self.toolbar) 58 | layout.addWidget(self.python_editor) 59 | 60 | self.setLayout(layout) 61 | 62 | codeRequested = Signal(str) 63 | 64 | def file_run(self): 65 | pass 66 | #self.codeRequested.emit(self.python_editor.document().toPlainText()) 67 | 68 | 69 | def file_open(self): 70 | filters = "*.py" 71 | filename, _ = QFileDialog.getOpenFileName(self, "Open Script (.py)", self.dir_path, filters) 72 | if filename: 73 | self.python_editor.file.open(filename) 74 | 75 | def file_save(self): 76 | filters = "*.py" 77 | filename, _ = QFileDialog.getSaveFileName(self, "Save Script (.py)", self.dir_path, filters) 78 | if filename: 79 | self.python_editor.file.save(filename) 80 | 81 | 82 | 83 | 84 | class MyPythonCodeEdit(widgets.PyCodeEditBase): 85 | def __init__(self): 86 | super(MyPythonCodeEdit, self).__init__() 87 | 88 | # starts the default pyqode.python server (which enable the jedi code 89 | # completion worker). 90 | #Now it does not work, why????? 91 | if hasattr(sys, 'frozen'): 92 | self.backend.start(server.__file__, interpreter="python") 93 | else: 94 | self.backend.start(server.__file__) 95 | 96 | 97 | # some other modes/panels require the analyser mode, the best is to 98 | # install it first 99 | #self.modes.append(pymodes.DocumentAnalyserMode()) 100 | 101 | #--- core panels 102 | self.panels.append(panels.FoldingPanel()) 103 | self.panels.append(panels.LineNumberPanel()) 104 | self.panels.append(panels.CheckerPanel()) 105 | self.panels.append(panels.SearchAndReplacePanel(), 106 | panels.SearchAndReplacePanel.Position.BOTTOM) 107 | self.panels.append(panels.EncodingPanel(), api.Panel.Position.TOP) 108 | # add a context menu separator between editor's 109 | # builtin action and the python specific actions 110 | self.add_separator() 111 | 112 | #--- python specific panels 113 | self.panels.append(pypanels.QuickDocPanel(), api.Panel.Position.BOTTOM) 114 | 115 | #--- core modes 116 | self.modes.append(modes.CaretLineHighlighterMode()) 117 | self.modes.append(modes.CodeCompletionMode()) 118 | self.modes.append(modes.ExtendedSelectionMode()) 119 | self.modes.append(modes.FileWatcherMode()) 120 | self.modes.append(modes.OccurrencesHighlighterMode()) 121 | self.modes.append(modes.RightMarginMode()) 122 | self.modes.append(modes.SmartBackSpaceMode()) 123 | self.modes.append(modes.SymbolMatcherMode()) 124 | self.modes.append(modes.ZoomMode()) 125 | 126 | #--- python specific modes 127 | self.modes.append(pymodes.CommentsMode()) 128 | self.modes.append(pymodes.CalltipsMode()) 129 | self.modes.append(pymodes.FrostedCheckerMode()) 130 | self.modes.append(pymodes.PEP8CheckerMode()) 131 | self.modes.append(pymodes.PyAutoCompleteMode()) 132 | self.modes.append(pymodes.PyAutoIndentMode()) 133 | self.modes.append(pymodes.PyIndenterMode()) 134 | 135 | 136 | """ 137 | app = QtWidgets.QApplication(sys.argv) 138 | window = QtWidgets.QMainWindow() 139 | editor = MyPythonCodeEdit() 140 | editor.file.open(__file__) 141 | window.setCentralWidget(editor) 142 | window.show() 143 | app.exec_() 144 | """ -------------------------------------------------------------------------------- /pcloudpy/gui/shell/IPythonConsole.py: -------------------------------------------------------------------------------- 1 | import os 2 | os.environ["QT_API"]= "pyside" 3 | from PySide import QtGui, QtCore 4 | 5 | 6 | def new_load_qt(api_options): 7 | from PySide import QtCore, QtGui, QtSvg 8 | return QtCore, QtGui, QtSvg, 'pyside' 9 | 10 | from qtconsole import qt_loaders 11 | qt_loaders.load_qt = new_load_qt 12 | 13 | 14 | #from IPython.qt.console.rich_ipython_widget import RichIPythonWidget 15 | #from IPython.qt.inprocess import QtInProcessKernelManager 16 | from IPython.lib import guisupport 17 | from qtconsole.rich_ipython_widget import RichIPythonWidget 18 | from qtconsole.inprocess import QtInProcessKernelManager 19 | 20 | 21 | class EmbedIPython(RichIPythonWidget): 22 | """ 23 | Based on: 24 | http://stackoverflow.com/questions/11513132/embedding-ipython-qt-console-in-a-pyqt-application 25 | """ 26 | 27 | def __init__(self, **kwarg): 28 | super(RichIPythonWidget, self).__init__() 29 | self.app = app = guisupport.get_app_qt4() 30 | self.kernel_manager = QtInProcessKernelManager() 31 | self.kernel_manager.start_kernel() 32 | self.kernel = self.kernel_manager.kernel 33 | self.kernel.gui = 'qt4' 34 | self.kernel.shell.push(kwarg) 35 | self.kernel_client = self.kernel_manager.client() 36 | self.kernel_client.start_channels() 37 | 38 | def get_kernel_shell(self): 39 | return self.kernel_manager.kernel.shell 40 | 41 | def get_kernel_shell_user(self): 42 | return self.kernel_manager.kernel.shell.user_ns 43 | 44 | 45 | class IPythonConsole(QtGui.QMainWindow): 46 | 47 | def __init__(self, parent=None, App=None): 48 | super(IPythonConsole, self).__init__(parent) 49 | self.App = App 50 | self.console = EmbedIPython(App=self.App) 51 | self.console.kernel.shell.run_cell('%pylab qt') 52 | self.console.kernel.shell.run_cell("import numpy as np") 53 | self.console.kernel.shell.run_cell("from matplotlib import rcParams") 54 | self.console.kernel.shell.run_cell("rcParams['backend.qt4']='PySide'") 55 | self.console.kernel.shell.run_cell("import matplotlib.pyplot as plt") 56 | 57 | vbox = QtGui.QVBoxLayout() 58 | vbox.addWidget(self.console) 59 | 60 | b = QtGui.QWidget() 61 | b.setLayout(vbox) 62 | self.setCentralWidget(b) 63 | 64 | def execute_code(self, text): 65 | self.console.execute(text) 66 | #self.console.kernel.shell.run_cell(text) 67 | 68 | -------------------------------------------------------------------------------- /pcloudpy/gui/shell/PythonConsole.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import code 5 | 6 | from rlcompleter import Completer 7 | 8 | from PySide.QtGui import * 9 | from PySide.QtCore import * 10 | 11 | """ 12 | Notes: 13 | http://www.pythoncentral.io/embed-interactive-python-interpreter-console/ 14 | http://stackoverflow.com/questions/2758159/how-to-embed-a-python-interpreter-in-a-pyqt-widget/8219536#8219536 15 | http://stackoverflow.com/questions/12431555/enabling-code-completion-in-an-embedded-python-interpreter 16 | """ 17 | 18 | 19 | class PythonConsole(QWidget): 20 | 21 | def __init__(self, parent, app=None): 22 | 23 | super(PythonConsole, self).__init__(parent) 24 | hBox = QHBoxLayout() 25 | self.textEdit = PyInterp(self) 26 | self.app = app 27 | # this is how you pass in locals to the interpreter 28 | self.textEdit.initInterpreter(locals()) 29 | self.resize(650, 3009) 30 | hBox.addWidget(self.textEdit) 31 | 32 | self.setLayout(hBox) 33 | 34 | """ 35 | def centerOnScreen(self): 36 | # center the widget on the screen 37 | resolution = QDesktopWidget().screenGeometry() 38 | self.move((resolution.width() / 2) - (self.frameSize().width() / 2), 39 | (resolution.height() / 2) - (self.frameSize().height() / 2)) 40 | """ 41 | 42 | class PyInterp(QTextEdit): 43 | 44 | class InteractiveInterpreter(code.InteractiveInterpreter): 45 | 46 | def __init__(self, locals): 47 | code.InteractiveInterpreter.__init__(self, locals) 48 | 49 | def runIt(self, command): 50 | code.InteractiveInterpreter.runsource(self, command) 51 | 52 | def __init__(self, parent): 53 | super(PyInterp, self).__init__(parent) 54 | 55 | sys.stdout = self 56 | sys.stderr = self 57 | self.refreshMarker = False # to change back to >>> from ... 58 | self.multiLine = False # code spans more than one line 59 | self.command = '' # command to be ran 60 | self.printBanner() # print sys info 61 | self.marker() # make the >>> or ... marker 62 | self.history = [] # list of commands entered 63 | self.historyIndex = -1 64 | self.interpreterLocals = {} 65 | 66 | # setting the color for bg and text 67 | palette = QPalette() 68 | palette.setColor(QPalette.Base, QColor(255, 255, 255)) 69 | palette.setColor(QPalette.Text, QColor(37/255.0, 85/255.0,152/255.0)) 70 | self.setPalette(palette) 71 | self.setFont(QFont('Courier', 12)) 72 | 73 | # initilize interpreter with self locals 74 | self.initInterpreter(locals()) 75 | 76 | def printBanner(self): 77 | self.write(sys.version) 78 | self.write(' on ' + sys.platform + '\n') 79 | #self.write('PyQt4 ' + PYQT_VERSION_STR + '\n') 80 | msg = 'Type !hist for a history view and !hist(n) history index recall' 81 | self.write(msg + '\n') 82 | 83 | def marker(self): 84 | if self.multiLine: 85 | self.insertPlainText('... ') 86 | else: 87 | self.insertPlainText('>>> ') 88 | 89 | def initInterpreter(self, interpreterLocals=None): 90 | if interpreterLocals: 91 | # when we pass in locals, we don't want it to be named "self" 92 | # so we rename it with the name of the class that did the passing 93 | # and reinsert the locals back into the interpreter dictionary 94 | selfName = interpreterLocals['self'].__class__.__name__ 95 | interpreterLocalVars = interpreterLocals.pop('self') 96 | self.interpreterLocals[selfName] = interpreterLocalVars 97 | else: 98 | self.interpreterLocals = interpreterLocals 99 | self.interpreter = self.InteractiveInterpreter(self.interpreterLocals) 100 | 101 | def updateInterpreterLocals(self, newLocals): 102 | className = newLocals.__class__.__name__ 103 | self.interpreterLocals[className] = newLocals 104 | 105 | def write(self, line): 106 | 107 | self.insertPlainText(line) 108 | self.ensureCursorVisible() 109 | 110 | def clearCurrentBlock(self): 111 | # block being current row 112 | length = len(self.document().lastBlock().text()[4:]) 113 | if length == 0: 114 | return None 115 | else: 116 | # should have a better way of doing this but I can't find it 117 | [self.textCursor().deletePreviousChar() for x in xrange(length)] 118 | return True 119 | 120 | def recallHistory(self): 121 | # used when using the arrow keys to scroll through history 122 | self.clearCurrentBlock() 123 | if self.historyIndex <> -1: 124 | self.insertPlainText(self.history[self.historyIndex]) 125 | return True 126 | 127 | def customCommands(self, command): 128 | 129 | if command == '!hist': # display history 130 | self.append('') # move down one line 131 | # vars that are in the command are prefixed with ____CC and deleted 132 | # once the command is done so they don't show up in dir() 133 | backup = self.interpreterLocals.copy() 134 | history = self.history[:] 135 | history.reverse() 136 | for i, x in enumerate(history): 137 | iSize = len(str(i)) 138 | delta = len(str(len(history))) - iSize 139 | line = line = ' ' * delta + '%i: %s' % (i, x) + '\n' 140 | self.write(line) 141 | self.updateInterpreterLocals(backup) 142 | self.marker() 143 | return True 144 | 145 | if re.match('!hist\(\d+\)', command): # recall command from history 146 | backup = self.interpreterLocals.copy() 147 | history = self.history[:] 148 | history.reverse() 149 | index = int(command[6:-1]) 150 | self.clearCurrentBlock() 151 | command = history[index] 152 | if command[-1] == ':': 153 | self.multiLine = True 154 | self.write(command) 155 | self.updateInterpreterLocals(backup) 156 | return True 157 | 158 | return False 159 | 160 | def keyPressEvent(self, event): 161 | 162 | """ 163 | if event.key() == Qt.Key_Escape: 164 | # proper exit 165 | self.interpreter.runIt('exit()') 166 | """ 167 | 168 | if event.key() == Qt.Key_Down: 169 | if self.historyIndex == len(self.history): 170 | self.historyIndex -= 1 171 | try: 172 | if self.historyIndex > -1: 173 | self.historyIndex -= 1 174 | self.recallHistory() 175 | else: 176 | self.clearCurrentBlock() 177 | except: 178 | pass 179 | return None 180 | 181 | if event.key() == Qt.Key_Up: 182 | try: 183 | if len(self.history) - 1 > self.historyIndex: 184 | self.historyIndex += 1 185 | self.recallHistory() 186 | else: 187 | self.historyIndex = len(self.history) 188 | except: 189 | pass 190 | return None 191 | 192 | if event.key() == Qt.Key_Home: 193 | # set cursor to position 4 in current block. 4 because that's where 194 | # the marker stops 195 | blockLength = len(self.document().lastBlock().text()[4:]) 196 | lineLength = len(self.document().toPlainText()) 197 | position = lineLength - blockLength 198 | textCursor = self.textCursor() 199 | textCursor.setPosition(position) 200 | self.setTextCursor(textCursor) 201 | return None 202 | 203 | if event.key() in [Qt.Key_Left, Qt.Key_Backspace]: 204 | # don't allow deletion of marker 205 | if self.textCursor().positionInBlock() == 4: 206 | return None 207 | 208 | if event.key() in [Qt.Key_Return, Qt.Key_Enter]: 209 | # set cursor to end of line to avoid line splitting 210 | textCursor = self.textCursor() 211 | position = len(self.document().toPlainText()) 212 | textCursor.setPosition(position) 213 | self.setTextCursor(textCursor) 214 | 215 | line = str(self.document().lastBlock().text())[4:] # remove marker 216 | line.rstrip() 217 | self.historyIndex = -1 218 | 219 | if self.customCommands(line): 220 | return None 221 | else: 222 | try: 223 | line[-1] 224 | self.haveLine = True 225 | if line[-1] == ':': 226 | self.multiLine = True 227 | self.history.insert(0, line) 228 | except: 229 | self.haveLine = False 230 | 231 | if self.haveLine and self.multiLine: # multi line command 232 | self.command += line + '\n' # + command and line 233 | self.append('') # move down one line 234 | self.marker() # handle marker style 235 | return None 236 | 237 | if self.haveLine and not self.multiLine: # one line command 238 | self.command = line # line is the command 239 | self.append('') # move down one line 240 | self.interpreter.runIt(self.command) 241 | self.command = '' # clear command 242 | self.marker() # handle marker style 243 | return None 244 | 245 | if self.multiLine and not self.haveLine: # multi line done 246 | self.append('') # move down one line 247 | self.interpreter.runIt(self.command) 248 | self.command = '' # clear command 249 | self.multiLine = False # back to single line 250 | self.marker() # handle marker style 251 | return None 252 | 253 | if not self.haveLine and not self.multiLine: # just enter 254 | self.append('') 255 | self.marker() 256 | return None 257 | return None 258 | 259 | # allow all other key events 260 | super(PyInterp, self).keyPressEvent(event) 261 | 262 | 263 | if __name__ == '__main__': 264 | 265 | app = QApplication(sys.argv) 266 | win = PythonConsole(None) 267 | win.show() 268 | 269 | app.exec_() 270 | -------------------------------------------------------------------------------- /pcloudpy/gui/shell/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pcloudpy/gui/utils/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Miguel' 2 | -------------------------------------------------------------------------------- /pcloudpy/gui/utils/qhelpers.py: -------------------------------------------------------------------------------- 1 | """Helper Methods 2 | """ 3 | 4 | import collections 5 | 6 | from PyQt5.QtCore import * 7 | from PyQt5.QtGui import * 8 | from PyQt5.QtWidgets import * 9 | from PyQt5.QtCore import pyqtSignal as Signal 10 | 11 | 12 | 13 | def read_file(filename): 14 | 15 | fd = QFile(filename) 16 | if fd.open(QIODevice.ReadOnly | QFile.Text): 17 | text = QTextStream(fd).readAll() 18 | fd.close() 19 | return text 20 | else: 21 | return None 22 | 23 | 24 | def createAction(parent, text, slot=None, shortcut=None, icon=None, tip=None, checkable=False, signal="triggered()"): 25 | 26 | """ 27 | Helper Method for the creation of Action Objects 28 | 29 | :param parent 30 | :param text 31 | :param slot 32 | :param shortcut 33 | :param icon 34 | :param tip 35 | :param checkable 36 | :param signal 37 | """ 38 | 39 | action = QAction(text, parent) 40 | if icon is not None: 41 | action.setIcon(QIcon(":/%s" % icon)) 42 | if shortcut is not None: 43 | action.setShortcut(shortcut) 44 | if tip is not None: 45 | action.setToolTip(tip) 46 | action.setStatusTip(tip) 47 | if slot is not None: 48 | print("REVIEW****", parent) 49 | #parent.connect(action, SIGNAL(signal), slot) 50 | if checkable: 51 | action.setCheckable(True) 52 | return action 53 | 54 | 55 | def addActions(target, actions): 56 | 57 | if not isinstance(actions, collections.Iterable): 58 | target.addAction(actions) 59 | else: 60 | for action in actions: 61 | if action is None: 62 | target.addSeparator() 63 | else: 64 | target.addAction(action) 65 | 66 | 67 | def addWidgets(target, widgets): 68 | if not isinstance(widgets, collections.Iterable): 69 | target.addWidget(widgets) 70 | else: 71 | for widget in widgets: 72 | target.addWidget(widget) 73 | 74 | def setEnabled(actions, state): 75 | 76 | if not isinstance(actions, collections.Iterable): 77 | actions.setEnabled(state) 78 | else: 79 | for action in actions: 80 | if action is not None: 81 | action.setEnabled(state) 82 | 83 | 84 | def setVisible(target, state): 85 | if not isinstance(target, collections.Iterable): 86 | target.setVisible(state) 87 | else: 88 | for item in target: 89 | if item is not None: 90 | item.setVisible(state) 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | markdown2>=2.3.0 2 | laspy>=1.2.5 3 | PyYAML>=3.11 4 | pyqode.core>=2.6.6 5 | pyqode.qt>=2.6.0 6 | pyqode.python>=2.6.3 7 | -------------------------------------------------------------------------------- /resources/pcloudpy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/resources/pcloudpy.png -------------------------------------------------------------------------------- /resources/pcloudpy_tunnel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/resources/pcloudpy_tunnel.png -------------------------------------------------------------------------------- /resources/pcloudpy_v0.10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmolero/pcloudpy/c8e4b342f9180374db97af3d87d60ece683b7bc0/resources/pcloudpy_v0.10.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name='pcloudpy', 4 | version='0.10', 5 | description='Point Cloud Viewer and Processing Toolkit in Python', 6 | author='Miguel Molero-Armenta', 7 | author_email='miguel.molero@gmail.com', 8 | url='https://github.com/mmolero/pcloudpy', 9 | packages=find_packages(exclude=["tests"]), 10 | include_package_data = True, 11 | ) 12 | --------------------------------------------------------------------------------