Closed16

IFCの形状情報の秘密に迫る

kiyukakiyuka

秘密を暴くぞ!!!

そういうわけで細かく調べたいのでひとまずスクラップを作っておく。

IfcOpenShellとかIFC.jsを使えばいいんだけど、なんか知りたいよね?たぶん車輪の再開発をすることになると思うけど、でもやります。

kiyukakiyuka

自力でIFCファイルから3D形状を取得しようと思ったけど、IFCの形状データは三角形メッシュの形になっているわけではない。たとえば、IfcExtrudedAreaSolid は平面を押し出す形で定義されているし、IfcFacetedBrepは平面のみで構成された形状になっている。
つまり、取得した3D形状を表示させるには、それら三角形メッシュ以外の定義をもとにして表示させる必要があるのだけれども、多くの3D形状を表示させるライブラリは三角形メッシュの表示が前提になっている。

Web上で3D表示ができるthree.js, babylon.js のどちらも(おそらく)三角形メッシュしか表示の対応していない。
Pythonでメッシュを扱うライブラリに PyMeshtrimesh があるが、これらも三角形メッシュを扱うライブラリのようだ。

調べてみた結果、IFCの形状情報をごにょごにょできそうなのが pythonocc-core ぐらいしかなかった。IfcOpenShell で使用しているライブラリ。

kiyukakiyuka

web-ifc は独自実装で三角形メッシュに変換しているっぽい?
ぱっと見だと GLM くらいしか外部ライブラリを使っていなさそうだった。

kiyukakiyuka

jsだとOpenCascade.js があるみたい。

どちらにせよ Open CASCADE Technology 以外でメッシュ以外の3D形状を扱えるライブラリがほぼないっぽい?
ちゃんと調べてないけどCADソフトなんかでも使われていてデファクト?っぽいのでひとまずPythonで使えるようにする。

kiyukakiyuka

宗教上の理由?でcondaは使えないのでpythonocc-coreをWSL2でビルドする。

基本的に公式の手順通り進めればいいんだけど一部間違っている?ので実行したコマンドをメモ。
インストール手順

# 作業フォルダ
mkdir ~/build-occ
cd ~/build-occ/

必要なライブラリインストール

sudo apt-get update
sudo apt-get install -y wget libglu1-mesa-dev libgl1-mesa-dev libxmu-dev libxi-dev build-essential cmake libfreetype6-dev tk-dev python3-dev rapidjson-dev python3 git python3-pip libpcre2-dev

swig をビルド

wget http://prdownloads.sourceforge.net/swig/swig-4.1.1.tar.gz
tar -zxvf swig-4.1.1.tar.gz
cd swig-4.1.1
./configure
make -j4
# sudo が必要
sudo make install

OpenCascade のビルド

cd ~/build-occ/

# ファイル名指定は「-O(大文字)」で、URLたぶんクオートする必要あり
wget -O occt-7.7.2.tgz "https://git.dev.opencascade.org/gitweb/?p=occt.git;a=snapshot;h=cec1ecd0c9f3b3d2572c47035d11949e8dfa85e2;sf=tgz"
tar -zxvf occt-7.7.2.tgz

cd occt-cec1ecd
mkdir cmake-build
cd cmake-build

cmake -DINSTALL_DIR=/opt/build/occt772 -DBUILD_RELEASE_DISABLE_EXCEPTIONS=OFF ..
make -j4
sudo make install

# ルート権限が必要
sudo sh -c 'echo "/opt/build/occt772/lib" >> /etc/ld.so.conf.d/occt.conf'

pythonの仮想環境作成(公式手順書にはない)

mkdir ~/envs
cd ~/envs

# バージョンは環境に合わせて入れる
sudo apt install python3.8-venv
python3 -m venv occ
source occ/bin/activate

pip install svgwrite numpy matplotlib
# PyQt5 は失敗したのでスキップ
# pip install PyQt5

# pythonocc-demos を動かすなら以下も必要
pip install scipy opencv-python pythreejs

pythonocc のビルド

cd ~/build-occ/

git clone https://github.com/tpaviot/pythonocc-core.git
cd pythonocc-core
mkdir cmake-build && cd cmake-build

# INSTALL_DIRECTORY は Python仮想環境を指定する
cmake \
 -DOCCT_INCLUDE_DIR=/opt/build/occt772/include/opencascade \
 -DOCCT_LIBRARY_DIR=/opt/build/occt772/lib \
 -DPYTHONOCC_BUILD_TYPE=Release \
 -DPYTHONOCC_INSTALL_DIRECTORY=$HOME/envs/occ/lib/python3.8/site-packages/OCC \
 -DPYTHONOCC_MESHDS_NUMPY=ON \
  ..

