• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

KU-Leuven-Geomatics / geomapi / 15188762246

22 May 2025 02:06PM UTC coverage: 46.454% (-0.6%) from 47.067%
15188762246

push

github

Jellevermandere
Documentaion and imageNode focallenght

3144 of 6768 relevant lines covered (46.45%)

0.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.91
geomapi/nodes/setnode.py
1
"""
2
**SetNode** - a Python Class to govern the data and metadata of remote sensing data captured in the same epoch, or that all code from the same file.
3

4
This node builds upon the [OpenCV](https://opencv.org/), [Open3D](https://www.open3d.org/) and [PIL](https://pillow.readthedocs.io/en/stable/) API for the image definitions.
5
Be sure to check the properties defined in those abstract classes to initialise the Node.
6

7
.. image:: ../../../docs/pics/graph_set_1.png
8

9
**NOTE**: This node does not represent sensory data, but rather a group of data. The resource is a convex hull of all the linkedNodes.
10

11
Goals:
12
- Given a path, import all the linked images, meshes, ect... into a set
13
- Convert non-RDF metadata files (json, xml, ect..) to setsNodes and export them to RDF
14
- get the boundingbox of the whole set
15
- use URIRef() to reference the images, ect...
16

17
"""  
18
#IMPORT PACKAGES
19
import concurrent
1✔
20
import ifcopenshell
1✔
21
import numpy as np
1✔
22
import os
1✔
23
import open3d as o3d
1✔
24

25
from rdflib import RDF, XSD, Graph, Namespace, URIRef
1✔
26
import numpy as np
1✔
27
from pathlib import Path
1✔
28
from scipy.spatial.transform import Rotation as R
1✔
29
from typing import List, Optional,Tuple,Union
1✔
30

31
#IMPORT MODULES
32
from geomapi.nodes import Node
1✔
33
from geomapi.nodes.imagenode import ImageNode
1✔
34
import geomapi.utils as ut
1✔
35
from geomapi.utils import rdf_property, GEOMAPI_PREFIXES
1✔
36
import geomapi.utils.geometryutils as gmu
1✔
37

38
class SetNode(Node):
1✔
39

40
    def __init__(self, 
1✔
41
                subject: Optional[URIRef] = None,
42
                graph: Optional[Graph] = None,
43
                graphPath: Optional[Path] = None,
44
                name: Optional[str] = None,
45
                path: Optional[Path] = None,
46
                timestamp: Optional[str] = None,
47
                resource = None,
48
                cartesianTransform: Optional[np.ndarray] = None,
49
                orientedBoundingBox: Optional[o3d.geometry.OrientedBoundingBox] = None,
50
                convexHull: Optional[o3d.geometry.TriangleMesh] =None,
51
                loadResource: bool = False,
52
                linkedNodes: List = None,
53
                linkedSubjects: List = None,
54
                **kwargs):
55
        """
56
        Creates a SetNode & all the child nodes. Overloaded function.
57
        This Node can be initialised from one or more of the inputs below.
58
        By default, no data is imported in the Node to speed up processing.
59
        If you also want the data, call node.get_resource() or set getResource() to True.
60

61
        Args:
62
            - subject (RDFlib URIRef) : subject to be used as the main identifier in the RDF Graph
63
            
64
            - graph (RDFlib Graph) : Two possible graphs are parsed:
65
                - The RDF Graph of only the setNode (1 subject)
66
                - AN RDF Graph with only resourceNodes
67
                
68
            - graphPath (Path) :  The path of the Graph of only the set.
69
            
70
            - path (Path) : Path to the convexhull resource of the setNode
71

72
            - resource (o3d.geometry) : o3d.geometry.TriangleMesh of the convexhull of the setNode or a list of resources to be converted to LinkedNodes (PointCloudNode, MeshNode, ImageNode, etc.)
73
                        
74
            - LinkedNodes (Nodes, optional) : A set of Nodes (ImageNodes, MeshNodes) to contain within the set 
75
            
76
            - getResource (bool, optional= False) : If True, the node will search for its physical resource on drive 
77
            
78
            - getMetaData (bool, optional= True) : If True, the node will attempt to extract geometric metadata from the resource if present (cartesianBounds, etc.) 
79
        
80
        Returns:
81
           
82
            - Args(subject) : create a new Graph and Node with the given subject
83
            
84
            - Args(Graph) : parse the graph with the given subject
85
                - 1 or more are matched: Use that node as the SetNode
86
                - 1 or more are found but not matched: Raise an error
87
                - None are found: Create a new node with the given subject
88
            
89
            - Args(resources) : create a setNode and linkedNodes from the resources
90
            
91
            - Args(linkedNodes) : create a new Graph from the joint metadata
92
        """
