Initial commit
This commit is contained in:
commit
a437c068c8
64 changed files with 561683 additions and 0 deletions
837
scripts/CityGML2OBJv2/CityGML2OBJs.py
Normal file
837
scripts/CityGML2OBJv2/CityGML2OBJs.py
Normal file
|
|
@ -0,0 +1,837 @@
|
|||
##!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import config
|
||||
# The MIT License (MIT)
|
||||
|
||||
# This code is part of the CityGML2OBJs package
|
||||
|
||||
# Copyright (c) 2014
|
||||
# Filip Biljecki
|
||||
# Delft University of Technology
|
||||
# fbiljecki@gmail.com
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import markup3dmodule
|
||||
import polygon3dmodule
|
||||
import componentseparationmodule as csm
|
||||
from lxml import etree
|
||||
import os
|
||||
import argparse
|
||||
import glob
|
||||
import numpy as np
|
||||
import itertools
|
||||
import matplotlib.pyplot as plt
|
||||
import CityGMLTranslation as cgt
|
||||
from decimal import Decimal
|
||||
from config import setVersion
|
||||
import time
|
||||
|
||||
# -- ARGUMENTS
|
||||
# -i -- input directory (it will read and convert ALL CityGML files in a directory)
|
||||
# -o -- output directory (it will output the generated OBJs in that directory in the way that Delft.gml becomes Delft.obj)
|
||||
# -- SETTINGS of the converter (can be combined):
|
||||
# -s 0 (default) -- converts all geometries in one class in one file under the same object (plain OBJ file).
|
||||
# -s 1 -- differentiate between semantics, output each semantic class as one file, e.g. Delft-WallSurface.obj. Please note that in this case the grouped "plain" OBJ is not generated.
|
||||
# if no thematic boundaries are found, this option is ignored.
|
||||
# -g 0 (default) -- keeps all objects in the same bin.
|
||||
# -g 1 -- it creates one object for every building.
|
||||
# -v 1 -- validation
|
||||
# -p 1 -- skip triangulation and write polygons. Polys with interior not supported.
|
||||
# -t 1 -- translation (reduction) of coordinates so the smallest vertex (one with the minimum coordinates) is at (0, 0)
|
||||
# -a 1 or 2 or 3 -- this is a very custom setting for adding the texture based on attributes, here you can see the settings for my particular case of the solar radiation. By default it is off.
|
||||
|
||||
# -- Text to be printed at the beginning of each OBJ
|
||||
header = """# Converted from CityGML to OBJ with CityGML2OBJs.
|
||||
# Conversion tool developed by Filip Biljecki, TU Delft <fbiljecki@gmail.com>, see more at Github:
|
||||
# https://github.com/tudelft3d/CityGML2OBJs
|
||||
#
|
||||
"""
|
||||
|
||||
|
||||
def get_index(point, list_vertices, shift=0):
|
||||
"""Index the vertices.
|
||||
The third option is for incorporating a local index (building-level) to the global one (dataset-level)."""
|
||||
global vertices
|
||||
"""Unique identifier and indexer of vertices."""
|
||||
if point in list_vertices:
|
||||
return list_vertices.index(point) + 1 + shift, list_vertices
|
||||
else:
|
||||
list_vertices.append(point)
|
||||
return list_vertices.index(point) + 1 + shift, list_vertices
|
||||
|
||||
|
||||
def write_vertices(list_vertices, cla):
|
||||
"""Write the vertices in the OBJ format."""
|
||||
global vertices_output
|
||||
for each in list_vertices:
|
||||
vertices_output[cla].append("v" + " " + str(each[0]) + " " + str(each[1]) + " " + str(each[2]) + "\n")
|
||||
|
||||
|
||||
def remove_reccuring(list_vertices):
|
||||
"""Removes recurring vertices, which messes up the triangulation.
|
||||
Inspired by http://stackoverflow.com/a/1143432"""
|
||||
# last_point = list_vertices[-1]
|
||||
list_vertices_without_last = list_vertices[:-1]
|
||||
found = set()
|
||||
for item in list_vertices_without_last:
|
||||
if str(item) not in found:
|
||||
yield item
|
||||
found.add(str(item))
|
||||
|
||||
|
||||
def poly_to_obj(poly, cl, material=None):
|
||||
"""Main conversion function of one polygon to one or more faces in OBJ,
|
||||
in a specific semantic class. Supports assigning a material."""
|
||||
global local_vertices
|
||||
global vertices
|
||||
global face_output
|
||||
# -- Decompose the polygon into exterior and interior
|
||||
e, i = markup3dmodule.polydecomposer(poly)
|
||||
# -- Points forming the exterior LinearRing
|
||||
epoints = markup3dmodule.GMLpoints(e[0])
|
||||
# print(epoints)
|
||||
# -- Clean recurring points, except the last one
|
||||
last_ep = epoints[-1]
|
||||
epoints_clean = list(remove_reccuring(epoints))
|
||||
epoints_clean.append(last_ep)
|
||||
# print("epoints: ", epoints)
|
||||
# print("epoints_clean: ", epoints_clean)
|
||||
|
||||
# -- LinearRing(s) forming the interior
|
||||
irings = []
|
||||
for iring in i:
|
||||
ipoints = markup3dmodule.GMLpoints(iring)
|
||||
# -- Clean them in the same manner as the exterior ring
|
||||
last_ip = ipoints[-1]
|
||||
ipoints_clean = list(remove_reccuring(ipoints))
|
||||
ipoints_clean.append(last_ip)
|
||||
irings.append(ipoints_clean)
|
||||
# print("irings: ", irings)
|
||||
# -- If the polygon validation option is enabled
|
||||
if VALIDATION:
|
||||
# -- Check the polygon
|
||||
valid = polygon3dmodule.isPolyValid(epoints_clean, True)
|
||||
if valid:
|
||||
for iring in irings:
|
||||
if not polygon3dmodule.isPolyValid(iring, False):
|
||||
valid = False
|
||||
# -- If everything is valid send them to the Delaunay triangulation
|
||||
if valid:
|
||||
if SKIPTRI:
|
||||
# -- Triangulation is skipped, polygons are converted directly to faces
|
||||
# -- The last point is removed since it's equal to the first one
|
||||
t = [epoints_clean[:-1]]
|
||||
else:
|
||||
# -- Triangulate polys
|
||||
# t = polygon3dmodule.triangulation(epoints, irings)
|
||||
try:
|
||||
t = polygon3dmodule.triangulation(epoints_clean, irings)
|
||||
except:
|
||||
t = []
|
||||
# t = polygon3dmodule.triangulation(epoints_clean, irings)
|
||||
|
||||
# -- Process the triangles/polygons
|
||||
for tri in t:
|
||||
# -- Face marker
|
||||
f = "f "
|
||||
# -- For each point in the triangle/polygon (face) get the index "v" or add it to the index
|
||||
for ep in range(0, len(tri)):
|
||||
v, local_vertices[cl] = get_index(tri[ep], local_vertices[cl], len(vertices[cl]))
|
||||
f += str(v) + " "
|
||||
# -- Add the material if invoked
|
||||
if material:
|
||||
face_output[cl].append("usemtl " + str(mtl(material, min_value, max_value, res)) + str("\n"))
|
||||
# -- Store all together
|
||||
face_output[cl].append(f + "\n")
|
||||
else:
|
||||
# Get the gml:id of the Polygon if it exists
|
||||
polyid = poly.xpath("@g:id", namespaces={'g': ns_gml})
|
||||
if polyid:
|
||||
polyid = polyid[0]
|
||||
print("\t\t!! Detected an invalid polygon (%s). Skipping..." % polyid)
|
||||
else:
|
||||
print("\t\t!! Detected an invalid polygon. Skipping...")
|
||||
|
||||
else:
|
||||
# -- Do exactly the same, but without the validation
|
||||
try:
|
||||
if SKIPTRI:
|
||||
t = [epoints_clean[:-1]]
|
||||
else:
|
||||
t = polygon3dmodule.triangulation(epoints_clean, irings)
|
||||
|
||||
|
||||
except:
|
||||
|
||||
t = []
|
||||
|
||||
for tri in t:
|
||||
f = "f "
|
||||
for ep in range(0, len(tri)):
|
||||
v, local_vertices[cl] = get_index(tri[ep], local_vertices[cl], len(vertices[cl]))
|
||||
f += str(v) + " "
|
||||
|
||||
if material:
|
||||
face_output[cl].append("usemtl " + str(mtl(material, min_value, max_value, res)) + str("\n"))
|
||||
face_output[cl].append(f + "\n")
|
||||
|
||||
|
||||
# -- Parse command-line arguments
|
||||
PARSER = argparse.ArgumentParser(description='Convert a CityGML to OBJ.')
|
||||
PARSER.add_argument('-i', '--directory',
|
||||
help='Directory containing CityGML file(s).', required=True)
|
||||
PARSER.add_argument('-o', '--results',
|
||||
help='Directory where the OBJ file(s) should be written.', required=True)
|
||||
PARSER.add_argument('-s', '--semantics',
|
||||
help='Write one OBJ (0) or multiple OBJ per semantic class (1). 0 is default.', required=False)
|
||||
PARSER.add_argument('-g', '--grouping',
|
||||
help='Writes all buildings in one group (0) or multiple groups (1). 0 is default.', required=False)
|
||||
PARSER.add_argument('-a', '--attribute',
|
||||
help='Creates a texture regarding the value of an attribute of the surface. No material is default.',
|
||||
required=False)
|
||||
PARSER.add_argument('-v', '--validation',
|
||||
help='Validates polygons, and if they are not valid give a warning and skip them. No validation is default.',
|
||||
required=False)
|
||||
PARSER.add_argument('-t', '--translate',
|
||||
help='Translates all vertices, so that the smallest vertex is at zero. No translation is default.',
|
||||
required=False)
|
||||
PARSER.add_argument('-p', '--polypreserve',
|
||||
help='Skip the triangulation (preserve polygons). Triangulation is default.', required=False)
|
||||
|
||||
# Changes by Th_Fr: 2 New optional parameters added, see description for details!
|
||||
PARSER.add_argument('-tC', '--translateCityGML',
|
||||
help='Perform a Translation of the CityGML Dataset into a local CRS before further processing. No translation is default.',
|
||||
required=False)
|
||||
|
||||
PARSER.add_argument('-tCw', '--translateCityGMLwrite',
|
||||
help='Perform a Translation of the CityGML Dataset into a local CRS before further processing. The translation parameters are stored in a designated .txt file. No Translation is default ',
|
||||
required=False)
|
||||
|
||||
PARSER.add_argument('-sepC', '--separateComponents',
|
||||
help='Save each building component into an individual file with the filename serving as an identifier.',
|
||||
required=False)
|
||||
|
||||
PARSER.add_argument('-appW', '--approximateWindows',
|
||||
help='Approximate windows by their convex hulls to save some processing time.',
|
||||
required=False)
|
||||
|
||||
PARSER.add_argument('-addBB', '--addBoundingBox',
|
||||
help='Add small triangles defining the bounding box to each of the components.',
|
||||
required=False)
|
||||
|
||||
# Todo: Neue funktion muss noch fertig implementiert werden
|
||||
PARSER.add_argument('-importBB', '--importBoundingBox',
|
||||
help='Add small triangles defining an imported bounding box to each of the components.',
|
||||
required=False)
|
||||
|
||||
PARSER.add_argument('-addBBJSON', '--addBoundingBoxJSON',
|
||||
help='The bounding box of the building is additionally saved in a designated json-file',
|
||||
required=False)
|
||||
|
||||
PARSER.add_argument('-tbw', '--translateBuildingWise', # todo: implementation yet to be completed
|
||||
help='Translate into a local coordinate system building-wise.',
|
||||
required=False)
|
||||
|
||||
# End of changes by Th_Fr
|
||||
|
||||
ARGS = vars(PARSER.parse_args())
|
||||
DIRECTORY = os.path.join(ARGS['directory'], '')
|
||||
RESULT = os.path.join(ARGS['results'], '')
|
||||
|
||||
SEMANTICS = ARGS['semantics']
|
||||
if SEMANTICS == '1':
|
||||
SEMANTICS = True
|
||||
elif SEMANTICS == '0':
|
||||
SEMANTICS = False
|
||||
else:
|
||||
SEMANTICS = False
|
||||
|
||||
OBJECTS = ARGS['grouping']
|
||||
if OBJECTS == '1':
|
||||
OBJECTS = True
|
||||
elif OBJECTS == '0':
|
||||
OBJECTS = False
|
||||
else:
|
||||
OBJECTS = False
|
||||
|
||||
ATTRIBUTE = ARGS['attribute']
|
||||
if ATTRIBUTE == '1':
|
||||
ATTRIBUTE = 1
|
||||
elif ATTRIBUTE == '2':
|
||||
ATTRIBUTE = 2
|
||||
elif ATTRIBUTE == '3':
|
||||
ATTRIBUTE = 3
|
||||
elif ATTRIBUTE == '0':
|
||||
ATTRIBUTE = False
|
||||
else:
|
||||
ATTRIBUTE = False
|
||||
|
||||
VALIDATION = ARGS['validation']
|
||||
if VALIDATION == '1':
|
||||
VALIDATION = True
|
||||
elif VALIDATION == '0':
|
||||
VALIDATION = False
|
||||
else:
|
||||
VALIDATION = False
|
||||
|
||||
TRANSLATE = ARGS['translate']
|
||||
if TRANSLATE == '1':
|
||||
TRANSLATE = True
|
||||
elif TRANSLATE == '0':
|
||||
TRANSLATE = False
|
||||
else:
|
||||
TRANSLATE = False
|
||||
if TRANSLATE:
|
||||
global smallest_point
|
||||
|
||||
SKIPTRI = ARGS['polypreserve']
|
||||
if SKIPTRI == '1':
|
||||
SKIPTRI = True
|
||||
elif SKIPTRI == '0':
|
||||
SKIPTRI = False
|
||||
else:
|
||||
SKIPTRI = False
|
||||
|
||||
# Changes By Th_Fr:
|
||||
|
||||
TRANSLATECGML = ARGS['translateCityGML']
|
||||
if TRANSLATECGML == '1':
|
||||
TRANSLATECGML = True
|
||||
elif TRANSLATECGML == '0':
|
||||
TRANSLATECGML = False
|
||||
else:
|
||||
TRANSLATECGML = False
|
||||
|
||||
TRANSLATECGMLW = ARGS['translateCityGMLwrite']
|
||||
if TRANSLATECGMLW == '1':
|
||||
TRANSLATECGMLW = True
|
||||
elif TRANSLATECGMLW == '0':
|
||||
TRANSLATECGMLW = False
|
||||
else:
|
||||
TRANSLATECGMLW = False
|
||||
|
||||
SEPARATERCOMPONENTS = ARGS['separateComponents']
|
||||
if SEPARATERCOMPONENTS == '1':
|
||||
SEPARATERCOMPONENTS = True
|
||||
elif SEPARATERCOMPONENTS == '0':
|
||||
SEPARATERCOMPONENTS = False
|
||||
else:
|
||||
SEPARATERCOMPONENTS = False
|
||||
|
||||
APPROXIMATEWINDOWS = ARGS['approximateWindows']
|
||||
if APPROXIMATEWINDOWS == '1':
|
||||
APPROXIMATEWINDOWS = True
|
||||
elif APPROXIMATEWINDOWS == '0':
|
||||
APPROXIMATEWINDOWS = False
|
||||
else:
|
||||
APPROXIMATEWINDOWS = False
|
||||
|
||||
ADDBOUNDINGBOX = ARGS['addBoundingBox']
|
||||
if ADDBOUNDINGBOX == '1':
|
||||
ADDBOUNDINGBOX = True
|
||||
elif ADDBOUNDINGBOX == '0':
|
||||
ADDBOUNDINGBOX = False
|
||||
else:
|
||||
ADDBOUNDINGBOX = False
|
||||
|
||||
|
||||
IMPORTBOUNDINGBOX = ARGS['importBoundingBox']
|
||||
if ADDBOUNDINGBOX == True:
|
||||
IMPORTBOUNDINGBOX = None
|
||||
|
||||
ADDBOUNDINGBOXJSON = ARGS['addBoundingBoxJSON']
|
||||
if ADDBOUNDINGBOXJSON == '1':
|
||||
ADDBOUNDINGBOXJSON = True
|
||||
elif ADDBOUNDINGBOXJSON == '0' and IMPORTBOUNDINGBOX is not None:
|
||||
ADDBOUNDINGBOXJSON = False
|
||||
else:
|
||||
ADDBOUNDINGBOXJSON = False
|
||||
|
||||
if IMPORTBOUNDINGBOX is not None:
|
||||
ADDBOUNDINGBOXJSON = True
|
||||
|
||||
TRANSLATEBUILDINGS = ARGS['translateBuildingWise'] # todo: muss noch implementiert werden
|
||||
if TRANSLATEBUILDINGS == '1':
|
||||
TRANSLATEBUILDINGS = True
|
||||
elif TRANSLATEBUILDINGS == '0':
|
||||
TRANSLATEBUILDINGS = False
|
||||
else:
|
||||
TRANSLATEBUILDINGS = False
|
||||
|
||||
# End of Changes by Th_Fr
|
||||
# -----------------------------------------------------------------
|
||||
# -- Attribute stuff
|
||||
|
||||
# -- Number of classes (colours)
|
||||
res = 101
|
||||
# -- Configuration
|
||||
# -- Color the surfaces based on the normalised kWh/m^2 value <irradiation>. The plain OBJ will be coloured for the total irradiation.
|
||||
if ATTRIBUTE == 1:
|
||||
min_value = 350.0 # 234.591880403
|
||||
max_value = 1300.0 # 1389.97943395
|
||||
elif ATTRIBUTE == 2:
|
||||
min_value = 157.0136575
|
||||
max_value = 83371.4359245
|
||||
elif ATTRIBUTE == 3:
|
||||
min_value = 24925.0
|
||||
max_value = 103454.0
|
||||
|
||||
# -- Statistic parameter
|
||||
atts = []
|
||||
|
||||
|
||||
# -- Colouring function
|
||||
def mtl(att, min_value, max_value, res):
|
||||
"""Finds the corresponding material."""
|
||||
ar = np.linspace(0, 1, res).tolist()
|
||||
# -- Get rid of floating point errors
|
||||
for i in range(0, len(ar)):
|
||||
ar[i] = round(ar[i], 4)
|
||||
# -- Normalise the attribute
|
||||
v = float(att - min_value) / (max_value - min_value)
|
||||
# -- Get the material
|
||||
assigned_material = min(ar, key=lambda x: abs(x - v))
|
||||
return str(assigned_material)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Start time
|
||||
start_time = time.time()
|
||||
# -- Start of the program
|
||||
print("CityGML2OBJ. Searching for CityGML files...")
|
||||
global _VERSION
|
||||
# -- Find all CityGML files in the directory
|
||||
os.chdir(DIRECTORY)
|
||||
# -- Supported extensions
|
||||
# Old version: types = ('*.gml', '*.GML', '*.xml', '*.XML')
|
||||
types = ('*.gml', '*.xml')
|
||||
files_found = []
|
||||
for files in types:
|
||||
files_found.extend(glob.glob(files))
|
||||
for f in files_found:
|
||||
FILENAME = f[:f.rfind('.')]
|
||||
FULLPATH = os.path.join(DIRECTORY, f)
|
||||
|
||||
# -- Reading and parsing the CityGML file(s)
|
||||
CITYGML = etree.parse(FULLPATH)
|
||||
# -- Getting the root of the XML tree
|
||||
root = CITYGML.getroot()
|
||||
# -- Determine CityGML version
|
||||
# If 1.0
|
||||
if root.tag == "{http://www.opengis.net/citygml/1.0}CityModel":
|
||||
print("CityGML 1.0")
|
||||
config.setVersion(1)
|
||||
# -- Name spaces
|
||||
ns_citygml = "http://www.opengis.net/citygml/1.0"
|
||||
ns_gml = "http://www.opengis.net/gml"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/1.0"
|
||||
ns_tran = "http://www.opengis.net/citygml/transportation/1.0"
|
||||
ns_veg = "http://www.opengis.net/citygml/vegetation/1.0"
|
||||
ns_gen = "http://www.opengis.net/citygml/generics/1.0"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:1.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/1.0"
|
||||
ns_frn = "http://www.opengis.net/citygml/cityfurniture/1.0"
|
||||
ns_tun = "http://www.opengis.net/citygml/tunnel/1.0"
|
||||
ns_wtr = "http://www.opengis.net/citygml/waterbody/1.0"
|
||||
ns_brid = "http://www.opengis.net/citygml/bridge/1.0"
|
||||
ns_app = "http://www.opengis.net/citygml/appearance/1.0"
|
||||
|
||||
# added by Th_Fr
|
||||
elif root.tag == "{http://www.opengis.net/citygml/3.0}CityModel":
|
||||
print("CityGML 3.0")
|
||||
config.setVersion(3)
|
||||
ns_citygml = "http://www.opengis.net/citygml/3.0"
|
||||
ns_con = "http://www.opengis.net/citygml/construction/3.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_gml = "http://www.opengis.net/gml/3.2"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/3.0"
|
||||
ns_app = "http://www.opengis.net/citygml/appearance/3.0"
|
||||
ns_pcl = "http://www.opengis.net/citygml/pointcloud/3.0"
|
||||
ns_gen = "http://www.opengis.net/citygml/generics/3.0"
|
||||
ns_gss = "http://www.isotc211.org/2005/gss"
|
||||
na_pfx0 = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||
ns_gsr = "http://www.isotc211.org/2005/gsr"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_gco = "http://www.isotc211.org/2005/gco"
|
||||
ns_tran = "http://www.opengis.net/citygml/transportation/3.0"
|
||||
ns_gmd = "http://www.isotc211.org/2005/gmd"
|
||||
ns_gts = "http://www.isotc211.org/2005/gts"
|
||||
ns_veg = "http://www.opengis.net/citygml/vegetation/3.0"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xal:3"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/3.0"
|
||||
ns_brid = "http://www.opengis.net/citygml/bridge/3.0"
|
||||
ns_frn = "http://www.opengis.net/citygml/cityfurniture/3.0"
|
||||
ns_tun = "http://www.opengis.net/citygml/tunnel/3.0"
|
||||
ns_wtr = "http://www.opengis.net/citygml/waterbody/3.0"
|
||||
|
||||
# -- Else probably means 2.0
|
||||
else:
|
||||
print("CityGML 2.0")
|
||||
config.setVersion(2)
|
||||
# -- Name spaces
|
||||
ns_citygml = "http://www.opengis.net/citygml/2.0"
|
||||
|
||||
ns_gml = "http://www.opengis.net/gml"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/2.0"
|
||||
ns_tran = "http://www.opengis.net/citygml/transportation/2.0"
|
||||
ns_veg = "http://www.opengis.net/citygml/vegetation/2.0"
|
||||
ns_gen = "http://www.opengis.net/citygml/generics/2.0"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/2.0"
|
||||
ns_frn = "http://www.opengis.net/citygml/cityfurniture/2.0"
|
||||
ns_tun = "http://www.opengis.net/citygml/tunnel/2.0"
|
||||
ns_wtr = "http://www.opengis.net/citygml/waterbody/2.0"
|
||||
ns_brid = "http://www.opengis.net/citygml/bridge/2.0"
|
||||
ns_app = "http://www.opengis.net/citygml/appearance/2.0"
|
||||
|
||||
nsmap = {
|
||||
None: ns_citygml,
|
||||
'gml': ns_gml,
|
||||
'bldg': ns_bldg,
|
||||
'tran': ns_tran,
|
||||
'veg': ns_veg,
|
||||
'gen': ns_gen,
|
||||
'xsi': ns_xsi,
|
||||
'xAL': ns_xAL,
|
||||
'xlink': ns_xlink,
|
||||
'dem': ns_dem,
|
||||
'frn': ns_frn,
|
||||
'tun': ns_tun,
|
||||
'brid': ns_brid,
|
||||
'app': ns_app
|
||||
|
||||
}
|
||||
|
||||
# Changes by Th_FR
|
||||
|
||||
if TRANSLATECGML:
|
||||
cgt.translateToLocalCRS(CITYGML, FILENAME, root, ns_bldg, ns_gml, ns_citygml, ns_frn, ns_veg, RESULT,
|
||||
write2file=False,
|
||||
applyHeight=Decimal("0")) # Todo: by TH_Fr: Diese Funktion ist noch nicht fertig
|
||||
|
||||
if TRANSLATECGMLW:
|
||||
cgt.translateToLocalCRS(CITYGML, FILENAME, root, ns_bldg, ns_gml, ns_citygml, ns_frn, ns_veg, RESULT,
|
||||
write2file=True, applyHeight=Decimal("0"))
|
||||
|
||||
# End of changes by Th_FR
|
||||
|
||||
# -- Empty lists for cityobjects and buildings
|
||||
cityObjects = []
|
||||
buildings = []
|
||||
other = []
|
||||
|
||||
# -- This denotes the dictionaries in which the surfaces are put.
|
||||
output = {}
|
||||
vertices_output = {}
|
||||
face_output = {}
|
||||
|
||||
# -- This denotes the dictionaries in which all surfaces are put. It is later ignored in the semantic option was invoked.
|
||||
output['All'] = []
|
||||
output['All'].append(header)
|
||||
if ATTRIBUTE:
|
||||
output['All'].append("mtllib colormap.mtl\n")
|
||||
vertices_output['All'] = []
|
||||
face_output['All'] = []
|
||||
|
||||
# -- If the semantic option was invoked, this part adds additional dictionaries.
|
||||
if SEMANTICS:
|
||||
# -- Easy to modify list of thematic boundaries
|
||||
semanticSurfaces = ['GroundSurface', 'WallSurface', 'RoofSurface', 'ClosureSurface', 'CeilingSurface',
|
||||
'InteriorWallSurface', 'FloorSurface', 'OuterCeilingSurface', 'OuterFloorSurface', 'Door',
|
||||
'Window']
|
||||
for semanticSurface in semanticSurfaces:
|
||||
output[semanticSurface] = []
|
||||
output[semanticSurface].append(header)
|
||||
# -- Add the material library
|
||||
if ATTRIBUTE:
|
||||
output[semanticSurface].append("mtllib colormap.mtl\n")
|
||||
vertices_output[semanticSurface] = []
|
||||
face_output[semanticSurface] = []
|
||||
|
||||
# -- Directory of vertices (indexing)
|
||||
vertices = {}
|
||||
vertices['All'] = []
|
||||
if SEMANTICS:
|
||||
for semanticSurface in semanticSurfaces:
|
||||
vertices[semanticSurface] = []
|
||||
vertices['Other'] = []
|
||||
face_output['Other'] = []
|
||||
output['Other'] = []
|
||||
|
||||
# -- Find all instances of cityObjectMember and put them in a list
|
||||
for obj in root.getiterator('{%s}cityObjectMember' % ns_citygml):
|
||||
cityObjects.append(obj)
|
||||
|
||||
print(FILENAME)
|
||||
|
||||
if len(cityObjects) > 0:
|
||||
# -- Report the progress and contents of the CityGML file
|
||||
print("\tThere are", len(cityObjects), "cityObject(s) in this CityGML file.")
|
||||
# -- Store each building separately
|
||||
for cityObject in cityObjects:
|
||||
for child in cityObject.getchildren():
|
||||
if child.tag == '{%s}Building' % ns_bldg:
|
||||
buildings.append(child)
|
||||
|
||||
for cityObject in cityObjects:
|
||||
for child in cityObject.getchildren():
|
||||
if child.tag == '{%s}Road' % ns_tran or child.tag == '{%s}PlantCover' % ns_veg or \
|
||||
child.tag == '{%s}GenericCityObject' % ns_gen or child.tag == '{%s}CityFurniture' % ns_frn or \
|
||||
child.tag == '{%s}Relief' % ns_dem or child.tag == '{%s}Tunnel' % ns_tun or \
|
||||
child.tag == '{%s}WaterBody' % ns_wtr or child.tag == '{%s}Bridge' % ns_brid:
|
||||
other.append(child)
|
||||
|
||||
print("\tAnalysing objects and extracting the geometry...")
|
||||
|
||||
# -- Count the buildings
|
||||
b_counter = 0
|
||||
b_total = len(buildings)
|
||||
print(" There are ", b_total, " buildings in the dataset")
|
||||
|
||||
# -- Do each building separately
|
||||
for b in buildings:
|
||||
# addd by th_fr
|
||||
if SEPARATERCOMPONENTS:
|
||||
json_filepath = RESULT + "index.json"
|
||||
# todo: mus snoch implementiert werde
|
||||
|
||||
csm.addCRSToJSON(root, json_filepath)
|
||||
csm.separateComponents(b, RESULT, APPROXIMATEWINDOWS=APPROXIMATEWINDOWS,
|
||||
ADDBOUNDINGBOX=ADDBOUNDINGBOX, ADDBOUNDINGBOXJSON=ADDBOUNDINGBOXJSON,
|
||||
TRANSLATEBUILDINGS=TRANSLATEBUILDINGS, IMPORTBOUNDINGBOX=IMPORTBOUNDINGBOX, b_counter=b_counter)
|
||||
# End time
|
||||
end_time = time.time()
|
||||
# Calculate elapsed time
|
||||
elapsed_time = end_time - start_time
|
||||
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||
b_counter += 1
|
||||
continue
|
||||
|
||||
# -- Build the local list of vertices to speed up the indexing
|
||||
local_vertices = {}
|
||||
local_vertices['All'] = []
|
||||
|
||||
if SEMANTICS:
|
||||
for semanticSurface in semanticSurfaces:
|
||||
local_vertices[semanticSurface] = []
|
||||
|
||||
# -- Increment the building counter
|
||||
b_counter += 1
|
||||
# -- If the object option is on, get the name for each building or create one
|
||||
if OBJECTS:
|
||||
ob = b.xpath("@g:id", namespaces={'g': ns_gml})
|
||||
if not ob:
|
||||
ob = b_counter
|
||||
else:
|
||||
ob = ob[0]
|
||||
|
||||
# -- Print progress for large files every 1000 buildings.
|
||||
if b_counter == 1000:
|
||||
print("\t1000... ", )
|
||||
elif b_counter % 1000 == 0 and b_counter == (b_total - b_total % 1000):
|
||||
print(str(b_counter) + "...")
|
||||
elif b_counter > 0 and b_counter % 1000 == 0:
|
||||
print(str(b_counter) + "... ", )
|
||||
|
||||
# -- Add the object identifier
|
||||
if OBJECTS:
|
||||
face_output['All'].append('o ' + str(ob) + '\n')
|
||||
|
||||
# -- Add the attribute for the building
|
||||
if ATTRIBUTE:
|
||||
for ch in b.getchildren():
|
||||
if ch.tag == "{%s}yearlyIrradiation" % ns_citygml:
|
||||
bAttVal = float(ch.text)
|
||||
|
||||
# -- OBJ with all surfaces in the same bin
|
||||
polys = markup3dmodule.polygonFinder(b)
|
||||
# -- Process each surface
|
||||
polycounter = 0
|
||||
for poly in polys:
|
||||
if ATTRIBUTE:
|
||||
poly_to_obj(poly, 'All', bAttVal)
|
||||
if ATTRIBUTE == 3:
|
||||
atts.append(bAttVal)
|
||||
else:
|
||||
# print etree.tostring(poly)
|
||||
poly_to_obj(poly, 'All')
|
||||
polycounter = polycounter + 1
|
||||
|
||||
# -- Semantic decomposition, with taking special care about the openings
|
||||
if SEMANTICS:
|
||||
# -- First take care about the openings since they can mix up
|
||||
openings = []
|
||||
openingpolygons = []
|
||||
for child in b.getiterator():
|
||||
if child.tag == '{%s}opening' % ns_bldg:
|
||||
openings.append(child)
|
||||
for o in child.findall('.//{%s}Polygon' % ns_gml):
|
||||
openingpolygons.append(o)
|
||||
|
||||
# -- Process each opening
|
||||
for o in openings:
|
||||
for child in o.getiterator():
|
||||
unique_identifier = child.xpath("@g:id", namespaces={'g': ns_gml})
|
||||
if child.tag == '{%s}Window' % ns_bldg or child.tag == '{%s}Door' % ns_bldg:
|
||||
# print(unique_identifier)
|
||||
if child.tag == '{%s}Window' % ns_bldg:
|
||||
t = 'Window'
|
||||
# print(t)
|
||||
else:
|
||||
t = 'Door'
|
||||
# print(t)
|
||||
polys = markup3dmodule.polygonFinder(o)
|
||||
for poly in polys:
|
||||
poly_to_obj(poly, t)
|
||||
|
||||
# -- Process other thematic boundaries
|
||||
for cl in output:
|
||||
cls = []
|
||||
for child in b.getiterator():
|
||||
if child.tag == '{%s}%s' % (ns_bldg, cl):
|
||||
cls.append(child)
|
||||
# -- Is this the first feature of this object?
|
||||
firstF = True
|
||||
for feature in cls:
|
||||
# -- If it is the first feature, print the object identifier
|
||||
unique_identifier = feature.xpath("@g:id", namespaces={
|
||||
'g': ns_gml})
|
||||
if OBJECTS and firstF:
|
||||
face_output[cl].append('o ' + str(ob) + "_" + str(unique_identifier) + '\n')
|
||||
|
||||
firstF = False
|
||||
# -- This is not supposed to happen, but just to be sure...
|
||||
if feature.tag == '{%s}Window' % ns_bldg or feature.tag == '{%s}Door' % ns_bldg:
|
||||
continue
|
||||
|
||||
# print(f"unigue identifier: {str(ob) + str(unique_identifier)}")
|
||||
|
||||
# -- Find all polygons in this semantic boundary hierarchy
|
||||
|
||||
for p in feature.findall('.//{%s}Polygon' % ns_gml):
|
||||
if ATTRIBUTE == 1 or ATTRIBUTE == 2:
|
||||
# -- Flush the previous value
|
||||
attVal = None
|
||||
if cl == 'RoofSurface':
|
||||
# print p.xpath("//@c:irradiation", namespaces={'c' : ns_citygml})
|
||||
# -- Silly way but it works, as I can't get the above xpath to work for some reason
|
||||
for ch in p.getchildren():
|
||||
if ATTRIBUTE == 1:
|
||||
if ch.tag == "{%s}irradiation" % ns_citygml:
|
||||
attVal = float(ch.text)
|
||||
atts.append(attVal)
|
||||
elif ATTRIBUTE == 2:
|
||||
if ch.tag == "{%s}totalIrradiation" % ns_citygml:
|
||||
attVal = float(ch.text)
|
||||
atts.append(attVal)
|
||||
elif ATTRIBUTE == 3:
|
||||
attVal = None
|
||||
if cl == 'RoofSurface':
|
||||
attVal = bAttVal
|
||||
else:
|
||||
# -- If the attribute option is off, pass no material
|
||||
attVal = None
|
||||
found_opening = False
|
||||
for optest in openingpolygons:
|
||||
if p == optest:
|
||||
found_opening = True
|
||||
break
|
||||
# -- If there is an opening skip it
|
||||
if found_opening:
|
||||
pass
|
||||
else:
|
||||
# -- Finally process the polygon
|
||||
poly_to_obj(p, cl, attVal)
|
||||
|
||||
# -- Merge the local list of vertices to the global
|
||||
for cl in local_vertices:
|
||||
for vertex in local_vertices[cl]:
|
||||
vertices[cl].append(vertex)
|
||||
|
||||
if len(other) > 0:
|
||||
vertices_output['Other'] = []
|
||||
local_vertices = {}
|
||||
local_vertices['Other'] = []
|
||||
for oth in other:
|
||||
# local_vertices = {}
|
||||
# local_vertices['All'] = []
|
||||
polys = markup3dmodule.polygonFinder(oth)
|
||||
# -- Process each surface
|
||||
for poly in polys:
|
||||
poly_to_obj(poly, 'Other')
|
||||
for vertex in local_vertices['Other']:
|
||||
vertices['Other'].append(vertex)
|
||||
|
||||
print("\tExtraction done. Sorting geometry and writing file(s).")
|
||||
|
||||
# -- Translate (convert) the vertices to a local coordinate system
|
||||
if TRANSLATE:
|
||||
print("\tTranslating the coordinates of vertices.")
|
||||
list_of_all_vertices = []
|
||||
for cl in output:
|
||||
if len(vertices[cl]) > 0:
|
||||
for vtx in vertices[cl]:
|
||||
list_of_all_vertices.append(vtx)
|
||||
smallest_vtx = polygon3dmodule.smallestPoint(list_of_all_vertices)
|
||||
dx = smallest_vtx[0]
|
||||
dy = smallest_vtx[1]
|
||||
dz = smallest_vtx[2]
|
||||
for cl in output:
|
||||
if len(vertices[cl]) > 0:
|
||||
for idx, vtx in enumerate(vertices[cl]):
|
||||
vertices[cl][idx][0] = vtx[0] - dx
|
||||
vertices[cl][idx][1] = vtx[1] - dy
|
||||
vertices[cl][idx][2] = vtx[2] - dz
|
||||
|
||||
# -- Write the OBJ(s)
|
||||
os.chdir(RESULT)
|
||||
# -- Theme by theme
|
||||
for cl in output:
|
||||
if len(vertices[cl]) > 0:
|
||||
write_vertices(vertices[cl], cl)
|
||||
output[cl].append("\n" + ''.join(vertices_output[cl]))
|
||||
output[cl].append("\n" + ''.join(face_output[cl]))
|
||||
if cl == 'All':
|
||||
adj_suffix = ""
|
||||
else:
|
||||
adj_suffix = "-" + str(cl)
|
||||
|
||||
with open(RESULT + FILENAME + str(adj_suffix) + ".obj", "w") as obj_file:
|
||||
obj_file.write(''.join(output[cl]))
|
||||
|
||||
print("\tOBJ file(s) written.")
|
||||
|
||||
# -- Print the range of attributes. Useful for defining the range of the colorbar.
|
||||
if ATTRIBUTE:
|
||||
print('\tRange of attributes:', min(atts), '--', max(atts))
|
||||
|
||||
else:
|
||||
print(
|
||||
"\tThere is a problem with this file: no cityObjects have been found. Please check if the file complies to CityGML.")
|
||||
|
||||
# End time
|
||||
end_time = time.time()
|
||||
# Calculate elapsed time
|
||||
elapsed_time = end_time - start_time
|
||||
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||
339
scripts/CityGML2OBJv2/CityGMLTranslation.py
Normal file
339
scripts/CityGML2OBJv2/CityGMLTranslation.py
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
##!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# The MIT License (MIT)
|
||||
|
||||
# This code is part of the CityGML2OBJs package
|
||||
|
||||
# Copyright (c) 2023
|
||||
# Thomas Fröch
|
||||
# Technische Universität München (TUM)
|
||||
# thomas.froech@tum.de
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import markup3dmodule as m3dm
|
||||
from decimal import Decimal, getcontext
|
||||
import numpy as np
|
||||
|
||||
# Setting the precision of Decimal
|
||||
getcontext().prec = 28
|
||||
|
||||
def performStableAddition(number1, number2):
|
||||
# Input: string number1, string number 2, output: string sum
|
||||
# Determine the number of positions after the comma.
|
||||
# print("Number 1 totally before: ", number1)
|
||||
# print("Number 2 totally before: ", number2)
|
||||
|
||||
try:
|
||||
n_after_comma_number1 = len(number1.split(".")[1])
|
||||
except:
|
||||
n_after_comma_number1 = 0
|
||||
try:
|
||||
n_after_comma_number2 = len(number2.split(".")[1])
|
||||
except:
|
||||
n_after_comma_number2 = 0
|
||||
lengths = [n_after_comma_number1, n_after_comma_number2]
|
||||
|
||||
# convert float numbers into integers by removing the comma
|
||||
# print("Number 1 before all: ", number1)
|
||||
# print("Number 2 before all: ", number2)
|
||||
number1 = number1.replace(".", "")
|
||||
number2 = number2.replace(".", "")
|
||||
|
||||
# print("Number 1 after: ", number1)
|
||||
# print("Number 2 after: ", number2)
|
||||
|
||||
# find the number with more positions after the comma
|
||||
abs_lengths = np.abs(lengths) # Erstellen eines Arrays mit den Beträgen
|
||||
max_index = np.argmax(abs_lengths)
|
||||
|
||||
# Distinguish the two different cases
|
||||
# Case 1: number one has more positions after the comma than number two
|
||||
if max_index == 0:
|
||||
# Find the difference of positions after the comma
|
||||
n_positions_difference = (lengths[0] - lengths[1])
|
||||
# print("n_positions_difference: ", n_positions_difference)
|
||||
# Fill up the missing digits of number 2 with zeros
|
||||
# print("number2 before: ", number2)
|
||||
for i in range(n_positions_difference):
|
||||
number2 = number2 + "0"
|
||||
# print("number2_after", number2)
|
||||
# print("Number1: ", number1)
|
||||
# convert both stings into integers
|
||||
number1_int = np.double(number1)
|
||||
number2_int = np.double(number2)
|
||||
|
||||
# Add both numbers
|
||||
number_sum = number1_int + number2_int
|
||||
|
||||
# convert back into string
|
||||
format_string = "{:.0f}"
|
||||
number_sum_string = format_string.format(number_sum)
|
||||
|
||||
# Insert the comma at the correct position
|
||||
length_of_number = len(number_sum_string.replace("-",
|
||||
""))
|
||||
if length_of_number >= lengths[max_index]:
|
||||
if n_after_comma_number1 != 0:
|
||||
number_sum_string = number_sum_string[:-(lengths[0])] + "." + number_sum_string[-(lengths[0]):]
|
||||
return number_sum_string
|
||||
else:
|
||||
number_sum_string = number_sum_string
|
||||
return number_sum_string
|
||||
elif length_of_number < n_after_comma_number1:
|
||||
new_string = ""
|
||||
for i in range(len(number_sum_string)):
|
||||
if number_sum_string[i] == "-":
|
||||
new_string += "0."
|
||||
else:
|
||||
new_string += number_sum_string[i]
|
||||
number_sum_string = new_string
|
||||
return number_sum_string
|
||||
else:
|
||||
number_sum_string = number_sum_string
|
||||
return number_sum_string
|
||||
|
||||
# Case 2: number two has more positions after the comma than number one
|
||||
else:
|
||||
# find the difference of positions after the comma
|
||||
n_positions_difference = lengths[1] - lengths[0]
|
||||
|
||||
# Fill up the missing digits of number 2 with zeros
|
||||
for i in range(n_positions_difference):
|
||||
number1 = number1 + "0"
|
||||
|
||||
# convert both stings into integers
|
||||
number1_int = Decimal(number1)
|
||||
number2_int = Decimal(number2)
|
||||
|
||||
# Add both numbers
|
||||
number_sum = number1_int + number2_int
|
||||
|
||||
# convert back into string
|
||||
format_string = "{:.0f}"
|
||||
number_sum_string = format_string.format(number_sum)
|
||||
|
||||
# Insert the comma at the correct position
|
||||
length_of_number = len(number_sum_string.replace("-",
|
||||
""))
|
||||
if length_of_number > n_after_comma_number2:
|
||||
if n_after_comma_number2 != 0:
|
||||
|
||||
number_sum_string = number_sum_string[:-(lengths[1])] + "." + number_sum_string[-(lengths[1]):]
|
||||
return number_sum_string
|
||||
|
||||
else:
|
||||
number_sum_string = number_sum_string
|
||||
return number_sum_string
|
||||
|
||||
elif length_of_number < n_after_comma_number2:
|
||||
new_string = ""
|
||||
for i in range(len(number_sum_string)):
|
||||
if number_sum_string[i] == "-":
|
||||
new_string += "0."
|
||||
else:
|
||||
new_string += number_sum_string[i]
|
||||
number_sum_string = new_string
|
||||
return number_sum_string
|
||||
else:
|
||||
number_sum_string = number_sum_string
|
||||
return number_sum_string
|
||||
|
||||
|
||||
# This function is used in order to extract all the envelopes from the CityGML-File
|
||||
# These envelopes are going to be used in order to determine the translation parameters later
|
||||
def getEnvelopes(root, ns_bldg, ns_gml, ns_citygml):
|
||||
envelopes = []
|
||||
for envelope in root.getiterator('{%s}Envelope' % ns_gml):
|
||||
envelopes.append(envelope)
|
||||
return envelopes
|
||||
|
||||
|
||||
# This function is used in order to calculate the translation parameters from the
|
||||
# envelopes that were previously extracted from the envelopes
|
||||
def getTranslationParameters(envelopes, ns_gml):
|
||||
# Setting up some initial values
|
||||
dx = Decimal("0")
|
||||
dy = Decimal("0")
|
||||
lowerCorner = []
|
||||
upperCorner = []
|
||||
|
||||
# Iterating through all the envelopes in the CityGML-File
|
||||
for envelope in envelopes:
|
||||
# print(" Envelope: ", envelope)
|
||||
# Finding the upper and the lower corner
|
||||
for child in envelope.getchildren():
|
||||
if child.tag == '{%s}lowerCorner' % ns_gml:
|
||||
lowerCorner.append(child.text)
|
||||
elif child.tag == '{%s}upperCorner' % ns_gml:
|
||||
upperCorner.append(child.text)
|
||||
# Converting into Decimal
|
||||
pointCounter = 0
|
||||
for point in lowerCorner:
|
||||
dy = dy + (Decimal(point.split(" ")[0]))
|
||||
dx = dx + (Decimal(point.split(" ")[1]))
|
||||
pointCounter = pointCounter + 1
|
||||
|
||||
dyret = -dy / pointCounter
|
||||
dxret = -dx / pointCounter
|
||||
|
||||
return [Decimal(str(int(dxret))), Decimal(str(int(dyret)))]
|
||||
|
||||
|
||||
# This function is used to Parse the text from the CityGML file to Decimal numbers and
|
||||
# to aplly the previously calculated translation parameters
|
||||
# Notice: this code only allows ONE "gml:PosList-Element" for each Interior and exterior of a Polygon
|
||||
# If there are more than just one,just the first one is going to be transformed
|
||||
def splitAndApplyTrafo(coordString, transParam):
|
||||
# Splitting the coordinate string by empty spaces
|
||||
split = coordString.split(" ")
|
||||
alalala = len(split)
|
||||
# Apply the Trafo
|
||||
if split[0] == '':
|
||||
split.pop(0)
|
||||
if split[-1] == "":
|
||||
split.pop(-1)
|
||||
counter = 0
|
||||
length = int(len(split))
|
||||
length_new = int(length / 3)
|
||||
for i in range(length_new):
|
||||
|
||||
|
||||
# print("y")
|
||||
split[counter] = performStableAddition(split[counter], str(transParam[1]))
|
||||
# print("x")
|
||||
split[counter + 1] = performStableAddition(split[counter + 1], str(transParam[0]))
|
||||
# print("z")
|
||||
split[counter + 2] = performStableAddition(split[counter + 2], str(transParam[2]))
|
||||
|
||||
counter += 3
|
||||
# converting back to a string
|
||||
translated = ""
|
||||
for i in split:
|
||||
if len(translated) == 0:
|
||||
translated = str(i)
|
||||
else:
|
||||
translated = translated + " " + str(i)
|
||||
return translated
|
||||
|
||||
|
||||
# This code is used in order to find all the coordinates that are defined in the CityGML-File
|
||||
# Please Notice: the search for coordinates here has the same limitations as the search for coordinates that
|
||||
# is used in the "CityGML2OBJ" functionality!
|
||||
def applyTranslationToCityGML(CITYGML, root, transParam, ns_citygml, ns_gml, ns_frn, ns_veg, filename):
|
||||
# Iterate over all the cityObjectMembers
|
||||
for obj in root.getiterator('{%s}cityObjectMember' % ns_citygml):
|
||||
# Iterate over all the children of cityObject Member
|
||||
for child in obj.getchildren():
|
||||
# Exclude all the implicitly referenced objects from the transformation
|
||||
if child.findall(
|
||||
'.//{%s}ImplicitGeometry' % ns_citygml) == []:
|
||||
polys = m3dm.polygonFinder(child)
|
||||
# Iterate over all the polygons of the children of cityObjectMember
|
||||
for poly in polys:
|
||||
# decompose all the polygons in the interior and exterior rings
|
||||
exter, inter = m3dm.polydecomposer(poly)
|
||||
# iterate over all the exterior rings
|
||||
for e in exter:
|
||||
# find all the coordinates that are stored as a "posList"
|
||||
if len(e.findall('.//{%s}posList' % ns_gml)) > 0:
|
||||
points_tmp = e.findall('.//{%s}posList' % ns_gml)[0].text
|
||||
points = points_tmp.replace('\n', ' ')
|
||||
translated = splitAndApplyTrafo(points, transParam)
|
||||
# print("Before: ", e.findall('.//{%s}posList' % ns_gml)[0].text)
|
||||
e.findall('.//{%s}posList' % ns_gml)[0].text = translated
|
||||
# print("Result: ", e.findall('.//{%s}posList' % ns_gml)[0].text)
|
||||
# find all the coordinates that are stored as "pos"
|
||||
elif len(e.findall('.//{%s}pos' % ns_gml)) > 0:
|
||||
points = e.findall('.//{%s}pos' % ns_gml)
|
||||
counter = 0
|
||||
for k in points:
|
||||
translated = splitAndApplyTrafo(k.text, transParam)
|
||||
e.findall('.//{%s}pos' % ns_gml)[counter].text = translated
|
||||
counter = counter + 1
|
||||
# iterate over all the interior rings
|
||||
for i in inter:
|
||||
# find all the coordinates that are stored as a "posList"
|
||||
if len(i.findall('.//{%s}posList' % ns_gml)) > 0:
|
||||
points_tmp = i.findall('.//{%s}posList' % ns_gml)[0].text
|
||||
points = points_tmp.replace('\n', ' ')
|
||||
translated = splitAndApplyTrafo(points, transParam)
|
||||
i.findall('.//{%s}posList' % ns_gml)[0].text = translated
|
||||
# find all the coordinates that are stored as "pos"
|
||||
elif len(i.findall('.//{%s}pos' % ns_gml)) > 0:
|
||||
points = i.findall('.//{%s}pos' % ns_gml)
|
||||
counter = 0
|
||||
for k in points:
|
||||
translated = splitAndApplyTrafo(k.text, transParam)
|
||||
i.findall('.//{%s}pos' % ns_gml)[counter].text = translated
|
||||
counter = counter + 1
|
||||
|
||||
else: # This condition is used in order to transform the reference points of the implicitly defined geometries
|
||||
# Step 1: find all the reference points:
|
||||
referencePoints = child.findall('.//{%s}referencePoint' % ns_citygml)
|
||||
for referencePoint in referencePoints:
|
||||
points_tmp = referencePoint.findall('.//{%s}pos' % ns_gml)
|
||||
points = points_tmp.replace('\n', ' ')
|
||||
counter = 0
|
||||
for l in points:
|
||||
translated = splitAndApplyTrafo(l.text, transParam)
|
||||
referencePoint.findall('.//{%s}pos' % ns_gml)[counter].text = translated
|
||||
counter = counter + 1
|
||||
|
||||
# Iterate over all the envelopes
|
||||
for envelope in root.getiterator('{%s}Envelope' % ns_gml):
|
||||
lowerCorner = envelope.findall('.//{%s}lowerCorner' % ns_gml)[0].text
|
||||
upperCorner = envelope.findall('.//{%s}upperCorner' % ns_gml)[0].text
|
||||
translatedLowerCorner = splitAndApplyTrafo(lowerCorner, transParam)
|
||||
translatedUpperCorner = splitAndApplyTrafo(upperCorner, transParam)
|
||||
envelope.findall('.//{%s}lowerCorner' % ns_gml)[0].text = translatedLowerCorner
|
||||
envelope.findall('.//{%s}upperCorner' % ns_gml)[0].text = translatedUpperCorner
|
||||
|
||||
CITYGML.write(filename + "_local_" + ".gml")
|
||||
return root
|
||||
|
||||
|
||||
# This function is used in order to write the previously calculated translation parameters to a
|
||||
# designated .txt file. The use of this functionality is optional and can be activated by setting the
|
||||
# optional "write2file" parameter to "True" when calling the "translateToLocalCRS" - function.
|
||||
def writeTransparam2File(filename, directory, transParam):
|
||||
textfileName = directory + filename + "_Translation_Parameters.txt"
|
||||
f = open(textfileName, "w")
|
||||
f.write("This file contains the translation parameters that were applied to the original CityGML file." + "\n" +
|
||||
"Conversion tool developed by Filip Biljecki, TU Delft <fbiljecki@gmail.com>" + "\n" +
|
||||
"Conversion tool extended by Thomas Fröch, TUM <thomas.froech@tum.de>" + "\n" +
|
||||
"see more at Github:" + "\n" +
|
||||
"https://github.com/tudelft3d/CityGML2OBJs" + "\n" + "\n")
|
||||
key = ['y', 'x', 'z']
|
||||
for i in range(len(transParam)):
|
||||
f.write(key[i] + ': ' + str(transParam[i]) + ' ')
|
||||
f.close()
|
||||
print("Translation parameters written to: " + textfileName)
|
||||
return 0
|
||||
|
||||
def translateToLocalCRS(CITYGML, file, root, ns_bldg, ns_gml, ns_citygml, ns_frn, ns_veg, directory, write2file=False,
|
||||
applyHeight=Decimal("0")):
|
||||
envelopes = getEnvelopes(root, ns_bldg, ns_gml, ns_citygml)
|
||||
transParam = getTranslationParameters(envelopes, ns_gml)
|
||||
transParam.append(applyHeight)
|
||||
if write2file == True:
|
||||
writeTransparam2File(file, directory, transParam)
|
||||
applyTranslationToCityGML(CITYGML, root, transParam, ns_citygml, ns_gml, ns_frn, ns_veg, file)
|
||||
return 0
|
||||
21
scripts/CityGML2OBJv2/LICENSE
Normal file
21
scripts/CityGML2OBJv2/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Chair of Geoinformatics, Technical University of Munich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
80
scripts/CityGML2OBJv2/README.md
Normal file
80
scripts/CityGML2OBJv2/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# :cityscape: CityGML2OBJ 2.0 :cityscape:
|
||||
Command line converter of **CityGML (.gml)** to **OBJ (.obj)** files, while maintaining the semantics
|
||||
|
||||