make -j4
# sudo が必要
sudo make install 

# これは `.bashrc` に追記する
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/build/occt772/lib
kiyukakiyuka

ひとまず動いたのでIFCの形状を作って表示させたい

kiyukakiyuka

表示はこれで

from OCC.Display.WebGl.jupyter_renderer import JupyterRenderer

shape = BRepPrimAPI_MakeBox(10, 20, 30).Shape()

my_renderer = JupyterRenderer()
my_renderer.DisplayShape(
    shape,
    render_edges=True,
    topo_level="Face",
    shape_color="#abdda4",
    update=True,
)
kiyukakiyuka

形状を取得していく。データはIfcOpenShellのサンプルファイルを使う。
https://blenderbim.org/docs-python/ifcopenshell-python/hello_world.html

まずは簡単そうな壁を。データを見てみるとこんな感じ。
#15006IfcExtrudedAreaSolid から右が形状の定義。

IfcExtrudedAreaSolid は平面を押し出すことによって作られる形状で SweptArea が平面の定義、Position が位置、ExtrudedDirection が押し出す方向、Depth が押し出す長さ。

SweptAreaはこの場合だと、

  • 複数の頂点(4点)からなる多角形で定義されていて、
  • 押し出し方向は(画像では見えてないけど)Z方向 (0, 0, 1)
  • Depth は 2.5 (見えてないけど)

という形状になっている。ひとまず形状だけみたいので位置はおいておく。

そういうわけでこれを pythonocc で表示してみる。

kiyukakiyuka

ひとまずゴリ押しで値を取得

import ifcopenshell
ifc = ifcopenshell.open('model/AC20-FZK-Haus.ifc')
element = ifc.by_id(15042)
representations = element.Representation.Representations
for item in representations[0].Items:
    if item.is_a() == 'IfcExtrudedAreaSolid':
        depth = item.Depth 
        direction = item.ExtrudedDirection.DirectionRatios
        swept_area = item.SweptArea
        if swept_area.is_a() == 'IfcArbitraryClosedProfileDef':
            outer_curve = swept_area.OuterCurve
            if outer_curve.is_a() == 'IfcPolyline':
                points = outer_curve.Points
                points = [point.Coordinates for point in points]
            else:
                print('not IfcPolyline !!')
        else:
            print('not IfcArbitraryClosedProfileDef !!')
    else:
        print('not IfcExtrudedAreaSolid !!')

データからoccで形状作成

from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeEdge
from OCC.Core.gp import gp_Pnt, gp_Vec, gp_Dir
from OCC.Extend.ShapeFactory import make_wire, make_face, make_extrusion

def create_extrude_shape(points, direction, length):
    gp_points = [gp_Pnt(*p, 0) for p in points]
    edges = []
    for i in range(len(gp_points) - 1):
        edge = BRepBuilderAPI_MakeEdge(gp_points[i], gp_points[i+1]).Edge()
        edges.append(edge)

    wire = make_wire(edges)
    face = make_face(wire)
    extrusion = make_extrusion(face, length, gp_Vec(*direction))
    return extrusion

shape = create_extrude_shape(points, direction, depth)

結果。できてる。

kiyukakiyuka

次は正しい位置に置きたい。位置は IfcLocalPlacement が連なっているような感じ。
この場合は、#14983 の位置は #477 の相対位置、#477 の位置は #432 の相対位置、 ... というような指定がされている。

これはIfcSiteの位置があり、その位置を基準にしてIfcBuildingの位置が定義され、さらにIfcBuildingの位置を基準に IfcBuildingStoreyの位置があり、最後に壁の相対位置が設定されているという状態。

kiyukakiyuka

位置を定義しているIfcAxis2Placement3Dの定義はちょっと特殊。
Locationは位置、AxisはZ軸の方向、RefDirectionはX軸の方向となっている。
物体の位置指定をするために、そこから変換行列を計算して、角度と移動量を取得する。

取得するコード。だいたいChatGPT。

def calculate_transformation_matrix(location, axis, ref_direction):
    # Z軸(Axis)の単位ベクトルを計算
    z_axis = np.array(axis) / np.linalg.norm(axis)
    
    # X軸(RefDirection)の単位ベクトルを計算
    x_axis = np.array(ref_direction) / np.linalg.norm(ref_direction)
    
    # Y軸はZ軸とX軸の外積で計算
    y_axis = np.cross(z_axis, x_axis)
    
    # 回転行列を構築
    rotation_matrix = np.array([x_axis, y_axis, z_axis]).T
    
    # 変換行列を4x4の形で作成
    transformation_matrix = np.eye(4)
    transformation_matrix[:3, :3] = rotation_matrix
    transformation_matrix[:3, 3] = location  # 平行移動ベクトルの追加
    
    return transformation_matrix