93

94
        #set properties (protected inputs)   
95
        self.linkedNodes=linkedNodes
1✔
96
        self.linkedSubjects= linkedSubjects
1✔
97

98

99
        super().__init__(   subject = subject,
1✔
100
                            graph = graph,
101
                            graphPath = graphPath,
102
                            name = name,
103
                            path = path,
104
                            timestamp = timestamp,
105
                            resource = resource,
106
                            cartesianTransform = cartesianTransform,
107
                            orientedBoundingBox = orientedBoundingBox,
108
                            convexHull = convexHull,
109
                            loadResource = loadResource,
110
                            **kwargs) 
111
                
112
#---------------------PROPERTIES----------------------------
113

114
    #---------------------linkedNodes----------------------------
115
    @property
1✔
116
    def linkedNodes(self): 
1✔
117
        """Get the linkedNodes (Node) of the node."""
118
        return self._linkedNodes
1✔
119

120
    @linkedNodes.setter
1✔
121
    def linkedNodes(self,list:List[Node]):
1✔
122
        if list is None:
1✔
123
            self._linkedNodes = None
1✔
124
            return
1✔
125
        list=ut.item_to_list(list)
1✔
126
        if all('Node' in str(type(value)) for value in list):
1✔
127
            self._linkedNodes = list
1✔
128
        else:
129
            raise ValueError('Some elements in self.linkedNodes are not Nodes')    
×
130
    
131
    #---------------------linkedSubjects----------------------------
132
    @property
1✔
133
    @rdf_property(predicate=GEOMAPI_PREFIXES['geomapi'].hasPart, serializer = lambda uris: list(map(str, uris)))
1✔
134
    def linkedSubjects(self): 
1✔
135
        """Get the linkedSubjects (URIRef) of the node."""
136
        if not self._linkedSubjects and self.linkedNodes:
1✔
137
            self._linkedSubjects= [node.subject for node in self.linkedNodes]
1✔
138
        return self._linkedSubjects
1✔
139

140
    @linkedSubjects.setter
1✔
141
    def linkedSubjects(self,list:List[URIRef]):
1✔
142
        if list is None:
1✔
143
            self._linkedSubjects = None
1✔
144
            return
1✔
145
        list=ut.item_to_list(list)
×
146
        if all('URIRef' in str(type(value)) for value in list):
×
147
            self._linkedSubjects = list
×
148
        else:
149
            raise ValueError('Some elements are not URIRefs')
×
150
                    
151
                    
152
                    
153
                    
154
                    
155
#---------------------METHODS----------------------------
156

157
    def _set_geometric_properties(self, _cartesianTransform = None, _convexHull = None, _orientedBoundingBox = None):
1✔
158
        
159
        # first try transform
160
        self.cartesianTransform = _cartesianTransform
1✔
161
        self.convexHull = _convexHull
1✔
162
        self.orientedBoundingBox = _orientedBoundingBox
1✔
163

164
        hasResource = self.resource is not None
1✔
165

166
        if self.cartesianTransform is None:
1✔
167
            if hasResource:
1✔
168
                self.cartesianTransform = gmu.get_cartesian_transform(translation=self.resource.get_center())
×
169
            elif self.convexHull is not None:
1✔
170
                self.cartesianTransform = gmu.get_cartesian_transform(translation=self.convexHull.get_center())
1✔
171
            elif self.orientedBoundingBox is not None:
1✔
172
                self.cartesianTransform = gmu.get_cartesian_transform(translation=self.orientedBoundingBox.get_center(), rotation=self.orientedBoundingBox.R) # the carthesian transform matches the rotation of the bounding box
1✔
173
            else:
174
                self.cartesianTransform = np.eye(4)
1✔
175

176
        if self.convexHull is None:
1✔
177
            if hasResource:
1✔
178
                self.convexHull = gmu.get_convex_hull(self.resource)
×
179
            elif self.orientedBoundingBox is not None:
1✔
180
                self.convexHull = gmu.get_convex_hull(self.orientedBoundingBox)
