CSV-Export: Nachbarschaftserkennung ueber Bounding-Box-Ueberschneidung (STRtree/shapely)
Neue Spalte "Nachbarn" (kommaseparierte TeileId-Liste) in export_csv.py: zwei Elemente gelten als benachbart, wenn ihre Bounding-Boxes (x/y-Ebene) sich ueberschneiden. Toleranz konfigurierbar in mm ueber cfg/export.cfg [Nachbarschaft] -> toleranz_mm, siehe lib/export_neighbors.py. Nur EXPORTCSV betroffen, EXPORTSIVAS bleibt unveraendert. shapely als neue Abhaengigkeit in requirements.txt ergaenzt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+18
-5
@@ -11,7 +11,7 @@ Aufruf:
|
||||
python export_csv.py <export_raw.json> <data_dir> <output.csv>
|
||||
|
||||
CSV-Format:
|
||||
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Merkmale
|
||||
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Merkmale
|
||||
|
||||
Position und Boundingbox stammen aus der von export.lsp (csv:get-bbox, per
|
||||
vla-getboundingbox) ermittelten Bounding-Box je Block:
|
||||
@@ -19,8 +19,11 @@ vla-getboundingbox) ermittelten Bounding-Box je Block:
|
||||
Boundingbox = Ausdehnung (Laenge, Breite, Hoehe) in x, y, z
|
||||
Planquadrat wird aus der x/y-Koordinate des Blocks berechnet, siehe
|
||||
export_planquadrat.py ([Planquadrate] in cfg/export.cfg).
|
||||
Position, Boundingbox und Planquadrat sind nur fuer EXPORTCSV vorhanden
|
||||
(nicht EXPORTSIVAS) - siehe csv:run-export.
|
||||
Nachbarn = kommaseparierte TeileId-Liste ueberschneidender Elemente (Bounding-
|
||||
Box-Ueberschneidung in der x/y-Ebene per shapely-STRtree, Toleranz konfigurierbar
|
||||
ueber [Nachbarschaft] -> toleranz_mm in cfg/export.cfg), siehe export_neighbors.py.
|
||||
Position, Boundingbox, Planquadrat und Nachbarn sind nur fuer EXPORTCSV
|
||||
vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -29,9 +32,11 @@ import os
|
||||
|
||||
from export_blockpatterns import load_patterns, matches_any
|
||||
from export_planquadrat import load_planquadrat_config, compute_planquadrat
|
||||
from export_neighbors import load_neighbor_tolerance_mm, compute_neighbor_ids
|
||||
|
||||
BLOCKPATTERNS = load_patterns()
|
||||
PLANQUADRAT_CFG = load_planquadrat_config()
|
||||
NEIGHBOR_TOLERANCE_MM = load_neighbor_tolerance_mm()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -240,13 +245,16 @@ def bbox_columns(block):
|
||||
Rueckgabe: dict mit "position" (Mittenkoordinate x,y,z) und "boundingbox"
|
||||
(Ausdehnung x,y,z) als Strings, oder leere Strings falls kein bbox-Feld
|
||||
vorhanden ist (z.B. EXPORTSIVAS oder die synthetische Omniflo-Sum-Zeile).
|
||||
"_bbox" enthaelt zusaetzlich die rohen Zahlenwerte fuer die Nachbarschafts-
|
||||
erkennung (siehe export_neighbors.py), wird nicht in die CSV geschrieben.
|
||||
"""
|
||||
bbox = block.get("bbox")
|
||||
if not bbox:
|
||||
return {"position": "", "boundingbox": ""}
|
||||
return {"position": "", "boundingbox": "", "_bbox": None}
|
||||
return {
|
||||
"position": f'{bbox.get("cx", 0):.2f}, {bbox.get("cy", 0):.2f}, {bbox.get("cz", 0):.2f}',
|
||||
"boundingbox": f'{bbox.get("dx", 0):.2f}, {bbox.get("dy", 0):.2f}, {bbox.get("dz", 0):.2f}',
|
||||
"_bbox": bbox,
|
||||
}
|
||||
|
||||
|
||||
@@ -452,6 +460,10 @@ def process_blocks(blocks, lookup):
|
||||
"boundingbox": "",
|
||||
})
|
||||
|
||||
neighbor_ids = compute_neighbor_ids(items, NEIGHBOR_TOLERANCE_MM)
|
||||
for item, nachbarn in zip(items, neighbor_ids):
|
||||
item["nachbarn"] = nachbarn
|
||||
|
||||
return items
|
||||
|
||||
|
||||
@@ -470,6 +482,7 @@ def format_csv_line(item):
|
||||
f';1'
|
||||
f';"{item["position"]}"'
|
||||
f';"{item["boundingbox"]}"'
|
||||
f';"{item["nachbarn"]}"'
|
||||
f';{merkmale_json}'
|
||||
)
|
||||
|
||||
@@ -501,7 +514,7 @@ def main():
|
||||
|
||||
items = process_blocks(blocks, lookup)
|
||||
|
||||
header = "Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Merkmale"
|
||||
header = "Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Merkmale"
|
||||
with open(output_csv, "w", encoding="utf-8") as f:
|
||||
f.write(header + "\n")
|
||||
for item in items:
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
export_neighbors.py - Ermittelt benachbarte Elemente ueber die Bounding-Box (STRtree).
|
||||
|
||||
Zwei Elemente gelten als benachbart, wenn ihre Bounding-Boxes sich in der
|
||||
x/y-Ebene (Grundriss, Z/Hoehe wird ignoriert) ueberschneiden. Um auch nah
|
||||
beieinander liegende, aber nicht direkt beruehrende Elemente (z.B. Kreisel)
|
||||
als benachbart zu erkennen, wird jede Bounding-Box vor der Pruefung um die
|
||||
Haelfte der konfigurierten Toleranz nach allen Seiten erweitert - zwei
|
||||
Elemente mit einem Abstand <= toleranz_mm gelten dann ebenfalls als
|
||||
benachbart. Die Toleranz wird aus cfg/export.cfg ([Nachbarschaft] ->
|
||||
toleranz_mm) gelesen.
|
||||
|
||||
Nur von export_csv.py genutzt (EXPORTCSV), nicht von export_sivas.py.
|
||||
"""
|
||||
|
||||
import configparser
|
||||
|
||||
from shapely.geometry import box
|
||||
from shapely.strtree import STRtree
|
||||
|
||||
from export_blockpatterns import cfg_path_from_env
|
||||
|
||||
|
||||
def load_neighbor_tolerance_mm(cfg_path=None):
|
||||
"""Liest die Nachbarschafts-Toleranz (mm) aus export.cfg. Default: 0."""
|
||||
if cfg_path is None:
|
||||
cfg_path = cfg_path_from_env()
|
||||
parser = configparser.ConfigParser(inline_comment_prefixes=("#",))
|
||||
parser.read(cfg_path, encoding="utf-8")
|
||||
return parser.getfloat("Nachbarschaft", "toleranz_mm", fallback=0.0)
|
||||
|
||||
|
||||
def compute_neighbor_ids(items, tolerance_mm):
|
||||
"""Ermittelt je Item die IDs (item["teileid"]) ueberschneidender Nachbarn.
|
||||
|
||||
items = Liste von dict mit "teileid" und optional "_bbox"
|
||||
({"cx","cy","dx","dy",...}, siehe export.lsp csv:get-bbox / export_csv.py
|
||||
bbox_columns). Items ohne _bbox (z.B. die synthetische Omniflo-Sum-Zeile)
|
||||
haben keine Nachbarn.
|
||||
|
||||
Rueckgabe: Liste von kommaseparierten Nachbar-ID-Strings, positionsgleich
|
||||
zu items (nicht ueber die ID dedupliziert, da TeileId nicht zwingend
|
||||
eindeutig ist).
|
||||
"""
|
||||
idx_with_bbox = [i for i, it in enumerate(items) if it.get("_bbox")]
|
||||
result = [""] * len(items)
|
||||
if len(idx_with_bbox) < 2:
|
||||
return result
|
||||
|
||||
half_tol = tolerance_mm / 2.0
|
||||
geoms = []
|
||||
for i in idx_with_bbox:
|
||||
bb = items[i]["_bbox"]
|
||||
minx = bb["cx"] - bb["dx"] / 2.0 - half_tol
|
||||
maxx = bb["cx"] + bb["dx"] / 2.0 + half_tol
|
||||
miny = bb["cy"] - bb["dy"] / 2.0 - half_tol
|
||||
maxy = bb["cy"] + bb["dy"] / 2.0 + half_tol
|
||||
geoms.append(box(minx, miny, maxx, maxy))
|
||||
|
||||
tree = STRtree(geoms)
|
||||
for local_i, global_i in enumerate(idx_with_bbox):
|
||||
hits = tree.query(geoms[local_i], predicate="intersects")
|
||||
neighbor_ids = [
|
||||
items[idx_with_bbox[int(local_j)]]["teileid"]
|
||||
for local_j in hits
|
||||
if int(local_j) != local_i
|
||||
]
|
||||
result[global_i] = ", ".join(neighbor_ids)
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user