def matrix_to_translation_angle(matrix):
    # 移動量の抽出
    translation = matrix[:3, 3]

    # 角度の計算
    sy = np.linalg.norm(matrix[:3, 0])
    singular = sy < 1e-6
    if not singular:
        x = np.arctan2(matrix[2, 1], matrix[2, 2])
        y = np.arctan2(-matrix[2, 0], sy)
        z = np.arctan2(matrix[1, 0], matrix[0, 0])
    else:
        x = np.arctan2(-matrix[1, 2], matrix[1, 1])
        y = np.arctan2(-matrix[2, 0], sy)
        z = 0

    return translation, np.array([x, y, z])
kiyukakiyuka

変換できているか確認のために壁表示

整理したりなんやかんやしたコード
from OCC.Extend.ShapeFactory import translate_shp, rotate_shp_3_axis
from OCC.Extend.ShapeFactory import make_wire, make_face, make_extrusion
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakePolygon

def get_polyline(item):
    if item.is_a() == 'IfcPolyline':
        points = item.Points
        points = [point.Coordinates for point in points]
        return points
    else:
        print('not IfcPolyline !!')
        
def extract_ifc_extruded_shape(item):
    if item.is_a() == 'IfcExtrudedAreaSolid':
        depth = item.Depth 
        direction = item.ExtrudedDirection.DirectionRatios
        swept_area = item.SweptArea
        position = item.Position
        if swept_area.is_a() == 'IfcArbitraryClosedProfileDef':
            outer_curve = swept_area.OuterCurve
            if outer_curve.is_a() == 'IfcPolyline':
                points = get_polyline(outer_curve)
                return points, direction, depth
            else:
                print('not IfcPolyline !!')
        else:
            print('not IfcArbitraryClosedProfileDef !!')
    else:
        print('not IfcExtrudedAreaSolid !!')

def points2face(points, is_close=True):
    gp_points = [(gp_Pnt(*p) if len(p) == 3 else gp_Pnt(*p, 0)) for p in points]
    polygon = BRepBuilderAPI_MakePolygon()
    for p in gp_points:
        polygon.Add(p)

    # 必要であれば始点と終点をつなぐ
    if is_close:
        polygon.Close()

    face = BRepBuilderAPI_MakeFace(polygon.Wire()).Face()
    return face

def create_extrude_shape(points, direction, length):
    face = points2face(points, is_close=False)
    extrusion = make_extrusion(face, length, gp_Vec(*direction))
    return extrusion

shapes = []
walls = ifc.by_type('IfcWall')
for element in walls:
    placement = element.ObjectPlacement
    transformation_matrix = placement_to_transformation_matrix(placement)
    tr, angle = matrix_to_translation_angle(transformation_matrix)
    
    representations = element.Representation.Representations
    for item in representations[0].Items:
        ret = extract_ifc_extruded_shape(item)
        if ret is not None:
            points, direction, depth = ret
            shape = create_extrude_shape(*ret)
            rotated_shape = rotate_shp_3_axis(shape, *angle, "rad")
            trans_shape = translate_shp(rotated_shape, gp_Vec(*tr))
            shapes.append(trans_shape)

開口部ないし、2階部分の壁はIfcBooleanClippingResultだから表示できてないけど、できているということで。

kiyukakiyuka

IfcBooleanClippingResult は2の形状の足したり引いたりした形状。