1✔
181
            elif self.linkedNodes is not None:
1✔
182
                all_geometries = o3d.geometry.TriangleMesh()
1✔
183
                for node in self.linkedNodes:
1✔
184
                    all_geometries += node.convexHull
1✔
185
                self.convexHull =  gmu.get_convex_hull(all_geometries)
1✔
186
            else:
187
                box = o3d.geometry.TriangleMesh.create_box(width=1.0, height=1.0, depth=1.0)
1✔
188
                box.translate([-0.5, -0.5, -0.5])
1✔
189
                box.transform(self.cartesianTransform)
1✔
190
                self.convexHull = box
1✔
191

192
        if self.orientedBoundingBox is None:
1✔
193
            if hasResource:
1✔
194
                self.orientedBoundingBox = gmu.get_oriented_bounding_box(self.resource)
×
195
            else:
196
                self.orientedBoundingBox = gmu.get_oriented_bounding_box(self.convexHull)
1✔
197

198
    def get_graph(self, graphPath: Path = None, overwrite: bool = True, save: bool = False, base: URIRef = None, serializeAttributes: List = None, addLinkedNodes: bool = True) -> Graph:
1✔
199
        """Serialize the set's linkedNodes
200

201
        Args:
202
            - graphPath (Path) : The path of the graph to parse.
203
            - overwrite (bool) : Overwrite the existing graph or not.
204
            - base (str | URIRef) : BaseURI to match subjects to in the graph (improves readability) e.g. http://node#. Also, the base URI is used to set the subject of the graph. RDF rules and customs apply so the string must be a valid URI (http:// in front, and # at the end).
205
            - save (bool) : Save the graph to the self.graphPath or graphPath.
206
            - serializeAttributes (List(str)) : a list of attributes defined in the node that also need to be serialized
207
        
208
        Returns:
209
            Graph with linkedNodes
210
        """ 
211
        # Create the base graph of this Node, don't save yet
212
        self._graph = super().get_graph(graphPath=graphPath, 
1✔
213
                                      overwrite=overwrite, 
214
                                      save=False, base=base, 
215
                                      serializeAttributes=serializeAttributes)
216

217
        print(self._graph.serialize())
1✔
218
        if(addLinkedNodes and overwrite and self.linkedNodes):
1✔
219
            #Add the linked nodes to the graph
220
            for node in self.linkedNodes:
×
221
                node.get_graph(graphPath=graphPath, 
×
222
                               overwrite=overwrite, 
223
                               save=False, 
224
                               base=base, 
225
                               serializeAttributes=serializeAttributes)
226
                self._graph += node.graph
×
227

228
        # Save graph if requested
229
            if save:
×
230
                self.save_graph(graphPath)
×
231

232
        return self._graph
1✔
233

234
    
235

236
    def save_linked_resources(self,directory:str=None):
1✔
237
        """Export the resources of the linkedNodes.
238

239
        Args:
240
            directory (str, optional) : directory folder to store the data.
241

242
        Returns:
243
            bool: return True if export was succesful
244
        """ 
245
        if not self.linkedNodes:
1✔
246
            print("No linked Nodes defined")
×
247
            return
×
248
        for node in self.linkedNodes:
1✔
249
            node.save_resource(directory)  
1✔
250

251

252
    def transform(self, 
1✔
253
                  transformation: Optional[np.ndarray] = None, 
254
                  rotation: Optional[Union[np.ndarray, Tuple[float, float, float]]] = None, 
255
                  translation: Optional[np.ndarray] = None, 
256
                  rotate_around_center: bool = True):
257
        """
258
        Apply a transformation to the Node's cartesianTransform, orientedBoundingBox, and convexHull.
259
        Subclasses should override this method to transform their specific resource types.
260
        
261
        Args:
262
            - transformation (Optional[np.ndarray]): A 4x4 transformation matrix.
263
            - rotation (Optional[Union[np.ndarray, np.array[float, float, float]]]): A 3x3 rotation matrix or Euler angles (Rz, Ry, Rx).
264
            - translation (Optional[np.ndarray]): A 3-element translation vector.
265
            - rotate_around_center (bool): If True, rotate around the object's center (handled by subclass if needed).
266
        """
267
        
268
        super().transform(transformation = transformation, 
1✔
269
                          rotation = rotation, 
270
                          translation = translation,
271
                          rotate_around_center=rotate_around_center)
