Deep Learning

외부 CAD Data로 SketchGraphs 데이터셋 생성 with python (2)

ju_young 2023. 6. 11. 07:45
728x90

Previously

외부 CAD Data로 SketchGraphs 데이터셋 생성 with python (1) 에서는 FreeCAD를 사용하여 dxf에서 데이터를 뽑는 방법을 알아냈다. 이제 SketchGraphs데이터로 만들어봐야한다.


SketchGraphs

우선 SketchGraphs의 sketch의 sequence는 NodeOp, EdgeOp라는 객체(class)로 이루어져있다.

class NodeOp(typing.NamedTuple):
    """This class represents a node (or entity) operation in a construction sequence.

    An entity is specified by a label, along with a dictionary of parameters.
    """
    label: EntityType
    parameters: dict = {}


class EdgeOp(typing.NamedTuple):
    """This class represents an edge (or constraint) operation in a construction sequence.

    An edge is specified by a label, a variable number of targets, and a dictionary of parameters.
    Targets for an edge are nodes, which are referred to using their index in the construction sequence.
    """
    label: ConstraintType
    references: typing.Tuple[int, ...]
    parameters: dict = {}

주석에도 자세히 젹혀있지만 간단하게 정리하면 각각 다음과 같다.

  • NodeOp: Line, Circle 등의 Entity와 Entity의 Start, End Point
  • EdgeOp: NodeOp을 연결해주는 Node (sequence의 index를 사용)

1. EntityType

그리고 Entity의 종류는 다음과 같이 10가지가 있다.

class EntityType(enum.IntEnum):
    """Enumeration indicating the type of entity represented.
    """
    Point = 0 # x, y
    Line = 1 # 'dirX', 'dirY', 'pntX', 'pntY', 'startParam', 'endParam'
    Circle = 2 # 'xCenter', 'yCenter', 'xDir', 'yDir', 'radius'
    Ellipse = 3 # 'xCenter', 'yCenter', 'xDir', 'yDir', 'radius', 'minorRadius'
    Spline = 4 # degree, isPeriodic, isRational, controlPoints, knots, startParam, endParam
    Conic = 5 # ??
    Arc = 6 # 'xCenter', 'yCenter', 'xDir', 'yDir', 'radius', 'startParam', 'endParam'
    External = 7
    Stop = 8
    Unknown = 9

우선 Conic은 사용하지 않으니 무시하고 Enternal, Stop, Unknown이라는 이상한 Type이 보인다. 대충 sequence 들을 살펴보니 External은 sequence의 시작, Stop은 sequence의 끝을 의미하는 것 같다. Unknown은 모르겠다.

 

주석에는 각 Type 별로 사용되는 parameter들을 작성했다. 우선 지금은 Line만 사용할 것이기 때문에 Line을 살펴보겠다.

def __init__(self, entityId, isConstruction=False, pntX=0, pntY=0, dirX=1, dirY=0, startParam=-0.5, endParam=0.5):
    # TODO: address these default parameter values
    super(Line, self).__init__(entityId, isConstruction)
    self.dirX = dirX
    self.dirY = dirY
    self.pntX = pntX
    self.pntY = pntY
    self.startParam = startParam
    self.endParam = endParam

Line은 시작점과 끝점을 가지고 있는데 __init__ 에서는 pntX, pntY 밖에 없고 dirX, dirY와 startParam, endParam을 가지고 있다. 이것들이 무엇을 의미하는지는 찾을 수 없었지만 Line에는 start_point, end_point가 구현되어 있었다.

@property
def start_point(self):
    """Returns a tuple representing the start location of the line."""
    startX = self.pntX + self.startParam * self.dirX
    startY = self.pntY + self.startParam * self.dirY
    return np.array([startX, startY])

@property
def end_point(self):
    """Returns a tuple representing the end location of the line."""
    endX = self.pntX + self.endParam * self.dirX
    endY = self.pntY + self.endParam * self.dirY
    return np.array([endX, endY])

예를 들어서 __init__의 default 값대로 pntX=0, pntY=0, dirX=1, dirY=0, startParam=-0.5, endParam=0.5 라면 start_point (-0.5, 0), end_point(0.5, 0)이 된다. 그러면 자연스럽게 pntX, pntY는 중간점이라고 생각할 수 있고 dir * Param을 더해 시작점, 끝점을 계산할 수 있다고 생각할 수 있다. dir, Param를 정확하게 어떻게 나누는지는 모르겠지만 일단 넘어간다.

2. SubnodeType

class SubnodeType(enum.IntEnum):
    SN_Start = 101
    SN_End = 102
    SN_Center = 103