この場合、IfcExtrudedAreaSolid から IfcHalfSpaceSolid を引いている。
(見えてないけど OperatorDIFFERENCE
IfcHalfSpaceSolidは平面(位置、法線)で2つに区切られた空間。

kiyukakiyuka

とりあえず壁に穴を開ける

だいぶIfcOpenShellのC++のコードを参考にした。IfcOpenShell/src/ifcgeom

コード

実行部分

shapes = []
walls = ifc.by_type('IfcWall')
for element in walls:
    placement = element.ObjectPlacement
    transformation = placement_to_transformation(placement)
    
    representations = element.Representation.Representations
    for item in representations[0].Items:
        if item.is_a() == 'IfcExtrudedAreaSolid':
            shape = ifc_extruded_area_solid_to_shape(item)
            trans_shape = BRepBuilderAPI_Transform(shape, transformation).Shape()
            opening_shapes = get_opening_shape(element)
            for opening_shape in opening_shapes:
                trans_shape = BRepAlgoAPI_Cut(trans_shape, opening_shape).Shape()
                
            shapes.append(trans_shape)
        else:
            print(item.id(), item.is_a())

IFCからOCC

# IfcExtrudedAreaSolid から押し出し形状
def ifc_extruded_area_solid_to_shape(item):
    ret = extract_ifc_extruded_shape(item)
    if ret is not None:
        points, direction, depth, position = ret
        transformation = placement3d_to_transformation(position)
        shape = create_extrude_shape(points, direction, depth)
        trans_shape = BRepBuilderAPI_Transform(shape, transformation).Shape()
        return trans_shape

# IfcOpeningElement の形状作成
def get_opening_shape(element):
    has_openings = element.HasOpenings
    opening_shapes = []
    for opening in has_openings:
        placement = opening.RelatedOpeningElement.ObjectPlacement
        transformation = placement_to_transformation(placement)
    
        items = opening.RelatedOpeningElement.Representation.Representations[0].Items
        for item in items:
            shape = ifc_extruded_area_solid_to_shape(item)
            trans_shape = BRepBuilderAPI_Transform(shape, transformation).Shape()
            opening_shapes.append(trans_shape)

    return opening_shapes

# IfcLocalPlacement を 変換行列に
def placement_to_transformation(placement):
    transform = placement.RelativePlacement
    transformation = placement3d_to_transformation(transform)

    rel = placement.PlacementRelTo
    if rel is not None:
        transformation.PreMultiply(placement_to_transformation(rel))

    return transformation

# IfcAxis2Placement3D を 変換行列に
def placement3d_to_transformation(item):
    location, axis, direction = extract_ifc_placement3d(item)
    ax3 = gp_Ax3(gp_Pnt(*location), gp_Dir(*axis), gp_Dir(*direction))
    ax2 = gp.gp.XOY()
    xoy = gp_Ax3(ax2.Location(), ax2.Direction(), ax2.XDirection())

    transformation = gp_Trsf()
    transformation.SetTransformation(ax3, xoy)
    return transformation

OCC

# 点から面
def points2face(points, is_close=True):
    gp_points = [(gp_Pnt(*p) if len(p) == 3 else gp_Pnt(*p, 0)) for p in points]
    polygon = BRepBuilderAPI_MakePolygon()
    for p in gp_points:
        polygon.Add(p)

    # 必要であれば始点と終点をつなぐ
    if is_close:
        polygon.Close()

    face = BRepBuilderAPI_MakeFace(polygon.Wire()).Face()
    return face

# 押し出し形状
def create_extrude_shape(points, direction, length):
    face = points2face(points, is_close=True)
    extrusion = make_extrusion(face, length, gp_Vec(*direction))
    return extrusion

IFCから値取得

# IfcAxis2Placement3D から値取得
def extract_ifc_placement3d(item):
    return (
        item.Location.Coordinates,
        item.Axis.DirectionRatios,
        item.RefDirection.DirectionRatios,
    )

# IfcExtrudedAreaSolid から値取得
def extract_ifc_extruded_shape(item):
    if item.is_a() == 'IfcExtrudedAreaSolid':
        depth = item.Depth
        direction = item.ExtrudedDirection.DirectionRatios
        position = item.Position

        swept_area = item.SweptArea
        if swept_area.is_a() == 'IfcArbitraryClosedProfileDef':
            points = extract_ifc_arbitrary_closed(swept_area)
            if points is None:
                return
        elif swept_area.is_a() == 'IfcRectangleProfileDef':
            points = extract_ifc_rectangle(swept_area)
        else:
            print_not_defined(item)

        return points, direction, depth, position

    else:
        print_not_defined(item)

# IfcArbitraryClosedProfileDef から値取得
def extract_ifc_arbitrary_closed(item):
    outer_curve = item.OuterCurve
    if outer_curve.is_a() == 'IfcPolyline':
        points = get_polyline(outer_curve)
        return points
    else:
        print_not_defined(item)

# IfcRectangleProfileDef から値取得
def extract_ifc_rectangle(item):
    x, y = item.XDim / 2, item.YDim / 2
    points = [
        [-x, -y],
        [x, -y],
        [x, y],
        [-x, y],
    ]
    return points


# IfcPolyline から値取得
def get_polyline(item):
    if item.is_a() == 'IfcPolyline':
        points = item.Points
        points = [point.Coordinates for point in points]
        return points
    else:
        print_not_defined(item)

# 表示用
def print_not_defined(item):
    print(item.id(), item.is_a())
kiyukakiyuka

いくつか形状作ってみた感触、ドキュメントとIfcOpenShellのコード(IfcOpenShell/src /ifcgeom/ のエンティティ名のコード)を見れば、occで一通り形状を作ることは難しくなさそう。ドキュメントだけじゃよくわかんないし、ソースコードも一緒に見て自分で実装し直してみると理解できたりもする。

そんなわけである程度理解できたので、とりあえず一区切りということで一旦Close。
気が向いたらモデル全部表示させるくらいは試すかも?

このスクラップは2ヶ月前にクローズされました