272
        
273
        if(self.linkedNodes):
1✔
274
            for node in self.linkedNodes:
1✔
275
                # First set the node to the center in relation to the setNode
276
                #node.transform(np.linalg.inv(self.cartesianTransform))
277
                # Perform the transformation
278
                node.transform(transformation = transformation, 
1✔
279
                               rotation = rotation, 
280
                               translation = translation,
281
                               rotate_around_center=rotate_around_center)
282
                # Set the node back
283
                #node.transform(self.cartesianTransform)
284

285
    def show(self):
1✔
286
        if(self.linkedNodes is None or len(self.linkedNodes) == 0):
×
287
            print("No linkedNodes present")
×
288
            return
×
289
        geometries = []
×
290
        for node in self.linkedNodes:
×
291
            if(node.resource is None):
×
292
                geometries.append(gmu.mesh_get_lineset(node.convexHull))
×
293
            elif(isinstance(node.resource, (o3d.geometry.TriangleMesh, o3d.geometry.PointCloud, o3d.geometry.LineSet))):
×
294
                geometries.append(node.resource)
×
295
            elif(isinstance(node, ImageNode)):
×
296
                frustum_lines, image_plane = gmu.create_camera_frustum_mesh_with_image(
×
297
                node.cartesianTransform,
298
                node.imageWidth, 
299
                node.imageHeight, 
300
                node.focalLength35mm, 
301
                node.depth,
302
                image_cv2=self.resource)
303
                geometries.append(frustum_lines)
×
304
                geometries.append(image_plane)
×
305
            else:
306
                geometries.append(gmu.mesh_get_lineset(node.convexHull))
×
307
        gmu.show_geometries(geometries)
×
308

309

310

311

312
    #def save_resource(self,directory:str=None,extension :str = '.ply') -> bool:
313
    #    """Export the resource (Convex hull) of the Node.
314
#
315
    #    Args:
316
    #        directory (str, optional) : directory folder to store the data.
317
    #        extension (str, optional) : file extension. Defaults to '.ply'.
318
#
319
    #    Raises:
320
    #        ValueError: Unsuitable extension. Please check permitted extension types in utils._init_.
321
#
322
    #    Returns:
323
    #        bool: return True if export was succesful
324
    #    """ 
325
    #    # perform the path check and create the directory
326
    #    if not super().save_resource(directory, extension):
327
    #        return False
328
#
329
    #    #write files
330
    #    if o3d.io.write_triangle_mesh(str(self.path), self.resource):
331
    #        return True
332
    #    return False
333

334

335

336
    # def get_metadata_from_linked_nodes(self):
337
    #     """Returns the setNode metadata from the linkedNodes. 
338

339
    #     Returns:
340
    #         - cartesianTransform
341
    #         - orientedBoundingBox 
342
    #         - convexHull
343
    #     """
344
    #     if self._graph:
345
    #         return True
346

347
    #     if getattr(self,'timestamp',None) is None and self.linkedNodes:
348
    #         self.timestamp=self.linkedNodes[0].get_timestamp()
349

350
    #     points=o3d.utility.Vector3dVector()        
351
    #     for node in self.linkedNodes: 
352
    #         if getattr(node,'get_oriented_bounding_box',None) is not None:           
353
    #             box=node.get_oriented_bounding_box()
354
    #             if (box):
355
    #                 points.extend(box.get_box_points())    
356
                
357
    #         elif getattr(node,'cartesianTransform',None) is not None:
358
    #             t=gmu.get_translation(node.get_cartesian_transform())
359
    #             t=np.reshape(t,(1,3))
360
    #             p=o3d.utility.Vector3dVector(t)
361
    #             points.extend(p)
362

363
    #     if np.asarray(points).shape[0] >=5:    
364
    #         self.cartesianTransform=gmu.get_cartesian_transform(translation=np.mean( np.asarray(points),0)) 
365
    #         self.orientedBoundingBox=o3d.geometry.OrientedBoundingBox.create_from_points(points)
366
    #         self.orientedBounds=np.asarray(self.orientedBoundingBox.get_box_points())
367
    #         self.cartesianBounds=gmu.get_cartesian_bounds(o3d.geometry.AxisAlignedBoundingBox.create_from_points(points))
368
    #         pcd= o3d.geometry.PointCloud()