각각 Entity의 시작점, 끝점, 중간점을 의미한다.

3. ConstraintType

뭔가 종류가 많지만 지금은 Coincident만 사용한다. 참고로 Coincident는 점과 점이 일치하는 Constraint를 말한다.

class ConstraintType(enum.IntEnum):
    Coincident = 0
    Projected = 1
    Mirror = 2
    Distance = 3
    Horizontal = 4
    Parallel = 5
    Vertical = 6
    Tangent = 7
    Length = 8
    Perpendicular = 9
    Midpoint = 10
    Equal = 11
    Diameter = 12
    Offset = 13
    Radius = 14
    Concentric = 15
    Fix = 16
    Angle = 17
    Circular_Pattern = 18
    Pierce = 19
    Linear_Pattern = 20
    Centerline_Dimension = 21
    Intersected = 22
    Silhoutted = 23
    Quadrant = 24
    Normal = 25
    Minor_Diameter = 26
    Major_Diameter = 27
    Rho = 28
    Unknown = 29
    Subnode = 101

4. Sequence

실제로 validation dataset을 다운로드받아 하나의 sequence(삼각형)를 출력해보았다. (아래는 보기 편하게 NodeOp와 EdgeOp 순서대로 정렬했다.)

# NodeOp
0 => NodeOp(label=<EntityType.External: 7>, parameters={})

1 => NodeOp(label=<EntityType.Line: 1>, parameters={'isConstruction': False, 'dirX': -0.1587597160446314, 'dirY': -0.9873172502095909, 'pntX': -0.043413616673332614, 'pntY': 0.05090344377336632, 'startParam': -0.004782675797261136, 'endParam': 0.005217324202738868})
2 => NodeOp(label=<SubnodeType.SN_Start: 101>, parameters={})
3 => NodeOp(label=<SubnodeType.SN_End: 102>, parameters={})

4 => NodeOp(label=<EntityType.Line: 1>, parameters={'isConstruction': False, 'dirX': 0.7756619622537874, 'dirY': -0.6311485722970499, 'pntX': -0.03893091994646168, 'pntY': 0.052595767444200636, 'startParam': -0.004800287569272317, 'endParam': 0.005199712430727682})
5 => NodeOp(label=<SubnodeType.SN_Start: 101>, parameters={})
6 => NodeOp(label=<SubnodeType.SN_End: 102>, parameters={})

7 => NodeOp(label=<EntityType.Line: 1>, parameters={'isConstruction': False, 'dirX': -0.9344216782984186, 'dirY': -0.3561686779125405, 'pntX': -0.03844773754103034, 'pntY': 0.04796082722455852, 'startParam': -0.0037991806313900202, 'endParam': 0.0062008193686099835})
8 => NodeOp(label=<SubnodeType.SN_Start: 101>, parameters={})
9 => NodeOp(label=<SubnodeType.SN_End: 102>, parameters={})

10 => NodeOp(label=<EntityType.Stop: 8>, parameters={})

# EdgeOp
EdgeOp(label=<ConstraintType.Subnode: 101>, references=(2, 1), parameters={})
EdgeOp(label=<ConstraintType.Subnode: 101>, references=(3, 1), parameters={})
EdgeOp(label=<ConstraintType.Subnode: 101>, references=(5, 4), parameters={})
EdgeOp(label=<ConstraintType.Subnode: 101>, references=(6, 4), parameters={})
EdgeOp(label=<ConstraintType.Subnode: 101>, references=(8, 7), parameters={})
EdgeOp(label=<ConstraintType.Subnode: 101>, references=(9, 7), parameters={})

EdgeOp(label=<ConstraintType.Coincident: 0>, references=(5, 2), parameters={})
EdgeOp(label=<ConstraintType.Coincident: 0>, references=(8, 6), parameters={})
EdgeOp(label=<ConstraintType.Coincident: 0>, references=(9, 3), parameters={})

"[숫자] =>"에서 숫자는 index를 의미한다. 나중에서야알았는데 NodeOp만 index를 사용한다.

 

Subnode는 Entity와 Subnode(시작점, 끝점)의 constraint이고 Coincident는 Subnode(각 Entity의 끝점)들끼리의 constraint인 것을 확인할 수 있다.


FreeCAD

1. Load DXF

이전에 사용했던 삼각형 dxf 파일을 불러오는 것부터 다시 해보겠다.

import importDXF
doc = importDXF.open(dxf_file)

코드는 상당히 간단해보이지만 설정은 그렇지 않았다.

dxf를 불러오면 Document 객체가 return되는데 이 Documnet 안에 여러 Object들이 또 나누어져있다.

print(doc.Objects)
# [<App::FeaturePython object>, <group object>, <App::FeaturePython object>, <App::FeaturePython object>, <Part::PartFeature>, <Part::PartFeature>, <Part::PartFeature>]

2. Make Sketch

여기서 <Part::PartFeature>들을 sketch로 변환해야한다.

sketch = Draft.make_sketch(doc.Objects[4:])
# <Sketcher::SketchObject>

변환된 SketchObject는 constraints가 적용되지 않았기때문에 여기에 autoconstraints를 적용한다.

sketch_from_draft = Draft.make_sketch([sketch], autoconstraints=True, delete=False, radiusPrecision=0)
doc.recompute() # 새로고침같은 느낌

3. Constraints, Geometry

sketch내의 geometry와 constraint들을 출력해보면 다음과 같다. (설명은 이전 포스트 참고)

[('Type', 'Coincident'), ('First', 0), ('FirstPos', 2), ('Second', 1), ('SecondPos', 1), ('Third', -2000), ('ThirdPos', 0), ('Value', 0.0), ('Name', ''), ('Driving', True), ('InVirtualSpace', False), ('IsActive', True)]
[('Type', 'Coincident'), ('First', 1), ('FirstPos', 2), ('Second', 2), ('SecondPos', 2), ('Third', -2000), ('ThirdPos', 0), ('Value', 0.0), ('Name', ''), ('Driving', True), ('InVirtualSpace', False), ('IsActive', True)]
[('Type', 'Horizontal'), ('First', 2), ('FirstPos', 0), ('Second', -2000), ('SecondPos', 0), ('Third', -2000), ('ThirdPos', 0), ('Value', 0.0), ('Name', ''), ('Driving', True), ('InVirtualSpace', False), ('IsActive', True)]
[('Type', 'Coincident'), ('First', 2), ('FirstPos', 1), ('Second', 0), ('SecondPos', 1), ('Third', -2000), ('ThirdPos', 0), ('Value', 0.0), ('Name', ''), ('Driving', True), ('InVirtualSpace', False), ('IsActive', True)]
[<Line segment (0,0,0) (-10,-10,0) >, <Line segment (-10,-10,0) (-20,0,0) >, <Line segment (0,0,0) (-20,0,0) >]

좌표계가 달라서 그런지 좌표가 음수로 나온다.

그런데 LineSegment의 start point와 end point를 출력해보면 다음과 같이 나온다.

# Line1
Vector (0.0, 0.0, 0.0)
Vector (-10.000000000000005, -9.999999999999998, 0.0)
# Line2
Vector (-10.000000000000005, -9.999999999999998, 0.0)
Vector (-20.0, 0.0, 0.0)
# Line3
Vector (0.0, 0.0, 0.0)
Vector (-20.0, 0.0, 0.0)

-10.000000000000005, -9.999999999999998 처럼 값의 정밀도에서 큰 결점이 보인다. 미세한 차이라고 해서 이정도는 괜찮다고 생각한다면 큰 오산이다. 설계에서는 이러한 차이가 쌓이면 큰 문제가 되어버린다. 나비효과처럼...

일단 계속 진행해본다. (스트레스...)

4. Make NodeOp

from SketchGraphs.sketchgraphs.data import * # SketchGraphs
from Part import * # FreeCAD

seq = [NodeOp(label=EntityType.External, parameters=dict())] # 시작은 External

idx = 1
for geometry in sketch_from_draft.Geometry:
    if isinstance(geometry, LineSegment):
        start_point = geometry.StartPoint * -1
        end_point = geometry.EndPoint * -1

        pntX = (end_point.x - start_point.x) / 2 + start_point.x
        pntY = (end_point.y - start_point.y) / 2 + start_point.y

        startParam, endParam = 1, -1
        dirX, dirY = start_point.x - pntX, start_point.y - pntY

        parameters = {
            'isConstruction': False,
            'dirX': dirX,
            'dirY': dirY,
            'pntX': pntX,
            'pntY': pntY,
            'startParam': startParam,
            'endParam': endParam,
        }
        
        # Entity
        seq.append(NodeOp(label=EntityType.Line, parameters=parameters))
        # Subnode
        seq.append(NodeOp(label=SubnodeType.SN_Start, parameters=dict())) # entity idx + 1
        seq.append(NodeOp(label=SubnodeType.SN_End, parameters=dict())) # entity idx + 2
        # Entity to Subnode
        seq.append(EdgeOp(label=ConstraintType.Subnode, references=(idx, idx + 1), parameters=dict()))
        seq.append(EdgeOp(label=ConstraintType.Subnode, references=(idx, idx + 2), parameters=dict()))
        idx += 3

seq.append(NodeOp(label=EntityType.Stop, parameters=dict()))

# 0 => NodeOp(label=<EntityType.External: 7>, parameters={})
# 1 => NodeOp(label=<EntityType.Line: 1>, parameters={'isConstruction': False, 'dirX': -5.000000000000003, 'dirY': -4.999999999999999, 'pntX': 5.000000000000003, 'pntY': 4.999999999999999, 'startParam': 1, 'endParam': -1})
# 2 => NodeOp(label=<SubnodeType.SN_Start: 101>, parameters={})
# 3 => NodeOp(label=<SubnodeType.SN_End: 102>, parameters={})
# EdgeOp(label=<ConstraintType.Subnode: 101>, references=(1, 2), parameters={})
# EdgeOp(label=<ConstraintType.Subnode: 101>, references=(1, 3), parameters={})
# 4 => NodeOp(label=<EntityType.Line: 1>, parameters={'isConstruction': False, 'dirX': -4.999999999999998, 'dirY': 4.999999999999999, 'pntX': 15.000000000000004, 'pntY': 4.999999999999999, 'startParam': 1, 'endParam': -1})
# 5 => NodeOp(label=<SubnodeType.SN_Start: 101>, parameters={})
# 6 => NodeOp(label=<SubnodeType.SN_End: 102>, parameters={})
# EdgeOp(label=<ConstraintType.Subnode: 101>, references=(4, 5), parameters={})
# EdgeOp(label=<ConstraintType.Subnode: 101>, references=(4, 6), parameters={})
# 7 => NodeOp(label=<EntityType.Line: 1>, parameters={'isConstruction': False, 'dirX': -10.0, 'dirY': -0.0, 'pntX': 10.0, 'pntY': 0.0, 'startParam': 1, 'endParam': -1})
# 8 => NodeOp(label=<SubnodeType.SN_Start: 101>, parameters={})
# 9 => NodeOp(label=<SubnodeType.SN_End: 102>, parameters={})
# EdgeOp(label=<ConstraintType.Subnode: 101>, references=(7, 8), parameters={})
# EdgeOp(label=<ConstraintType.Subnode: 101>, references=(7, 9), parameters={})
# 10 => NodeOp(label=<EntityType.Stop: 8>, parameters={})

위처럼 Entity와 Entity의 subnode들을 추가해주었다. 즉, Entity당 3개의 NodeOp이 추가된 것이다. 여기서 SN_Start, SN_End 순서대로 추가한 이유는 SketchGraphs의 Position 값때문이다. 이전 포스트에서도 작성했듯이 Position이 1이면 start, 2이면 end이다.

 

추가로 Entity와 subnode와의 Subnode Constraint도 만들어주었다.

 

여기까지 만든 것을 SketchGraphs로 rendering해보면 다음과 같이 삼각형이 잘 나온다.

5. Make EdgeOp

이제 드디어 Constraint를 추가해주면 완성된다.

constraints = [constraint for constraint in sketch_from_draft.Constraints]
for constraint in constraints:
    first, firstPos = constraint.First + 1, constraint.FirstPos
    second, secondPos = constraint.Second + 1, constraint.SecondPos
    if constraint.Type == "Coincident":
        seq.append(EdgeOp(label=ConstraintType.Coincident, references=(first + firstPos, second + secondPos), parameters=dict()))
    elif constraint.Type == "Horizontal":
        seq.append(EdgeOp(label=ConstraintType.Horizontal, references=(first + firstPos,), parameters=dict()))
        
# EdgeOp(label=<ConstraintType.Coincident: 0>, references=(3, 5), parameters={})
# EdgeOp(label=<ConstraintType.Coincident: 0>, references=(6, 9), parameters={})
# EdgeOp(label=<ConstraintType.Coincident: 0>, references=(3, 5), parameters={})
# EdgeOp(label=<ConstraintType.Coincident: 0>, references=(6, 9), parameters={})
# EdgeOp(label=<ConstraintType.Horizontal: 4>, references=(7,), parameters={})
# EdgeOp(label=<ConstraintType.Coincident: 0>, references=(8, 2), parameters={})

 

각 reference의 index는 entity index + position이 된다.

 

rendering 결과 역시 이전과 같을 것이다. 단지 constraint만 추가된 것일뿐!

다음에는 여기에 AutoCompletion 모델을 적용해볼 예정이다. 그 때는 삼각형이 아닌 실무에서 사용하는 간단한 symbol을 사용해야할 것 같다.

728x90