|
||||
|
||||
## :arrow_forward: How to run?
|
||||
The `CityGML2OBJs.py` represents the starting point of the code, choose this file when configuring the runtime and pass the following parameters:
|
||||
|
||||
`-i your-input-citygml-path-here`
|
||||
|
||||
`-o your-output-obj-path-here`
|
||||
|
||||
Please make sure to use the absolute paths to the respective directories.
|
||||
|
||||
and Bob's your uncle! :construction_worker:
|
||||
|
||||
### :wrench: Optional features
|
||||
|
||||
| Optional feature | specification |
|
||||
| -------- | -------- |
|
||||
| Semanitcs Option|`-s 1`|
|
||||
| Geometry Validation | `-v 1`|
|
||||
| Object Preservation | `-g 1`|
|
||||
| Skip the triangulation | `-p 1`|
|
||||
| Conversion of the resulting dataset into a local coordinate system | `-t 1`|
|
||||
| Translation of the CityGML dataset into a local coordinate system before further processing, without saving the translation parameters|`-tC 1`|
|
||||
| Translation of the CityGML dataset into a local coordinate system before further processing, with saving the translation parameters to a designated .txt file|`-tCw 1`|
|
||||
|
||||
|
||||
## :page_with_curl: Requirements
|
||||
|
||||
### Python packages:
|
||||
|
||||
+ [Numpy](http://docs.scipy.org/doc/numpy/user/install.html)
|
||||
+ [Triangle](http://dzhelil.info/triangle/)
|
||||
+ [lxml](http://lxml.de)
|
||||
+ [Shapely](https://github.com/Toblerity/Shapely)
|
||||
+ [Decimal](https://docs.python.org/3/library/decimal.html)
|
||||
|
||||
#### Optional:
|
||||
|
||||
+ [Matplotlib](http://matplotlib.org/users/installing.html)
|
||||
|
||||
### Tested:
|
||||
|
||||
Using Python 3.10 and Windows 10 OS
|
||||
|
||||
### CityGML Requirements:
|
||||
|
||||
#### Mandatory:
|
||||
|
||||
+ CityGML 1.0 or 2.0
|
||||
+ Files must end with `.gml`, `.GML`, `.xml`, or `.XML`
|
||||
+ Vertices in either `<gml:posList>` or `<gml:pos>`
|
||||
+ Your files must be valid (e.g., free check with [CityDoctor](https://transfer.hft-stuttgart.de/gitlab/citydoctor/citydoctor2)
|
||||
|
||||
#### Optional, but recommended:
|
||||
|
||||
+ `<gml:id>` for each `<bldg:Building>` and other types of city objects
|
||||
+ `<gml:id>` for each `<gml:Polygon>`
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
Information on the limitations can be found in this [Wiki Page](https://github.com/tum-gis/citygml2obj-2.0/wiki/Limitations)
|
||||
|
||||
## :handshake: Credits
|
||||
We are indebted to [Filip Biljecki](https://github.com/fbiljecki), [Hugo Ledoux](https://github.com/hugoledoux) and [Ravi Peters](https://github.com/Ylannl) from [TU Delft](https://github.com/tudelft3d) for their initial version of the CityGML2OBJs converter. The archived version of the repo can still be found here: https://github.com/tudelft3d/CityGML2OBJs; the paper:
|
||||
|
||||
Biljecki, F., & Arroyo Ohori, K. (2015). Automatic semantic-preserving conversion between OBJ and CityGML. Eurographics Workshop on Urban Data Modelling and Visualisation 2015, pp. 25-30.
|
||||
|
||||
[[PDF]](https://filipbiljecki.com/publications/2015_udmv_citygml_obj.pdf) [[DOI]](http://doi.org/10.2312/udmv.20151345)
|
||||
|
||||
## :mailbox: Contact & Feedback
|
||||
|
||||
Feel free to open a discussion under Issues or write us an email
|
||||
|
||||
- [Thomas Froech](thomas.froech@tum.de)
|
||||
- [Benedikt Schwab](benedikt.schwab@tum.de)
|
||||
- [Olaf Wysocki](olaf.wysocki@tum.de)
|
||||
65857
scripts/CityGML2OBJv2/Sy_Olkieniki_GML.obj
Normal file
65857
scripts/CityGML2OBJv2/Sy_Olkieniki_GML.obj
Normal file
File diff suppressed because it is too large
Load diff
483630
scripts/CityGML2OBJv2/Sy_Olkieniki_GML.xml
Normal file
483630
scripts/CityGML2OBJv2/Sy_Olkieniki_GML.xml
Normal file
File diff suppressed because it is too large
Load diff
Binary file not shown.
BIN
scripts/CityGML2OBJv2/__pycache__/markup3dmodule.cpython-38.pyc
Normal file
BIN
scripts/CityGML2OBJv2/__pycache__/markup3dmodule.cpython-38.pyc
Normal file
Binary file not shown.
BIN
scripts/CityGML2OBJv2/__pycache__/polygon3dmodule.cpython-38.pyc
Normal file
BIN
scripts/CityGML2OBJv2/__pycache__/polygon3dmodule.cpython-38.pyc
Normal file
Binary file not shown.
756
scripts/CityGML2OBJv2/componentseparationmodule.py
Normal file
756
scripts/CityGML2OBJv2/componentseparationmodule.py
Normal file
|
|
@ -0,0 +1,756 @@
|
|||
import re
|
||||
import config
|
||||
import markup3dmodule as m3dm
|
||||
import polygon3dmodule as p3dm
|
||||
import json
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import open3d.core as o3c
|
||||
import os
|
||||
|
||||
|
||||
# Function to create triangles at each corner in 3D
|
||||
def create_corner_triangles(box_points, triangle_size=1):
|
||||
triangles = []
|
||||
for i, point in enumerate(box_points):
|
||||
x, y, z = point
|
||||
if i == 0: # Bottom-left-front corner
|
||||
triangles.append([[x, y, z], [x + triangle_size, y, z], [x, y + triangle_size, z]])
|
||||
elif i == 1: # Bottom-right-front corner
|
||||
triangles.append([[x, y, z], [x - triangle_size, y, z], [x, y + triangle_size, z]])
|
||||
elif i == 2: # Top-left-front corner
|
||||
triangles.append([[x, y, z], [x + triangle_size, y, z], [x, y - triangle_size, z]])
|
||||
elif i == 3: # Bottom-left-back corner
|
||||
triangles.append([[x, y, z], [x + triangle_size, y, z], [x, y + triangle_size, z]])
|
||||
elif i == 4: # Top-right-back corner
|
||||
triangles.append([[x, y, z], [x - triangle_size, y, z], [x, y - triangle_size, z]])
|
||||
elif i == 5: # Top-left-back corner
|
||||
triangles.append([[x, y, z], [x + triangle_size, y, z], [x, y - triangle_size, z]])
|
||||
elif i == 6: # Bottom-right-back corner
|
||||
triangles.append([[x, y, z], [x - triangle_size, y, z], [x, y + triangle_size, z]])
|
||||
elif i == 7: # Top-right-front corner
|
||||
triangles.append([[x, y, z], [x - triangle_size, y, z], [x, y - triangle_size, z]])
|
||||
return triangles
|
||||
|
||||
|
||||
def addTranslationParameters(e, i, trans_param):
|
||||
# Convert lists to numpy arrays for easier manipulation
|
||||
e = np.array(e)
|
||||
# i = np.array(i)
|
||||
i_translated = []
|
||||
trans_param = np.array(trans_param)
|
||||
# case distinction for empty translation parameters
|
||||
if len(trans_param) > 0:
|
||||
# Subtract the translation parameters from each point
|
||||
if len(e) > 0:
|
||||
e_translated = e - trans_param
|
||||
elif len(e) == 0:
|
||||
e_translated = np.asarray([])
|
||||
# Iterate over all the different interior rings
|
||||
if len(i) > 0:
|
||||
for interior_ring in i:
|
||||
interior_ring = np.asarray(interior_ring)
|
||||
# Translate the interior ring
|
||||
interior_ring_translated = interior_ring - trans_param
|
||||
# Coollect the translated interior rings
|
||||
i_translated.append(interior_ring_translated.tolist())
|
||||
else:
|
||||
i_translated = i
|
||||
return e_translated.tolist(), i_translated
|
||||
else:
|
||||
return e.tolist(), i
|
||||
|
||||
def getBufferedBBoxPoints(b):
|
||||
# Schritt 1: identifying all wallsurfaces and roof surfaces of the building
|
||||
output = {}
|
||||
specifyVersion()
|
||||
# comprehensive list of semantic surfaces
|
||||
semanticSurfaces = ['GroundSurface', 'WallSurface', 'RoofSurface', 'ClosureSurface', 'CeilingSurface', ]
|
||||
|
||||
for semanticSurface in semanticSurfaces:
|
||||
output[semanticSurface] = []
|
||||
data = []
|
||||
for cl in output:
|
||||
cls = []
|
||||
for child in b.getiterator():
|
||||
if child.tag == '{%s}%s' % (ns_bldg, cl):
|
||||
cls.append(child)
|
||||
|
||||
for feature in cls:
|
||||
for p in feature.findall('.//{%s}Polygon' % ns_gml):
|
||||
e, i = m3dm.polydecomposer(p)
|
||||
epoints = m3dm.GMLpoints(e[0])
|
||||
# -- Clean recurring points, except the last one
|
||||
last_ep = epoints[-1]
|
||||
epoints_clean = list(remove_reccuring(epoints))
|
||||
epoints_clean.append(last_ep)
|
||||
for point in epoints_clean:
|
||||
data.append(point)
|
||||
|
||||
# Schritt 2: Idetify the Bounding volume
|
||||
# 2.1 creating an open3d pointcloud from all the idetified vertex points
|
||||
pcd = o3d.t.geometry.PointCloud(o3c.Tensor(data, o3c.float32))
|
||||
# 2.2 obtain the axis aligned boundign box of the point cloud
|
||||
axis_aligned_bb = pcd.get_axis_aligned_bounding_box()
|
||||
# Schritt 3: Construct small triangles that describe the boundign box sufficienly
|
||||
box_points = axis_aligned_bb.get_box_points().numpy().tolist()
|
||||
# Convert the list to a numpy array for easier manipulation
|
||||
box_points = np.array(box_points)
|
||||
# Calculate the min and max coordinates
|
||||
min_x, min_y, min_z = np.min(box_points, axis=0)
|
||||
max_x, max_y, max_z = np.max(box_points, axis=0)
|
||||
# Add a 3m buffer
|
||||
buffer = 3
|
||||
min_x -= buffer
|
||||
min_y -= buffer
|
||||
min_z -= buffer
|
||||
max_x += buffer
|
||||
max_y += buffer
|
||||
max_z += buffer
|
||||
# Define the buffered bounding box points
|
||||
buffered_box_points = np.array([
|
||||
[min_x, min_y, min_z],
|
||||
[max_x, min_y, min_z],
|
||||
[min_x, max_y, min_z],
|
||||
[min_x, min_y, max_z],
|
||||
[max_x, max_y, max_z],
|
||||
[min_x, max_y, max_z],
|
||||
[max_x, min_y, max_z],
|
||||
[max_x, max_y, min_z]
|
||||
])
|
||||
return buffered_box_points
|
||||
|
||||
def obtainSRSInfo(root):
|
||||
specifyVersion()
|
||||
# obtain the envelope object
|
||||
envelopes = []
|
||||
for envelope in root.getiterator('{%s}Envelope' % ns_gml):
|
||||
envelopes.append(envelope)
|
||||
|
||||
# Extracting the srsName attribute from each Envelope
|
||||
srs_names = [envelope.get('srsName') for envelope in envelopes]
|
||||
srs_Dimensions = [envelope.get('srsDimension') for envelope in envelopes]
|
||||
return srs_names, srs_Dimensions
|
||||
|
||||
|
||||
# This function is used to create a corresponding json file defining the bbox of an object for each corresponding obj file
|
||||
def writeBBOXJSON(b, overall_counter, path, b_counter, trans_param):
|
||||
if len(trans_param) > 0:
|
||||
translation_parameters = {
|
||||
"d_x": str(trans_param[0]),
|
||||
"d_y": str(trans_param[1]),
|
||||
"d_z": str(trans_param[2])
|
||||
}
|
||||
else:
|
||||
translation_parameters = {
|
||||
"d_x": str(0),
|
||||
"d_y": str(0),
|
||||
"d_z": str(0)
|
||||
}
|
||||
|
||||
buffered_box_points_global = getBufferedBBoxPoints(b)
|
||||
|
||||
# translate to the local coordinate system
|
||||
buffered_box_points, _ = addTranslationParameters(buffered_box_points_global, [], trans_param=trans_param)
|
||||
|
||||
# From this set of points, obtain the minimum set that is necessary to describe the bounding box.
|
||||
min_point = buffered_box_points[0]
|
||||
max_point = buffered_box_points[4]
|
||||
|
||||
# Construct the json file path
|
||||
json_file_path = str(path) + str(b_counter) + "_" + str(overall_counter) + "_bbox_" + ".json"
|
||||
|
||||
# Write the bbox to a designated json file
|
||||
# Prüfen, ob die JSON-Datei existiert und laden
|
||||
if os.path.exists(json_file_path):
|
||||
with open(json_file_path, 'r') as json_file:
|
||||
axis_aligned_bbox = json.load(json_file)
|
||||
|
||||
|
||||
else:
|
||||
axis_aligned_bbox = {}
|
||||
|
||||
# Neuen Identifier hinzufügen
|
||||
axis_aligned_bbox["axis_aligned_bbox"] = {
|
||||
"min_point": str(min_point),
|
||||
"max_point": str(max_point),
|
||||
"translation_parameters": translation_parameters
|
||||
|
||||
}
|
||||
|
||||
# Zuordnungen in JSON-Datei schreiben
|
||||
with open(json_file_path, 'w') as json_file:
|
||||
json.dump(axis_aligned_bbox, json_file, indent=4)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
# This function is used to add information about the used spatial reference system to the json file
|
||||
def addCRSToJSON(root, json_file_path):
|
||||
specifyVersion()
|
||||
# obtain the envelope object
|
||||
envelopes = []
|
||||
for envelope in root.getiterator('{%s}Envelope' % ns_gml):
|
||||
envelopes.append(envelope)
|
||||
if envelopes:
|
||||
# Extracting the srsName attribute from each Envelope
|
||||
srs_names = [envelope.get('srsName') for envelope in envelopes]
|
||||
srs_Dimensions = [envelope.get('srsDimension') for envelope in envelopes]
|
||||
elif not envelopes:
|
||||
srs_names = "Unkown"
|
||||
srs_Dimensions = "Unkown"
|
||||
|
||||
|
||||
used_srs = srs_names[0]
|
||||
# Prüfen, ob die JSON-Datei existiert und laden
|
||||
if os.path.exists(json_file_path):
|
||||
with open(json_file_path, 'r') as json_file:
|
||||
crs_info = json.load(json_file)
|
||||
else:
|
||||
crs_info = {}
|
||||
|
||||
# Neuen Identifier hinzufügen
|
||||
crs_info["CRS"] = {
|
||||
"srsName": used_srs,
|
||||
"srsDimensions": srs_Dimensions
|
||||
}
|
||||
|
||||
# Zuordnungen in JSON-Datei schreiben
|
||||
with open(json_file_path, 'w') as json_file:
|
||||
json.dump(crs_info, json_file, indent=4)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def claculateCornerTriangles(b, trans_param):
|
||||
buffered_box_points = getBufferedBBoxPoints(b)
|
||||
|
||||
# Translate the Bounding box into the local coordinate system
|
||||
# buffered_box_points, _ = addTranslationParameters(buffered_box_points_global, [], trans_param=trans_param)
|
||||
|
||||
# Create triangles at the corners of the buffered bounding box
|
||||
corner_triangles = create_corner_triangles(buffered_box_points)
|
||||
|
||||
# Convert the triangles to lists
|
||||
corner_triangles = [np.array(triangle).tolist() for triangle in corner_triangles]
|
||||
|
||||
print("\nCorner Triangles:")
|
||||
for i, triangle in enumerate(corner_triangles):
|
||||
print(f"Triangle {i + 1}: {triangle}")
|
||||
|
||||
return corner_triangles
|
||||
|
||||
|
||||
# diese funktion dient dazu ein JSON file zu schreiben um die meta informationen über die einzelnen objekte zuspeichern
|
||||
def add_identifier_to_json(filename, tag, parentID, gmlID, json_file_path):
|
||||
"""
|
||||
Adds the identifier information for one .obj file to a JSON file.
|
||||
|
||||
Parameters:
|
||||
- number (int): The number corresponding to the .obj file.
|
||||
- tag (str): The tag corresponding to the .obj file.
|
||||
- parentID (str): The parent ID corresponding to the .obj file.
|
||||
- gmlID (str): The gml ID corresponding to the .obj file.
|
||||
- json_file_path (str): Path to the JSON file where identifier information will be stored.
|
||||
"""
|
||||
|
||||
# Prüfen, ob die JSON-Datei existiert und laden
|
||||
if os.path.exists(json_file_path):
|
||||
with open(json_file_path, 'r') as json_file:
|
||||
identifiers = json.load(json_file)
|
||||
else:
|
||||
identifiers = {}
|
||||
|
||||
# Neuen Identifier hinzufügen
|
||||
identifiers[filename] = {
|
||||
'tag': tag,
|
||||
'parentID': parentID,
|
||||
'gmlID': gmlID
|
||||
}
|
||||
|
||||
# Zuordnungen in JSON-Datei schreiben
|
||||
with open(json_file_path, 'w') as json_file:
|
||||
json.dump(identifiers, json_file, indent=4)
|
||||
|
||||
print(f"Zuordnung für {filename} wurde gespeichert.")
|
||||
|
||||
|
||||
def perturb_points(points, perturbation_scale=1e-6):
|
||||
"""
|
||||
Perturb the points slightly to avoid degenerate cases.
|
||||
|
||||
Parameters:
|
||||
points (list of tuple of floats): A list where each element is a tuple (x, y, z) representing a 3D point.
|
||||
perturbation_scale (float): The maximum magnitude of the perturbation applied to each coordinate.
|
||||
|
||||
Returns:
|
||||
list of list of floats: Perturbed list of points.
|
||||
"""
|
||||
points_array = np.array(points)
|
||||
perturbation = np.random.uniform(-perturbation_scale, perturbation_scale, points_array.shape)
|
||||
perturbed_points = points_array + perturbation
|
||||
return perturbed_points.tolist()
|
||||
|
||||
|
||||
def write_obj_file(surfaces, filename, tag, parentid, gmlid, counter, path, tr_1, translation_parameters):
|
||||
for triangle in tr_1:
|
||||
triangle_local, _ = addTranslationParameters(triangle, [], trans_param=translation_parameters)
|
||||
surfaces.append(triangle_local)
|
||||
with open(filename, 'w') as file:
|
||||
vertex_index = 1
|
||||
for triangle in surfaces:
|
||||
for vertex in triangle:
|
||||
file.write(f"v {vertex[0]} {vertex[1]} {vertex[2]}\n")
|
||||
file.write(f"f {vertex_index} {vertex_index + 1} {vertex_index + 2}\n")
|
||||
vertex_index += 3
|
||||
add_identifier_to_json(filename, tag, parentid, gmlid, (path + "index.json"))
|
||||
|
||||
|
||||
def remove_reccuring(list_vertices):
|
||||
"""Removes recurring vertices, which messes up the triangulation.
|
||||
Inspired by http://stackoverflow.com/a/1143432"""
|
||||
# last_point = list_vertices[-1]
|
||||
list_vertices_without_last = list_vertices[:-1]
|
||||
found = set()
|
||||
for item in list_vertices_without_last:
|
||||
if str(item) not in found:
|
||||
yield item
|
||||
found.add(str(item))
|
||||
|
||||
|
||||
def separate_string(s):
|
||||
# Define the regex pattern
|
||||
pattern = r'\{([^}]*)\}(.*)'
|
||||
# Search for the pattern in the input string
|
||||
match = re.search(pattern, s)
|
||||
if match:
|
||||
# Extract the parts
|
||||
inside_braces = match.group(1)
|
||||
outside_braces = match.group(2)
|
||||
return inside_braces, outside_braces
|
||||
else:
|
||||
return None, None
|
||||
|
||||
|
||||
def specifyVersion():
|
||||
global ns_citygml
|
||||
global ns_gml
|
||||
global ns_bldg
|
||||
global ns_xsi
|
||||
global ns_xAL
|
||||
global ns_xlink
|
||||
global ns_dem
|
||||
global ns_con
|
||||
global ns_app
|
||||
global ns_pcl
|
||||
global ns_gen
|
||||
global ns_gss
|
||||
global ns_pfx0
|
||||
global ns_gsr
|
||||
global ns_tran
|
||||
global ns_gmd
|
||||
global ns_gts
|
||||
global ns_veg
|
||||
global ns_frn
|
||||
global ns_tun
|
||||
global ns_wtr
|
||||
global nsmap
|
||||
|
||||
if config.getVersion() == 1:
|
||||
# -- Name spaces for CityGML 2.0
|
||||
ns_citygml = "http://www.opengis.net/citygml/1.0"
|
||||
ns_gml = "http://www.opengis.net/gml"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/1.0"
|
||||
ns_tran = "http://www.opengis.net/citygml/transportation/1.0"
|
||||
ns_veg = "http://www.opengis.net/citygml/vegetation/1.0"
|
||||
ns_gen = "http://www.opengis.net/citygml/generics/1.0"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:1.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/1.0"
|
||||
ns_frn = "http://www.opengis.net/citygml/cityfurniture/1.0"
|
||||
ns_tun = "http://www.opengis.net/citygml/tunnel/1.0"
|
||||
ns_wtr = "http://www.opengis.net/citygml/waterbody/1.0"
|
||||
ns_brid = "http://www.opengis.net/citygml/bridge/1.0"
|
||||
ns_app = "http://www.opengis.net/citygml/appearance/1.0"
|
||||
if config.getVersion() == 2:
|
||||
# -- Name spaces for CityGML 2.0
|
||||
ns_citygml = "http://www.opengis.net/citygml/2.0"
|
||||
ns_gml = "http://www.opengis.net/gml"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/2.0"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/2.0"
|
||||
elif config.getVersion() == 3:
|
||||
# -- Name spaces for CityGML 3.0
|
||||
ns_citygml = "http://www.opengis.net/citygml/3.0"
|
||||
ns_con = "http://www.opengis.net/citygml/construction/3.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_gml = "http://www.opengis.net/gml/3.2"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/3.0"
|
||||
ns_app = "http://www.opengis.net/citygml/appearance/3.0"
|
||||
ns_pcl = "http://www.opengis.net/citygml/pointcloud/3.0"
|
||||
ns_gen = "http://www.opengis.net/citygml/generics/3.0"
|
||||
ns_gss = "http://www.isotc211.org/2005/gss"
|
||||
ns_pfx0 = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||
ns_gsr = "http://www.isotc211.org/2005/gsr"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_tran = "http://www.opengis.net/citygml/transportation/3.0"
|
||||
ns_gmd = "http://www.isotc211.org/2005/gmd"
|
||||
ns_gts = "http://www.isotc211.org/2005/gts"
|
||||
ns_veg = "http://www.opengis.net/citygml/vegetation/3.0"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xal:3"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/3.0"
|
||||
ns_frn = "http://www.opengis.net/citygml/cityfurniture/3.0"
|
||||
ns_tun = "http://www.opengis.net/citygml/tunnel/3.0"
|
||||
ns_wtr = "http://www.opengis.net/citygml/waterbody/3.0"
|
||||
|
||||
nsmap = {
|
||||
None: ns_citygml,
|
||||
'gml': ns_gml,
|
||||
'bldg': ns_bldg,
|
||||
'xsi': ns_xsi,
|
||||
'xAL': ns_xAL,
|
||||
'xlink': ns_xlink,
|
||||
'dem': ns_dem
|
||||
}
|
||||
|
||||
|
||||
def compute_convex_hull(points):
|
||||
"""
|
||||
Computes the convex hull of a set of 3D points using Open3D, including triangulation of the hull's faces.
|
||||
|
||||
Parameters:
|
||||
points (list of tuple of floats): A list where each element is a tuple (x, y, z) representing a 3D point.
|
||||
|
||||
Returns:
|
||||
list: A list of faces, where each face is a list of vertex coordinates forming that face.
|
||||
"""
|
||||
# Convert the list of points to a NumPy array
|
||||
points_array = np.array(points)
|
||||
perturbed_points = perturb_points(points_array)
|
||||
|
||||
# Create an Open3D PointCloud object
|
||||
pcd = o3d.geometry.PointCloud()
|
||||
pcd.points = o3d.utility.Vector3dVector(perturbed_points)
|
||||
|
||||
# Compute the convex hull
|
||||
hull, triangles = pcd.compute_convex_hull()
|
||||
|
||||
# Extract vertices and faces (triangles)
|
||||
hull_vertices = np.asarray(hull.vertices)
|
||||
hull_triangles = np.asarray(hull.triangles)
|
||||
|
||||
# Prepare the faces in the desired format
|
||||
faces = []
|
||||
for triangle in hull_triangles:
|
||||
face = [list(hull_vertices[vertex]) for vertex in triangle]
|
||||
faces.append(face)
|
||||
return faces
|
||||
|
||||
|
||||
def process_polygon(p, trans_param):
|
||||
e = p[0]
|
||||
i = p[1]
|
||||
e_trans, i_trans = addTranslationParameters(e, i, trans_param=trans_param)
|
||||
t = p3dm.triangulation(e_trans, i_trans)
|
||||
# print(f"t: {t}")
|
||||
return t
|
||||
|
||||
|
||||
def process_polygons_parallel(polys, trans_param):
|
||||
data = []
|
||||
results = []
|
||||
for poly in polys:
|
||||
e, i = m3dm.polydecomposer(poly)
|
||||
epoints = m3dm.GMLpoints(e[0])
|
||||
# -- Clean recurring points, except the last one
|
||||
last_ep = epoints[-1]
|
||||
epoints_clean = list(remove_reccuring(epoints))
|
||||
epoints_clean.append(last_ep)
|
||||
# -- LinearRing(s) forming the interior
|
||||
irings = []
|
||||
for iring in i:
|
||||
ipoints = m3dm.GMLpoints(iring)
|
||||
# -- Clean them in the same manner as the exterior ring
|
||||
last_ip = ipoints[-1]
|
||||
ipoints_clean = list(remove_reccuring(ipoints))
|
||||
ipoints_clean.append(last_ip)
|
||||
irings.append(ipoints_clean)
|
||||
if len(epoints_clean) > 4:
|
||||
t = process_polygon([epoints_clean, irings], trans_param=trans_param)
|
||||
results.append(t)
|
||||
# data.append(poly_components)
|
||||
if len(epoints_clean) == 4:
|
||||
epoints_clean_translated, _ = addTranslationParameters(epoints_clean, [], trans_param=trans_param)
|
||||
results.append([epoints_clean_translated])
|
||||
|
||||
# t = process_polygon(poly)
|
||||
# results.append(t)
|
||||
|
||||
# cpu_cores = os.cpu_count()
|
||||
# print(f'Number of available CPU cores (using os): {cpu_cores}')
|
||||
# with ThreadPoolExecutor(max_workers=cpu_cores) as executor:
|
||||
# # Submitting all tasks
|
||||
# futures = [executor.submit(process_polygon, p) for p in data]
|
||||
# # Collecting results
|
||||
# for future in futures:
|
||||
# result = future.result() # This will re-raise any exception caught during the execution of the task
|
||||
# results.append(result)
|
||||
return results
|
||||
|
||||
|
||||
# this is an experimental method for parallelization
|
||||
def processOpening(o, path, buildingid, overall_counter, tr_1, trans_param, b_counter):
|
||||
for child in o.getiterator():
|
||||
unique_identifier = child.xpath("@g:id", namespaces={'g': ns_gml})
|
||||
if child.tag == '{%s}Window' % ns_bldg or child.tag == '{%s}Door' % ns_bldg:
|
||||
polys = m3dm.polygonFinder(o)
|
||||
t = process_polygons_parallel(polys, trans_param=trans_param)
|
||||
triangles = []
|
||||
for poly in t:
|
||||
for tr in poly:
|
||||
triangles.append(tr)
|
||||
filename = path + str(b_counter) + "_" + str(overall_counter) + ".obj"
|
||||
write_obj_file(triangles, filename, str(child.tag), buildingid, unique_identifier, overall_counter, path,
|
||||
tr_1, trans_param)
|
||||
|
||||
|
||||
def getAllExteriorPoints(polys):
|
||||
data = []
|
||||
for poly in polys:
|
||||
e, i = m3dm.polydecomposer(poly)
|
||||
epoints = m3dm.GMLpoints(e[0])
|
||||
# -- Clean recurring points, except the last one
|
||||
last_ep = epoints[-1]
|
||||
epoints_clean = list(remove_reccuring(epoints))
|
||||
epoints_clean.append(last_ep)
|
||||
for point in epoints_clean:
|
||||
data.append(point)
|
||||
return data
|
||||
|
||||
|
||||
def processWithApproximatedWindows(o, path, buildingid, overall_counter, tr_1, translation_parameters, b_counter):
|
||||
for child in o.getiterator():
|
||||
unique_identifier = child.xpath("@g:id", namespaces={'g': ns_gml})
|
||||
if child.tag == '{%s}Window' % ns_bldg or child.tag == '{%s}Door' % ns_bldg:
|
||||
polys = m3dm.polygonFinder(o)
|
||||
exterior_points = getAllExteriorPoints(polys)
|
||||
t_global = compute_convex_hull(exterior_points)
|
||||
_, t = addTranslationParameters(e=[], i=t_global, trans_param=translation_parameters)
|
||||
filename = path + str(b_counter) + "_" + str(overall_counter) + ".obj"
|
||||
write_obj_file(t, filename, str(child.tag), buildingid, unique_identifier, overall_counter, path, tr_1,
|
||||
translation_parameters=translation_parameters)
|
||||
|
||||
|
||||
# This function is used to im port a bounding box that is associated to the corresponding building component
|
||||
# It still has to be tested if it works
|
||||
def importBoundingBox(pathToBoundingBoxFile, trans_param):
|
||||
print("Hier")
|
||||
keys = ["_xmin", "_xmax", "_ymin", "_ymax", "_zmin", "_zmax"]
|
||||
values = {}
|
||||
|
||||
with open(pathToBoundingBoxFile, 'r') as file:
|
||||
for _ in range(15):
|
||||
line = file.readline()
|
||||
for key in keys:
|
||||
match = re.search(f'"{key}"\s*:\s*([-0-9.]+)', line)
|
||||
if match:
|
||||
values[key] = float(match.group(1))
|
||||
|
||||
if not all(k in values for k in keys):
|
||||
raise ValueError("Missing bounding box values in the first 15 lines")
|
||||
|
||||
min_x, max_x = values["_xmin"], values["_xmax"]
|
||||
min_y, max_y = values["_ymin"], values["_ymax"]
|
||||
min_z, max_z = values["_zmin"], values["_zmax"]
|
||||
|
||||
print(f"min x: {min_x} min y: {min_y} , min z: {min_z}")
|
||||
print(f"max x: {max_x} max y: {max_y} , max z: {max_z}")
|
||||
|
||||
box_points = np.array([
|
||||
[min_x, min_y, min_z],
|
||||
[max_x, min_y, min_z],
|
||||
[min_x, max_y, min_z],
|
||||
[min_x, min_y, max_z],
|
||||
[max_x, max_y, max_z],
|
||||
[min_x, max_y, max_z],
|
||||
[max_x, min_y, max_z],
|
||||
[max_x, max_y, min_z]
|
||||
])
|
||||
|
||||
# Create triangles at the corners of the buffered bounding box
|
||||
corner_triangles = create_corner_triangles(box_points)
|
||||
|
||||
# Convert the triangles to lists
|
||||
corner_triangles = [np.array(triangle).tolist() for triangle in corner_triangles]
|
||||
|
||||
print("\nCorner Triangles:")
|
||||
for i, triangle in enumerate(corner_triangles):
|
||||
print(f"Triangle {i + 1}: {triangle}")
|
||||
|
||||
return corner_triangles
|
||||
|
||||
|
||||
def separateComponents(b, path, APPROXIMATEWINDOWS, ADDBOUNDINGBOX, ADDBOUNDINGBOXJSON, TRANSLATEBUILDINGS,
|
||||
IMPORTBOUNDINGBOX, b_counter):
|
||||
if TRANSLATEBUILDINGS:
|
||||
# Step 1: Obtain the axis oriented bounding box of the building
|
||||
bounding_box_points = getBufferedBBoxPoints(b)
|
||||
|
||||
# Step 2 calculate the mean value of the points that the bbox points
|
||||
translation_parameters = np.mean(bounding_box_points, axis=0)
|
||||
|
||||
if not TRANSLATEBUILDINGS: # todo: nocheinmal üerlegen ob man hier nicht vielleicht besser elif oder so nehmen sollte
|
||||
translation_parameters = []
|
||||
|
||||
if IMPORTBOUNDINGBOX != None:
|
||||
ADDBOUNDINGBOX = False # make sure that the bounding box is not calculated
|
||||
|
||||
# Option to include the small triangles to mark the buffered bounding box
|
||||
if ADDBOUNDINGBOX:
|
||||
tr_1 = claculateCornerTriangles(b, trans_param=translation_parameters)
|
||||
elif not ADDBOUNDINGBOX:
|
||||
tr_1 = []
|
||||
global overall_counter
|
||||
overall_counter = 0
|
||||
output = {}
|
||||
specifyVersion()
|
||||
# comprehensive list of semantic surfaces
|
||||
semanticSurfaces = ['GroundSurface', 'WallSurface', 'RoofSurface', 'ClosureSurface', 'CeilingSurface',
|
||||
'InteriorWallSurface', 'FloorSurface', 'OuterCeilingSurface', 'OuterFloorSurface', 'Door',
|
||||
"outerBuildingInstallation",
|
||||
'Window', "BuildingInstallation", "BuildingConstructiveElement"]
|
||||
|
||||
for semanticSurface in semanticSurfaces:
|
||||
output[semanticSurface] = []
|
||||
|
||||
# get the building id for the building
|
||||
buildingid = b.xpath("@g:id", namespaces={'g': ns_gml})
|
||||
|
||||
# Handle the case when a building has no gml:id - This should however never occur...
|
||||
if not buildingid:
|
||||
buildingid = b_counter
|
||||
|
||||
# Todo: Muss noch implementiert werden
|
||||
# Import the bounding box
|
||||
if IMPORTBOUNDINGBOX != None:
|
||||
pathToBoundingBoxFile = IMPORTBOUNDINGBOX + "/" + str(buildingid) + ".json"
|
||||
tr_1 = importBoundingBox(pathToBoundingBoxFile=pathToBoundingBoxFile, trans_param=translation_parameters)
|
||||
|
||||
if config.getVersion() != 3:
|
||||
openings = []
|
||||
openingpolygons = []
|
||||
for child in b.getiterator():
|
||||
if child.tag == '{%s}opening' % ns_bldg:
|
||||
openings.append(child)
|
||||
for o in child.findall('.//{%s}Polygon' % ns_gml):
|
||||
openingpolygons.append(o)
|
||||
|
||||
for o in openings:
|
||||
# print("approximate windows: ", APPROXIMATEWINDOWS)
|
||||
if APPROXIMATEWINDOWS:
|
||||
processWithApproximatedWindows(o, path, buildingid, overall_counter, tr_1=tr_1,
|
||||
translation_parameters=translation_parameters, b_counter=b_counter)
|
||||
if not APPROXIMATEWINDOWS:
|
||||
processOpening(o, path, buildingid, overall_counter, tr_1, trans_param=translation_parameters,
|
||||
b_counter=b_counter)
|
||||
if ADDBOUNDINGBOXJSON:
|
||||
writeBBOXJSON(b, overall_counter=overall_counter, path=path, b_counter=b_counter,
|
||||
trans_param=translation_parameters)
|
||||
overall_counter += 1
|
||||
|
||||
if config.getVersion() == 3:
|
||||
openingpolygons = []
|
||||
print("Component separation for CityGML 3.0 is not implemented yet.")
|
||||
# todo: muss noch implementiert werden
|
||||
|
||||
# -- Process other thematic boundaries
|
||||
for cl in output:
|
||||
cls = []
|
||||
for child in b.getiterator():
|
||||
if child.tag == '{%s}%s' % (ns_bldg, cl):
|
||||
cls.append(child)
|
||||
|
||||
for feature in cls:
|
||||
# -- If it is the first feature, print the object identifier
|
||||
unique_identifier = feature.xpath("@g:id", namespaces={
|
||||
'g': ns_gml})
|
||||
if str(unique_identifier) != "[]" or str(unique_identifier) == "[]":
|
||||
cleaned_filename = str(unique_identifier)
|
||||
# -- This is not supposed to happen, but just to be sure...
|
||||
if feature.tag == '{%s}Window' % ns_bldg or feature.tag == '{%s}Door' % ns_bldg:
|
||||
continue
|
||||
tag = feature.tag
|
||||
_, cleaned_tag = separate_string(tag)
|
||||
# -- Find all polygons in this semantic boundary hierarchy
|
||||
poly_t = []
|
||||
t_ges = []
|
||||
number_of_polygons = len(feature.findall('.//{%s}Polygon' % ns_gml))
|
||||
pcounter = 0
|
||||
print(f"there are {number_of_polygons} polygons there!")
|
||||
for p in feature.findall('.//{%s}Polygon' % ns_gml):
|
||||
found_opening = False
|
||||
for optest in openingpolygons:
|
||||
if p == optest:
|
||||
found_opening = True
|
||||
break
|
||||
# -- If there is an opening skip it
|
||||
if found_opening:
|
||||
pass
|
||||
else:
|
||||
# -- Decompose the polygon into exterior and interior
|
||||
e, i = m3dm.polydecomposer(p)
|
||||
# -- Points forming the exterior LinearRing
|
||||
epoints = m3dm.GMLpoints(e[0])
|
||||
# -- Clean recurring points, except the last one
|
||||
last_ep = epoints[-1]
|
||||
epoints_clean = list(remove_reccuring(epoints))
|
||||
epoints_clean.append(last_ep)
|
||||
|
||||
# -- LinearRing(s) forming the interior
|
||||
irings = []
|
||||
for iring in i:
|
||||
ipoints = m3dm.GMLpoints(iring)
|
||||
# -- Clean them in the same manner as the exterior ring
|
||||
last_ip = ipoints[-1]
|
||||
ipoints_clean = list(remove_reccuring(ipoints))
|
||||
ipoints_clean.append(last_ip)
|
||||
irings.append(ipoints_clean)
|
||||
|
||||
# Applying the translation parameters
|
||||
e_trans, i_trans = addTranslationParameters(e=epoints_clean, i=irings,
|
||||
trans_param=translation_parameters)
|
||||
|
||||
try:
|
||||
if len(epoints_clean) > 4:
|
||||
t = p3dm.triangulation(e_trans, i_trans)
|
||||
# print("Nur drei Punkte")
|
||||
poly_t.append(t)
|
||||
if len(epoints_clean) == 4:
|
||||
t = e_trans[0:3]
|
||||
poly_t.append([t])
|
||||
if len(epoints_clean) < 3:
|
||||
t = []
|
||||
# print("Empty Surface!")
|
||||
except:
|
||||
t = []
|
||||
|
||||
for surfaces in poly_t:
|
||||
t_ges = t_ges + surfaces
|
||||
|
||||
pcounter += 1
|
||||
if pcounter % 100 == 0:
|
||||
print(pcounter)
|
||||
|
||||
filename = path + str(b_counter) + "_" + str(overall_counter) + ".obj"
|
||||
if ADDBOUNDINGBOXJSON:
|
||||
writeBBOXJSON(b, overall_counter=overall_counter, path=path, b_counter=b_counter,
|
||||
trans_param=translation_parameters)
|
||||
write_obj_file(t_ges, filename, str(feature.tag), buildingid, cleaned_filename, overall_counter, path,
|
||||
tr_1, translation_parameters=translation_parameters)
|
||||
overall_counter += 1
|
||||
|
||||
print("Segmentation finished!")
|
||||
return 0
|
||||
7
scripts/CityGML2OBJv2/config.py
Normal file
7
scripts/CityGML2OBJv2/config.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
def setVersion(version):
|
||||
global VERSION
|
||||
VERSION = version
|
||||
print(f"version: {VERSION}")
|
||||
|
||||
def getVersion():
|
||||
return VERSION
|
||||
52
scripts/CityGML2OBJv2/generateMTL.py
Normal file
52
scripts/CityGML2OBJv2/generateMTL.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# The MIT License (MIT)
|
||||
|
||||
# This code is part of the CityGML2OBJs package
|
||||
|
||||
# Copyright (c) 2014
|
||||
# Filip Biljecki
|
||||
# Delft University of Technology
|
||||
# fbiljecki@gmail.com
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.cm as cm
|
||||
|
||||
#-- Number of classes of the colormap
|
||||
no_values = 101
|
||||
|
||||
#-- Select the colormap and get its RGB values for each of the classes
|
||||
colormap = cm.get_cmap("afmhot", no_values) # http://matplotlib.org/examples/color/colormaps_reference.html
|
||||
colormap_vals = colormap(np.arange(no_values)).tolist()
|
||||
|
||||
#-- This is the MTL file
|
||||
mtlcontents = ""
|
||||
|
||||
#-- Class by class... MTL!
|
||||
for i in range(0, no_values):
|
||||
b = float(i)/100
|
||||
mtlcontents += "newmtl " + str(b) + "\n"
|
||||
mtlcontents += "Ka " + str(colormap_vals[i][0]) + " " + str(colormap_vals[i][1]) + " " + str(colormap_vals[i][2]) + "\n"
|
||||
mtlcontents += "Kd " + str(colormap_vals[i][0]) + " " + str(colormap_vals[i][1]) + " " + str(colormap_vals[i][2]) + "\n"
|
||||
#-- Write the MTL
|
||||
with open("colormap.mtl", "w") as mtl_file:
|
||||
mtl_file.write(mtlcontents)
|
||||
148
scripts/CityGML2OBJv2/markup3dmodule.py
Normal file
148
scripts/CityGML2OBJv2/markup3dmodule.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import config
|
||||
# The MIT License (MIT)
|
||||
|
||||
# This code is part of the CityGML2OBJs package
|
||||
|
||||
# Copyright (c) 2014
|
||||
# Filip Biljecki
|
||||
# Delft University of Technology
|
||||
# fbiljecki@gmail.com
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
from config import setVersion
|
||||
def specifyVersion():
|
||||
global ns_citygml
|
||||
global ns_gml
|
||||
global ns_bldg
|
||||
global ns_xsi
|
||||
global ns_xAL
|
||||
global ns_xlink
|
||||
global ns_dem
|
||||
global ns_con
|
||||
global ns_app
|
||||
global ns_pcl
|
||||
global ns_gen
|
||||
global ns_gss
|
||||
global ns_pfx0
|
||||
global ns_gsr
|
||||
global ns_tran
|
||||
global ns_gmd
|
||||
global ns_gts
|
||||
global ns_veg
|
||||
global ns_frn
|
||||
global ns_tun
|
||||
global ns_wtr
|
||||
global nsmap
|
||||
|
||||
#print("config.getVerision", config.getVersion())
|
||||
if config.getVersion() == 2 or config.getVersion() == 1:
|
||||
# -- Name spaces for CityGML 2.0
|
||||
ns_citygml = "http://www.opengis.net/citygml/2.0"
|
||||
ns_gml = "http://www.opengis.net/gml"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/2.0"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/2.0"
|
||||
elif config.getVersion() == 3:
|
||||
# -- Name spaces for CityGML 3.0
|
||||
ns_citygml = "http://www.opengis.net/citygml/3.0"
|
||||
ns_con = "http://www.opengis.net/citygml/construction/3.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_gml = "http://www.opengis.net/gml/3.2"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/3.0"
|
||||
ns_app = "http://www.opengis.net/citygml/appearance/3.0"
|
||||
ns_pcl = "http://www.opengis.net/citygml/pointcloud/3.0"
|
||||
ns_gen = "http://www.opengis.net/citygml/generics/3.0"
|
||||
ns_gss = "http://www.isotc211.org/2005/gss"
|
||||
ns_pfx0 = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||
ns_gsr = "http://www.isotc211.org/2005/gsr"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_tran = "http://www.opengis.net/citygml/transportation/3.0"
|
||||
ns_gmd = "http://www.isotc211.org/2005/gmd"
|
||||
ns_gts = "http://www.isotc211.org/2005/gts"
|
||||
ns_veg = "http://www.opengis.net/citygml/vegetation/3.0"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xal:3"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/3.0"
|
||||
ns_frn = "http://www.opengis.net/citygml/cityfurniture/3.0"
|
||||
ns_tun = "http://www.opengis.net/citygml/tunnel/3.0"
|
||||
ns_wtr = "http://www.opengis.net/citygml/waterbody/3.0"
|
||||
|
||||
nsmap = {
|
||||
None: ns_citygml,
|
||||
'gml': ns_gml,
|
||||
'bldg': ns_bldg,
|
||||
'xsi': ns_xsi,
|
||||
'xAL': ns_xAL,
|
||||
'xlink': ns_xlink,
|
||||
'dem': ns_dem
|
||||
}
|
||||
|
||||
|
||||
def polydecomposer(polygon):
|
||||
"""Extracts the <gml:exterior> and <gml:interior> of a <gml:Polygon>."""
|
||||
specifyVersion()
|
||||
exter = polygon.findall('.//{%s}exterior' % ns_gml)
|
||||
inter = polygon.findall('.//{%s}interior' % ns_gml)
|
||||
return exter, inter
|
||||
|
||||
|
||||
def polygonFinder(GMLelement):
|
||||
"""Find the <gml:polygon> element."""
|
||||
specifyVersion()
|
||||
#print("NSgml:", ns_gml)
|
||||
polygonsLocal = GMLelement.findall('.//{%s}Polygon' % ns_gml)
|
||||
#print(polygonsLocal)
|
||||
|
||||
#for polygon in polygonsLocal:
|
||||
# gml_id = polygon.get('{%s}id' % ns_gml)
|
||||
# print(gml_id)
|
||||
return polygonsLocal
|
||||
|
||||
|
||||
def GMLpoints(ring):
|
||||
specifyVersion()
|
||||
"Extract points from a <gml:LinearRing>."
|
||||
# -- List containing points
|
||||
listPoints = []
|
||||
# -- Read the <gml:posList> value and convert to string
|
||||
if len(ring.findall('.//{%s}posList' % ns_gml)) > 0:
|
||||
points = ring.findall('.//{%s}posList' % ns_gml)[0].text
|
||||
# -- List of coordinates
|
||||
coords = points.split()
|
||||
assert (len(coords) % 3 == 0)
|
||||
# -- Store the coordinate tuple
|
||||
for i in range(0, len(coords), 3):
|
||||
listPoints.append([float(coords[i]), float(coords[i + 1]), float(coords[i + 2])])
|
||||
elif len(ring.findall('.//{%s}pos' % ns_gml)) > 0:
|
||||
points = ring.findall('.//{%s}pos' % ns_gml)
|
||||
# -- Extract each point separately
|
||||
for p in points:
|
||||
coords = p.text.split()
|
||||
assert (len(coords) % 3 == 0)
|
||||
# -- Store the coordinate tuple
|
||||
for i in range(0, len(coords), 3):
|
||||
listPoints.append([float(coords[i]), float(coords[i + 1]), float(coords[i + 2])])
|
||||
else:
|
||||
return None
|
||||
|
||||
return listPoints
|
||||
77
scripts/CityGML2OBJv2/plotcolorbar.py
Normal file
77
scripts/CityGML2OBJv2/plotcolorbar.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# The MIT License (MIT)
|
||||
|
||||
# This code is part of the CityGML2OBJs package
|
||||
|
||||
# Copyright (c) 2014
|
||||
# Filip Biljecki
|
||||
# Delft University of Technology
|
||||
# fbiljecki@gmail.com
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import matplotlib as mpl
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
#-- Resource: http://matplotlib.org/examples/api/colorbar_only.html
|
||||
plt.rc('text', usetex=True)
|
||||
plt.rc('font', family='serif')
|
||||
#-- Make a figure and axes with dimensions as desired
|
||||
fig = plt.figure(figsize=(8, 1))
|
||||
ax1 = fig.add_axes([0.05, 0.80, 0.9, 0.15])
|
||||
|
||||
#-- Bounds
|
||||
vmin = 350
|
||||
vmax = 1300
|
||||
|
||||
#-- Colormap
|
||||
cmap = mpl.cm.afmhot
|
||||
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
|
||||
|
||||
cb1 = mpl.colorbar.ColorbarBase(ax1, cmap=cmap,
|
||||
norm=norm,
|
||||
orientation='horizontal')
|
||||
|
||||
#-- Label on the axis
|
||||
cb1.set_label(r"Annual solar irradiation [kWh/m$^{2}$/year]")
|
||||
|
||||
|
||||
cmap = mpl.colors.ListedColormap(['r', 'g', 'b', 'c'])
|
||||
cmap.set_over('0.25')
|
||||
cmap.set_under('0.75')
|
||||
|
||||
bounds = [1, 2, 4, 7, 8]
|
||||
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
|
||||
|
||||
#-- Change the last tick label
|
||||
labels = [item.get_text() for item in cb1.ax.get_xticklabels()]
|
||||
li = 0
|
||||
for l in labels:
|
||||
# labels[li] = r"$"+str(l)+"$"
|
||||
labels[li] = r""+str(l)
|
||||
li += 1
|
||||
labels[-1] = r"$\geq "+str(vmax) + "$"
|
||||
cb1.ax.set_xticklabels(labels)
|
||||
|
||||
#-- Output
|
||||
plt.savefig('colorbar.pdf', transparent=True)
|
||||
plt.savefig('colorbar.png', dpi=600, transparent=True)
|
||||
plt.show()
|
||||
716
scripts/CityGML2OBJv2/polygon3dmodule.py
Normal file
716
scripts/CityGML2OBJv2/polygon3dmodule.py
Normal file
|
|
@ -0,0 +1,716 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# The MIT License (MIT)
|
||||
|
||||
# This code is part of the CityGML2OBJs package
|
||||
|
||||
# Copyright (c) 2014
|
||||
# Filip Biljecki
|
||||
# Delft University of Technology
|
||||
# fbiljecki@gmail.com
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import math
|
||||
import markup3dmodule
|
||||
from lxml import etree
|
||||
import copy
|
||||
import triangle
|
||||
import numpy as np
|
||||
import shapely
|
||||
from sklearn.decomposition import PCA
|
||||
|
||||
|
||||
def getAreaOfGML(poly, height=True):
|
||||
"""Function which reads <gml:Polygon> and returns its area.
|
||||
The function also accounts for the interior and checks for the validity of the polygon."""
|
||||
exteriorarea = 0.0
|
||||
interiorarea = 0.0
|
||||
# -- Decompose the exterior and interior boundary
|
||||
e, i = markup3dmodule.polydecomposer(poly)
|
||||
# -- Extract points in the <gml:LinearRing> of <gml:exterior>
|
||||
epoints = markup3dmodule.GMLpoints(e[0])
|
||||
if isPolyValid(epoints):
|
||||
if height:
|
||||
exteriorarea += get3DArea(epoints)
|
||||
else:
|
||||
exteriorarea += get2DArea(epoints)
|
||||
for idx, iring in enumerate(i):
|
||||
# -- Extract points in the <gml:LinearRing> of <gml:interior>
|
||||
ipoints = markup3dmodule.GMLpoints(iring)
|
||||
if isPolyValid(ipoints):
|
||||
if height:
|
||||
interiorarea += get3DArea(ipoints)
|
||||
else:
|
||||
interiorarea += get2DArea(ipoints)
|
||||
# -- Account for the interior
|
||||
area = exteriorarea - interiorarea
|
||||
# -- Area in dimensionless units (coordinate units)
|
||||
return area
|
||||
|
||||
|
||||
# -- Validity of a polygon ---------
|
||||
def isPolyValid(polypoints, output=True):
|
||||
"""Checks if a polygon is valid. Second option is to supress output."""
|
||||
# -- Number of points of the polygon (including the doubled first/last point)
|
||||
npolypoints = len(polypoints)
|
||||
# -- Assume that it is valid, and try to disprove the assumption
|
||||
valid = True
|
||||
# -- Check if last point equal
|
||||
if polypoints[0] != polypoints[-1]:
|
||||
if output:
|
||||
print("\t\tA degenerate polygon. First and last points do not match.")
|
||||
valid = False
|
||||
# -- Check if it has at least three points
|
||||
if npolypoints < 4: # -- Four because the first point is doubled as the last one in the ring
|
||||
if output:
|
||||
print("\t\tA degenerate polygon. The number of points is smaller than 3.")
|
||||
valid = False
|
||||
# -- Check if the points are planar
|
||||
if not isPolyPlanar(polypoints):
|
||||
if output:
|
||||
print("\t\tA degenerate polygon. The points are not planar.")
|
||||
valid = False
|
||||
# -- Check if some of the points are repeating
|
||||
for i in range(1, npolypoints):
|
||||
if polypoints[i] == polypoints[i - 1]:
|
||||
if output:
|
||||
print("\t\tA degenerate polygon. There are identical points.")
|
||||
valid = False
|
||||
# -- Check if the polygon does not have self-intersections
|
||||
# -- Disabled, something doesn't work here, will work on this later.
|
||||
# if not isPolySimple(polypoints):
|
||||
# print "A degenerate polygon. The edges are intersecting."
|
||||
# valid = False
|
||||
return valid
|
||||
|
||||
|
||||
def isPolyPlanar(polypoints):
|
||||
"""Checks if a polygon is planar."""
|
||||
# -- Normal of the polygon from the first three points
|
||||
try:
|
||||
normal = unit_normal(polypoints[0], polypoints[1], polypoints[2])
|
||||
except:
|
||||
return False
|
||||
# -- Number of points
|
||||
npolypoints = len(polypoints)
|
||||
# -- Tolerance
|
||||
eps = 0.01
|
||||
# -- Assumes planarity
|
||||
planar = True
|
||||
for i in range(3, npolypoints):
|
||||
vector = [polypoints[i][0] - polypoints[0][0], polypoints[i][1] - polypoints[0][1],
|
||||
polypoints[i][2] - polypoints[0][2]]
|
||||
if math.fabs(dot(vector, normal)) > eps:
|
||||
planar = False
|
||||
return planar
|
||||
|
||||
|
||||
def isPolySimple(polypoints): #todo: this function has to be adapted
|
||||
"""Checks if the polygon is simple, i.e. it does not have any self-intersections.
|
||||
Inspired by http://www.win.tue.nl/~vanwijk/2IV60/2IV60_exercise_3_answers.pdf"""
|
||||
npolypoints = len(polypoints)
|
||||
# -- Check if the polygon is vertical, i.e. a projection cannot be made.
|
||||
# -- First copy the list so the originals are not modified
|
||||
temppolypoints = copy.deepcopy(polypoints)
|
||||
newpolypoints = copy.deepcopy(temppolypoints)
|
||||
# -- If the polygon is vertical
|
||||
#if math.fabs(unit_normal(temppolypoints[0], temppolypoints[1], temppolypoints[2])[2]) < 10e-6:
|
||||
# vertical = True
|
||||
|
||||
#else:
|
||||
# vertical = False
|
||||
|
||||
normal = calculate_polygon_normal(temppolypoints)
|
||||
if math.fabs(normal[2]) < 10e-6:
|
||||
vertical = True
|
||||
print("The polygon is vertical 2")
|
||||
print("math.fabs(normal[2]): ", math.fabs(normal[2]))
|
||||
else:
|
||||
vertical = False
|
||||
print("Not vertical 2")
|
||||
|
||||
# -- We want to project the vertical polygon to the XZ plane
|
||||
# -- If a polygon is parallel with the YZ plane that will not be possible
|
||||
YZ = True
|
||||
for i in range(1, npolypoints):
|
||||
if temppolypoints[i][0] != temppolypoints[0][0]:
|
||||
YZ = False
|
||||
continue
|
||||
# -- Project the plane in the special case
|
||||
if YZ:
|
||||
for i in range(0, npolypoints):
|
||||
newpolypoints[i][0] = temppolypoints[i][1]
|
||||
newpolypoints[i][1] = temppolypoints[i][2]
|
||||
# -- Project the plane
|
||||
elif vertical:
|
||||
for i in range(0, npolypoints):
|
||||
newpolypoints[i][1] = temppolypoints[i][2]
|
||||
else:
|
||||
pass # -- No changes here
|
||||
# -- Check for the self-intersection edge by edge
|
||||
for i in range(0, npolypoints - 3):
|
||||
if i == 0:
|
||||
m = npolypoints - 3
|
||||
else:
|
||||
m = npolypoints - 2
|
||||
for j in range(i + 2, m):
|
||||
if intersection(newpolypoints[i], newpolypoints[i + 1], newpolypoints[j % npolypoints],
|
||||
newpolypoints[(j + 1) % npolypoints]):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def intersection(p, q, r, s):
|
||||
"""Check if two line segments (pq and rs) intersect. Computation is in 2D.
|
||||
Inspired by http://www.win.tue.nl/~vanwijk/2IV60/2IV60_exercise_3_answers.pdf"""
|
||||
|
||||
eps = 10e-6
|
||||
|
||||
V = [q[0] - p[0], q[1] - p[1]]
|
||||
W = [r[0] - s[0], r[1] - s[1]]
|
||||
|
||||
d = V[0] * W[1] - W[0] * V[1]
|
||||
|
||||
if math.fabs(d) < eps:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
# ------------------------------------------
|
||||
|
||||
def collinear(p0, p1, p2):
|
||||
# -- http://stackoverflow.com/a/9609069
|
||||
x1, y1 = p1[0] - p0[0], p1[1] - p0[1]
|
||||
x2, y2 = p2[0] - p0[0], p2[1] - p0[1]
|
||||
return x1 * y2 - x2 * y1 < 1e-12
|
||||
|
||||
|
||||
# -- Area and other handy computations
|
||||
def det(a):
|
||||
"""Determinant of matrix a."""
|
||||
return a[0][0] * a[1][1] * a[2][2] + a[0][1] * a[1][2] * a[2][0] + a[0][2] * a[1][0] * a[2][1] - a[0][2] * a[1][1] * \
|
||||
a[2][0] - a[0][1] * a[1][0] * a[2][2] - a[0][0] * a[1][2] * a[2][1]
|
||||
|
||||
|
||||
def unit_normal(a, b, c):
|
||||
"""Unit normal vector of plane defined by points a, b, and c."""
|
||||
x = det([[1, a[1], a[2]],
|
||||
[1, b[1], b[2]],
|
||||
[1, c[1], c[2]]])
|
||||
y = det([[a[0], 1, a[2]],
|
||||
[b[0], 1, b[2]],
|
||||
[c[0], 1, c[2]]])
|
||||
z = det([[a[0], a[1], 1],
|
||||
[b[0], b[1], 1],
|
||||
[c[0], c[1], 1]])
|
||||
magnitude = (x ** 2 + y ** 2 + z ** 2) ** .5
|
||||
if magnitude == 0.0:
|
||||
raise ValueError(
|
||||
"The normal of the polygon has no magnitude. Check the polygon. The most common cause for this are two identical sequential points or collinear points.")
|
||||
return (x / magnitude, y / magnitude, z / magnitude)
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
"""Dot product of vectors a and b."""
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
|
||||
|
||||
def cross(a, b):
|
||||
"""Cross product of vectors a and b."""
|
||||
x = a[1] * b[2] - a[2] * b[1]
|
||||
y = a[2] * b[0] - a[0] * b[2]
|
||||
z = a[0] * b[1] - a[1] * b[0]
|
||||
return (x, y, z)
|
||||
|
||||
|
||||
def get3DArea(polypoints):
|
||||
"""Function which reads the list of coordinates and returns its area.
|
||||
The code has been borrowed from http://stackoverflow.com/questions/12642256/python-find-area-of-polygon-from-xyz-coordinates"""
|
||||
# -- Compute the area
|
||||
total = [0, 0, 0]
|
||||
for i in range(len(polypoints)):
|
||||
vi1 = polypoints[i]
|
||||
if i is len(polypoints) - 1:
|
||||
vi2 = polypoints[0]
|
||||
else:
|
||||
vi2 = polypoints[i + 1]
|
||||
prod = cross(vi1, vi2)
|
||||
total[0] += prod[0]
|
||||
total[1] += prod[1]
|
||||
total[2] += prod[2]
|
||||
result = dot(total, unit_normal(polypoints[0], polypoints[1], polypoints[2]))
|
||||
return math.fabs(result * .5)
|
||||
|
||||
|
||||
def get2DArea(polypoints):
|
||||
"""Reads the list of coordinates and returns its projected area (disregards z coords)."""
|
||||
flatpolypoints = copy.deepcopy(polypoints)
|
||||
for p in flatpolypoints:
|
||||
p[2] = 0.0
|
||||
return get3DArea(flatpolypoints)
|
||||
|
||||
|
||||
def getNormal(polypoints):
|
||||
"""Get the normal of the first three points of a polygon. Assumes planarity."""
|
||||
return unit_normal(polypoints[0], polypoints[1], polypoints[2])
|
||||
|
||||
|
||||
def getAngles(normal):
|
||||
"""Get the azimuth and altitude from the normal vector."""
|
||||
# -- Convert from polar system to azimuth
|
||||
azimuth = 90 - math.degrees(math.atan2(normal[1], normal[0]))
|
||||
if azimuth >= 360.0:
|
||||
azimuth -= 360.0
|
||||
elif azimuth < 0.0:
|
||||
azimuth += 360.0
|
||||
t = math.sqrt(normal[0] ** 2 + normal[1] ** 2)
|
||||
if t == 0:
|
||||
tilt = 0.0
|
||||
else:
|
||||
tilt = 90 - math.degrees(math.atan(normal[2] / t)) # 0 for flat roof, 90 for wall
|
||||
tilt = round(tilt, 3)
|
||||
|
||||
return azimuth, tilt
|
||||
|
||||
|
||||
def GMLstring2points(pointstring):
|
||||
"""Convert list of points in string to a list of points. Works for 3D points."""
|
||||
listPoints = []
|
||||
# -- List of coordinates
|
||||
coords = pointstring.split()
|
||||
# -- Store the coordinate tuple
|
||||
assert (len(coords) % 3 == 0)
|
||||
for i in range(0, len(coords), 3):
|
||||
listPoints.append([float(coords[i]), float(coords[i + 1]), float(coords[i + 2])])
|
||||
return listPoints
|
||||
|
||||
|
||||
def smallestPoint(list_of_points):
|
||||
"Finds the smallest point from a three-dimensional tuple list."
|
||||
smallest = []
|
||||
# -- Sort the points
|
||||
sorted_points = sorted(list_of_points, key=lambda x: (x[0], x[1], x[2]))
|
||||
# -- First one is the smallest one
|
||||
smallest = sorted_points[0]
|
||||
return smallest
|
||||
|
||||
|
||||
def highestPoint(list_of_points, a=None):
|
||||
"Finds the highest point from a three-dimensional tuple list."
|
||||
highest = []
|
||||
# -- Sort the points
|
||||
sorted_points = sorted(list_of_points, key=lambda x: (x[0], x[1], x[2]))
|
||||
# -- Last one is the highest one
|
||||
if a is not None:
|
||||
equalZ = True
|
||||
for i in range(-1, -1 * len(list_of_points), -1):
|
||||
if equalZ:
|
||||
highest = sorted_points[i]
|
||||
if highest[2] != a[2]:
|
||||
equalZ = False
|
||||
break
|
||||
else:
|
||||
break
|
||||
else:
|
||||
highest = sorted_points[-1]
|
||||
return highest
|
||||
|
||||
|
||||
def centroid(list_of_points):
|
||||
"""Returns the centroid of the list of points."""
|
||||
sum_x = 0
|
||||
sum_y = 0
|
||||
sum_z = 0
|
||||
n = float(len(list_of_points))
|
||||
for p in list_of_points:
|
||||
sum_x += float(p[0])
|
||||
sum_y += float(p[1])
|
||||
sum_z += float(p[2])
|
||||
return [sum_x / n, sum_y / n, sum_z / n]
|
||||
|
||||
# This function delivers very unsatisfying results for some reason.
|
||||
# The returned point lies on the contour of the polygon sometimes, wich then messes up the triangulation
|
||||
def point_inside(list_of_points):
|
||||
"""Returns a point that is guaranteed to be inside the polygon, thanks to Shapely."""
|
||||
# Th_Fr: new function that actually works
|
||||
representative_point_tmp = centroid(list_of_points)
|
||||
representative_point = shapely.geometry.Point(representative_point_tmp)
|
||||
# End of the changes by Th_Fr
|
||||
return representative_point.coords
|
||||
|
||||
|
||||
def plane(a, b, c):
|
||||
"""Returns the equation of a three-dimensional plane in space by entering the three coordinates of the plane."""
|
||||
p_a = (b[1] - a[1]) * (c[2] - a[2]) - (c[1] - a[1]) * (b[2] - a[2])
|
||||
p_b = (b[2] - a[2]) * (c[0] - a[0]) - (c[2] - a[2]) * (b[0] - a[0])
|
||||
p_c = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])
|
||||
p_d = -1 * (p_a * a[0] + p_b * a[1] + p_c * a[2])
|
||||
return p_a, p_b, p_c, p_d
|
||||
|
||||
# added by Th_Fr
|
||||
def planeAdjusted(points):
|
||||
"""
|
||||
Returns the equation of a plane in three dimensions using PCA.
|
||||
|
||||
Parameters:
|
||||
points: list of lists or numpy array of shape (n, 3)
|
||||
List of points in 3D space [x, y, z] through which the plane should pass.
|
||||
At least 3 points are required to define a plane uniquely.
|
||||
|
||||
Returns:
|
||||
p_a, p_b, p_c, p_d: float
|
||||
Parameters of the plane equation ax + by + cz + d = 0.
|
||||
"""
|
||||
# Convert points to numpy array for easier manipulation
|
||||
points = np.array(points)
|
||||
|
||||
# Check if at least 3 points are provided
|
||||
if points.shape[0] < 3:
|
||||
raise ValueError("At least 3 points are required to define a plane.")
|
||||
|
||||
# Use PCA to fit the plane
|
||||
pca = PCA(n_components=3)
|
||||
pca.fit(points)
|
||||
normal = pca.components_[2] # The normal vector to the plane
|
||||
|
||||
# Extract coefficients
|
||||
p_a, p_b, p_c = normal
|
||||
p_d = -np.dot(normal, pca.mean_) # Calculate d using the mean of points
|
||||
|
||||
return p_a, p_b, p_c, p_d
|
||||
|
||||
|
||||
def get_height(plane, x, y):
|
||||
"""Get the missing coordinate from the plane equation and the partial coordinates."""
|
||||
p_a, p_b, p_c, p_d = plane
|
||||
z = (-p_a * x - p_b * y - p_d) / p_c
|
||||
return z
|
||||
|
||||
|
||||
def get_y(plane, x, z):
|
||||
"""Get the missing coordinate from the plane equation and the partial coordinates."""
|
||||
p_a, p_b, p_c, p_d = plane
|
||||
y = (-p_a * x - p_c * z - p_d) / (p_b)
|
||||
return y
|
||||
|
||||
|
||||
def compare_normals(n1, n2):
|
||||
"""Compares if two normals are equal or opposite. Takes into account a small tolerance to overcome floating point errors."""
|
||||
tolerance = 10e-2
|
||||
# -- Assume equal and prove otherwise
|
||||
equal = True
|
||||
# -- i
|
||||
if math.fabs(n1[0] - n2[0]) > tolerance:
|
||||
equal = False
|
||||
# -- j
|
||||
elif math.fabs(n1[1] - n2[1]) > tolerance:
|
||||
equal = False
|
||||
# -- k
|
||||
elif math.fabs(n1[2] - n2[2]) > tolerance:
|
||||
equal = False
|
||||
return equal
|
||||
|
||||
|
||||
def reverse_vertices(vertices):
|
||||
"""Reverse vertices. Useful to reorient the normal of the polygon."""
|
||||
reversed_vertices = []
|
||||
nv = len(vertices)
|
||||
for i in range(nv - 1, -1, -1):
|
||||
reversed_vertices.append(vertices[i])
|
||||
return reversed_vertices
|
||||
|
||||
# Added by Th_FR, inspirde by https://pythonseminar.de/prufen-ob-die-liste-doppelte-elemente-enthalt-in-python/
|
||||
def has_duplicates(seq):
|
||||
seen = []
|
||||
unique_list = [x for x in seq if x not in seen and not seen.append(x)]
|
||||
return len(seq) != len(unique_list)
|
||||
# End of changes
|
||||
|
||||
# Added by Th_Fr
|
||||
def weighted_centroid(vertices):
|
||||
"""
|
||||
Calculate the weighted centroid of a polygon defined by vertices.
|
||||
|
||||
Arguments:
|
||||
vertices (numpy array): Array of vertices of the polygon.
|
||||
|
||||
Returns:
|
||||
numpy array: Weighted centroid [x, y, z].
|
||||
"""
|
||||
total_area = 0.0
|
||||
centroid = np.zeros(3)
|
||||
num_vertices = len(vertices)
|
||||
|
||||
for i in range(num_vertices):
|
||||
j = (i + 1) % num_vertices
|
||||
cross = np.cross(vertices[i], vertices[j])
|
||||
area = np.linalg.norm(cross)
|
||||
centroid += (vertices[i] + vertices[j]) * area
|
||||
total_area += area
|
||||
|
||||
|
||||
return centroid / (3 * total_area)
|
||||
|
||||
def calculate_polygon_normal_old(poly):
|
||||
"""
|
||||
Calculate the normal vector of a polygon using a weighted centroid and cross product approach.
|
||||
|
||||
Arguments:
|
||||
poly (list of lists): List of vertices of the polygon, where each vertex is [x, y, z].
|
||||
|
||||
Returns:
|
||||
numpy array: Normal vector (nx, ny, nz) of the polygon's plane.
|
||||
"""
|
||||
vertices = np.array(poly)
|
||||
num_vertices = len(vertices)
|
||||
|
||||
# Calculate weighted centroid
|
||||
#print("here b")
|
||||
centroid1 = centroid(vertices)
|
||||
|
||||
# Compute the normal vector using cross product of edges
|
||||
normal = np.zeros(3)
|
||||
for i in range(num_vertices):
|
||||
j = (i + 1) % num_vertices
|
||||
vi = vertices[i]
|
||||
vj = vertices[j]
|
||||
normal[0] += (vi[1] - centroid1[1]) * (vj[2] - centroid1[2]) - (vi[2] - centroid1[2]) * (vj[1] - centroid1[1])
|
||||
normal[1] += (vi[2] - centroid1[2]) * (vj[0] - centroid1[0]) - (vi[0] - centroid1[0]) * (vj[2] - centroid1[2])
|
||||
normal[2] += (vi[0] - centroid1[0]) * (vj[1] - centroid1[1]) - (vi[1] - centroid1[1]) * (vj[0] - centroid1[0])
|
||||
|
||||
norm = np.linalg.norm(normal)
|
||||
if norm != 0:
|
||||
normal /= norm
|
||||
else:
|
||||
normal = np.array([0.0, 0.0, 0.0])
|
||||
|
||||
return normal
|
||||
|
||||
|
||||
def calculate_polygon_normal(polygon):
|
||||
"""
|
||||
Calculate the surface normal of a polygon.
|
||||
|
||||
Parameters:
|
||||
polygon (list): A list of vertices, where each vertex is a list of three coordinates [x, y, z].
|
||||
|
||||
Returns:
|
||||
np.array: A normalized vector representing the surface normal.
|
||||
"""
|
||||
|
||||
normal = np.array([0.0, 0.0, 0.0])
|
||||
num_verts = len(polygon)
|
||||
|
||||
for i in range(num_verts):
|
||||
current = np.array(polygon[i])
|
||||
next_vert = np.array(polygon[(i + 1) % num_verts])
|
||||
|
||||
normal[0] += (current[1] - next_vert[1]) * (current[2] + next_vert[2])
|
||||
normal[1] += (current[2] - next_vert[2]) * (current[0] + next_vert[0])
|
||||
normal[2] += (current[0] - next_vert[0]) * (current[1] + next_vert[1])
|
||||
|
||||
normal = normalize(normal)
|
||||
return normal
|
||||
|
||||
|
||||
def normalize(vector):
|
||||
"""
|
||||
Normalize a vector.
|
||||
|
||||
Parameters:
|
||||
vector (np.array): A vector to normalize.
|
||||
|
||||
Returns:
|
||||
np.array: A normalized vector.
|
||||
"""
|
||||
norm = np.linalg.norm(vector)
|
||||
if norm == 0:
|
||||
return vector
|
||||
return vector / norm
|
||||
|
||||
|
||||
def triangulation(e, i):
|
||||
"""Triangulate the polygon with the exterior and interior list of points. Works only for convex polygons.
|
||||
Assumes planarity. Projects to a 2D plane and goes back to 3D."""
|
||||
vertices = []
|
||||
holes = []
|
||||
segments = []
|
||||
index_point = 0
|
||||
# -- Slope computation points
|
||||
a = [[], [], []]
|
||||
b = [[], [], []]
|
||||
for ip in range(len(e) - 1):
|
||||
vertices.append(e[ip])
|
||||
if a == [[], [], []] and index_point == 0:
|
||||
a = [e[ip][0], e[ip][1], e[ip][2]]
|
||||
if index_point > 0 and (e[ip] != e[ip - 1]):
|
||||
if b == [[], [], []]:
|
||||
b = [e[ip][0], e[ip][1], e[ip][2]]
|
||||
if ip == len(e) - 2:
|
||||
segments.append([index_point, 0])
|
||||
else:
|
||||
segments.append([index_point, index_point + 1])
|
||||
index_point += 1
|
||||
|
||||
for hole in i:
|
||||
first_point_in_hole = index_point
|
||||
for p in range(len(hole) - 1):
|
||||
if p == len(hole) - 2:
|
||||
segments.append([index_point, first_point_in_hole])
|
||||
else:
|
||||
segments.append([index_point, index_point + 1])
|
||||
index_point += 1
|
||||
vertices.append(hole[p])
|
||||
# -- A more robust way to get the point inside the hole, should work for non-convex interior polygons
|
||||
# alt: holes.append(point_inside(hole[:-1]))
|
||||
# -- Alternative, use centroid
|
||||
holes.append(centroid(hole[:-1])) # This should be useful!
|
||||
# -- Project to 2D since the triangulation cannot be done in 3D with the library that is used
|
||||
npolypoints = len(vertices)
|
||||
nholes = len(holes)
|
||||
# -- Check if the polygon is vertical, i.e. a projection cannot be made.
|
||||
# -- First copy the list so the originals are not modified
|
||||
temppolypoints = copy.deepcopy(vertices)
|
||||
newpolypoints = copy.deepcopy(vertices)
|
||||
tempholes = copy.deepcopy(holes)
|
||||
newholes = copy.deepcopy(holes)
|
||||
|
||||
# -- Compute the normal of the polygon for detecting vertical polygons and
|
||||
# -- for the correct orientation of the new triangulated faces
|
||||
# -- If the polygon is vertical
|
||||
#normal = unit_normal(temppolypoints[0], temppolypoints[1], temppolypoints[2])
|
||||
normal = calculate_polygon_normal(temppolypoints)
|
||||
|
||||
if math.fabs(normal[2]) < 10e-2:
|
||||
vertical = True
|
||||
#print("The polygon is vertical")
|
||||
#print("math.fabs(normal[2]): ", math.fabs(normal[2]))
|
||||
else:
|
||||
vertical = False
|
||||
#print("Not vertical")
|
||||
# -- We want to project the vertical polygon to the XZ plane
|
||||
# -- If a polygon is parallel with the YZ plane that will not be possible
|
||||
|
||||
|
||||
YZ = True
|
||||
for i in range(1, npolypoints):
|
||||
if temppolypoints[i][0] != temppolypoints[0][0]:
|
||||
YZ = False
|
||||
continue
|
||||
|
||||
|
||||
# -- Project the plane in the special case
|
||||
if YZ == True:
|
||||
for i in range(0, npolypoints):
|
||||
newpolypoints[i][0] = temppolypoints[i][1]
|
||||
newpolypoints[i][1] = temppolypoints[i][2]
|
||||
for i in range(0, nholes):
|
||||
newholes[i][0] = tempholes[i][1]
|
||||
newholes[i][1] = tempholes[i][2]
|
||||
# -- Project the plane
|
||||
elif vertical == True:
|
||||
for i in range(0, npolypoints):
|
||||
newpolypoints[i][1] = temppolypoints[i][2]
|
||||
for i in range(0, nholes):
|
||||
newholes[i][1] = tempholes[i][2]
|
||||
else:
|
||||
pass # -- No changes here
|
||||
|
||||
# -- Drop the last point (identical to first)
|
||||
for p in newpolypoints:
|
||||
# print("p: ", p)
|
||||
p.pop(-1)
|
||||
# print("p: ", p)
|
||||
# -- If there are no holes
|
||||
if len(newholes) == 0:
|
||||
newholes = None
|
||||
else:
|
||||
counter = 0
|
||||
for h in newholes:
|
||||
counter = counter + 1
|
||||
h = h.pop(-1)
|
||||
|
||||
# -- Plane information (assumes planarity) #todo: hier muss noch etwas angepasst werden; erledigt?
|
||||
a = e[0]
|
||||
b = e[1]
|
||||
c = e[2]
|
||||
# -- Construct the plane
|
||||
pl = planeAdjusted(e)
|
||||
|
||||
# -- Prepare the polygon to be triangulated
|
||||
# Change by Th_Fr: Distinguishing different cases!
|
||||
# There are two cases distinguished here: 1. A Polygon without holes, 2. A polygon with holes
|
||||
if newholes == None:
|
||||
poly = {'vertices': np.array(newpolypoints), 'segments': np.array(segments)}
|
||||
# For some reason this if.case sometimes fails, this is why there is a second version of the
|
||||
# Trinangulation without the optional 'pQjz' parameter
|
||||
if has_duplicates(newpolypoints) == False:
|
||||
t = triangle.triangulate(poly, 'pQjz')
|
||||
else:
|
||||
t = triangle.triangulate(poly)
|
||||
|
||||
else:
|
||||
poly = {'vertices': np.array(newpolypoints), 'segments': np.array(segments), 'holes': np.array(newholes)}
|
||||
t = triangle.triangulate(poly, 'pQjz')
|
||||
|
||||
# End of changes by Th_Fr
|
||||
|
||||
# -- Get the triangles and their vertices
|
||||
try:
|
||||
tris = t['triangles']
|
||||
except:
|
||||
print("strange error")
|
||||
tris = []
|
||||
|
||||
try:
|
||||
vert = t['vertices'].tolist()
|
||||
except:
|
||||
vert = []
|
||||
# -- Store the vertices of each triangle in a list
|
||||
tri_points = []
|
||||
for tri in tris:
|
||||
tri_points_tmp = []
|
||||
for v in tri.tolist():
|
||||
vert_adj = [[], [], []]
|
||||
if YZ:
|
||||
vert_adj[0] = temppolypoints[0][0]
|
||||
vert_adj[1] = vert[v][0]
|
||||
vert_adj[2] = vert[v][1]
|
||||
elif vertical:
|
||||
vert_adj[0] = vert[v][0]
|
||||
vert_adj[2] = vert[v][1]
|
||||
vert_adj[1] = get_y(pl, vert_adj[0], vert_adj[2])
|
||||
else:
|
||||
vert_adj[0] = vert[v][0]
|
||||
vert_adj[1] = vert[v][1]
|
||||
vert_adj[2] = get_height(pl, vert_adj[0], vert_adj[1])
|
||||
tri_points_tmp.append(vert_adj)
|
||||
try:
|
||||
tri_normal = unit_normal(tri_points_tmp[0], tri_points_tmp[1], tri_points_tmp[2])
|
||||
except:
|
||||
continue
|
||||
if compare_normals(normal, tri_normal):
|
||||
tri_points.append(tri_points_tmp)
|
||||
else:
|
||||
tri_points_tmp = reverse_vertices(tri_points_tmp)
|
||||
tri_points.append(tri_points_tmp)
|
||||
return tri_points
|
||||
73
scripts/CityGML2OBJv2/requirements.txt
Normal file
73
scripts/CityGML2OBJv2/requirements.txt
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
asttokens==3.0.1
|
||||
attrs==25.4.0
|
||||
blinker==1.9.0
|
||||
certifi==2026.1.4
|
||||
charset-normalizer==3.4.4
|
||||
click==8.3.1
|
||||
colorama==0.4.6
|
||||
comm==0.2.3
|
||||
ConfigArgParse==1.7.1
|
||||
contourpy==1.3.2
|
||||
cycler==0.12.1
|
||||
dash==4.0.0
|
||||
dash-core-components==2.0.0
|
||||
dash-html-components==2.0.0
|
||||
dash-table==5.0.0
|
||||
decorator==5.2.1
|
||||
exceptiongroup==1.3.1
|
||||
executing==2.2.1
|
||||
fastjsonschema==2.21.2
|
||||
Flask==3.1.3
|
||||
fonttools==4.61.1
|
||||
idna==3.15
|
||||
importlib_metadata==8.7.1
|
||||
ipython==8.38.0
|
||||
ipywidgets==8.1.8
|
||||
itsdangerous==2.2.0
|
||||
jedi==0.19.2
|
||||
Jinja2==3.1.6
|
||||
joblib==1.5.3
|
||||
jsonschema==4.26.0
|
||||
jsonschema-specifications==2025.9.1
|
||||
jupyter_core==5.9.1
|
||||
jupyterlab_widgets==3.0.16
|
||||
kiwisolver==1.4.9
|
||||
lxml==6.1.0
|
||||
MarkupSafe==3.0.3
|
||||
matplotlib==3.10.8
|
||||
matplotlib-inline==0.2.1
|
||||
narwhals==2.16.0
|
||||
nbformat==5.10.4
|
||||
nest-asyncio==1.6.0
|
||||
numpy==2.2.6
|
||||
open3d==0.19.0
|
||||
packaging==26.0
|
||||
parso==0.8.6
|
||||
pillow==12.2.0
|
||||
platformdirs==4.9.2
|
||||
plotly==6.5.2
|
||||
prompt_toolkit==3.0.52
|
||||
pure_eval==0.2.3
|
||||
Pygments==2.20.0
|
||||
pyparsing==3.3.2
|
||||
python-dateutil==2.9.0.post0
|
||||
pywin32==311
|
||||
referencing==0.37.0
|
||||
requests==2.33.0
|
||||
retrying==1.4.2
|
||||
rpds-py==0.30.0
|
||||
scikit-learn==1.7.2
|
||||
scipy==1.15.3
|
||||
shapely==2.1.2
|
||||
six==1.17.0
|
||||
stack-data==0.6.3
|
||||
tenacity==9.1.4
|
||||
threadpoolctl==3.6.0
|
||||
traitlets==5.14.3
|
||||
triangle==20250106
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.7.0
|
||||
wcwidth==0.6.0
|
||||
Werkzeug==3.1.6
|
||||
widgetsnbextension==4.0.15
|
||||
zipp==3.23.0
|
||||
Loading…
Add table
Add a link
Reference in a new issue