369
    #         pcd.points=points
370
    #         hull, _ =pcd.compute_convex_hull()
371
    #         self.resource=hull
372

373
    #def get_linked_resources(self,percentage:float=1.0):
374
    #    """Returns the resources of the linkedNodes. 
375
    #    If none is present, it will search for the data on drive from path, graphPath, name or subject. 
376
    #    Otherwise, it will be reconstructed from the metadata present
377
#
378
    #    Args:
379
    #        - self (setNode)
380
    #        - percentage(float,optional) : load percentage of point cloud resources in present PointCloudNodes.
381
#
382
    #    Returns:
383
    #        list[resource] or None
384
    #    """
385
    #    for node in self.linkedNodes:
386
    #        if 'PointCloud' in str(type(node)):
387
    #            node.get_resource(percentage)
388
    #        else:
389
    #            node.get_resource()
390
    #    return [n.resource for n in self.linkedNodes]
391
    #  
392
    #def get_linked_resources_multiprocessing(self,percentage:float=1.0):
393
    #    """Returns the resources of the linkedNodes by multi-processing the imports. 
394
    #    If none is present, it will search for the data on drive from path, graphPath, name or subject. 
395
#
396
    #    **NOTE**: Starting parallel processing takes a bit of time. As such, this method will only outperform get_linked_resources with 10+ linkedNodes
397
#
398
    #    Args:
399
    #        - self (setNode)
400
    #        - percentage(float,optional) : load percentage of point cloud resources in present PointCloudNodes.
401
#
402
    #    Returns:
403
    #        list[resource] or None
404
    #    """
405
    #    [n.get_path() for n in self.linkedNodes]
406
#
407
    #    with concurrent.futures.ProcessPoolExecutor() as executor:
408
    #        # first load all data and output it as np.arrays      
409
    #        results1=[executor.submit(gmu.e57_to_arrays,e57Path=n.path,e57Index=n.e57Index,percentage=percentage,tasknr=i) for i,n in enumerate(self.linkedNodes) 
410
    #                    if 'PointCloud' in str(type(n)) and 
411
    #                        n.resource is None and
412
    #                        n.path.endswith('.e57') and 
413
    #                        os.path.exists(n.path)]
414
#
415
    #        results2=[executor.submit(gmu.pcd_to_arrays,path=n.path,percentage=percentage,tasknr=i) for i,n in enumerate(self.linkedNodes) 
416
    #                    if 'PointCloud' in str(type(n)) and 
417
    #                        n.resource is None and
418
    #                        n.path.endswith('.pcd') and 
419
    #                        os.path.exists(n.path)]
420
    #        results3=[executor.submit(gmu.mesh_to_arrays,path=n.path,tasknr=i) for i,n in enumerate(self.linkedNodes) 
421
    #                    if ('MeshNode' in str(type(n)) or 'BIMNode' in str(type(n))) and 
422
    #                        n.resource is None and
423
    #                        os.path.exists(n.path)]
424
    #        results4=[executor.submit(gmu.img_to_arrays,path=n.path,tasknr=i) for i,n in enumerate(self.linkedNodes) 
425
    #                    if 'ImageNode' in str(type(n)) and 
426
    #                    n.resource is None and
427
    #                    os.path.exists(n.path)]
428
#
429
    #    # next, the arrays are assigned to point clouds outside the loop.
430
    #    # Note that these loops should be in parallel as they would otherwise obstruct one another.
431
    #    for r1 in concurrent.futures.as_completed(results1): 
432
    #        resource=gmu.arrays_to_pcd(r1.result())
433
    #        self.linkedNodes[r1.result()[-1]].resource=resource
434
    #    for r2 in concurrent.futures.as_completed(results2): 
435
    #        resource=gmu.arrays_to_pcd(r2.result())
436
    #        self.linkedNodes[r2.result()[-1]].resource=resource
437
    #    for r3 in concurrent.futures.as_completed(results3): 
438
    #        # print(len(r3.result()[0]))
439
    #        resource=gmu.arrays_to_mesh(r3.result())
440
    #        self.linkedNodes[r3.result()[-1]].resource=resource
441
    #    for r4 in concurrent.futures.as_completed(results4): 
442
    #        self.linkedNodes[r4.result()[-1]].resource=r4.result()[0]
443
    #    return [n.resource for n in self.linkedNodes]
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc