Silly98 commited on
Commit
bf9994d
·
verified ·
1 Parent(s): 7bc066e

Upload face_labeler.py

Browse files
Files changed (1) hide show
  1. face_labeler.py +378 -0
face_labeler.py ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ from dataclasses import dataclass
4
+ from typing import List, Optional, Tuple
5
+
6
+ # Qt binding selection + pythonocc backend init
7
+ try:
8
+ from PyQt5 import QtCore, QtWidgets
9
+ _qt_backend = "qt-pyqt5"
10
+ except ImportError: # pragma: no cover
11
+ from PySide2 import QtCore, QtWidgets
12
+ _qt_backend = "qt-pyside2"
13
+
14
+ from OCC.Display.backend import load_backend
15
+
16
+ load_backend(_qt_backend)
17
+
18
+ from OCC.Core.STEPControl import STEPControl_Reader
19
+ from OCC.Core.IFSelect import IFSelect_RetDone
20
+ from OCC.Core.TopExp import TopExp_Explorer
21
+ from OCC.Core.TopAbs import TopAbs_FACE
22
+ from OCC.Core.TopoDS import topods
23
+ from OCC.Core.Quantity import Quantity_Color, Quantity_TOC_RGB
24
+ from OCC.Display.qtDisplay import qtViewer3d
25
+
26
+ CLASS_NAMES = [
27
+ "ExtrudeSide",
28
+ "ExtrudeEnd",
29
+ "CutSide",
30
+ "CutEnd",
31
+ "Fillet",
32
+ "Chamfer",
33
+ "RevolveSide",
34
+ "RevolveEnd",
35
+ ]
36
+
37
+ CLASS_COLORS_HEX = [
38
+ "#e41a1c", # red
39
+ "#377eb8", # blue
40
+ "#4daf4a", # green
41
+ "#984ea3", # purple
42
+ "#0e2579", # teal-blue (fillet)
43
+ "#a65628", # brown
44
+ "#f781bf", # pink
45
+ "#00a7a7", # teal
46
+ ]
47
+
48
+ UNLABELED_COLOR_HEX = "#d0d0d0"
49
+ HIGHLIGHT_COLOR_HEX = "#FFD400" # fixed for current selection
50
+
51
+
52
+ def hex_to_rgb01(color_hex: str) -> Tuple[float, float, float]:
53
+ color_hex = color_hex.lstrip("#")
54
+ r = int(color_hex[0:2], 16) / 255.0
55
+ g = int(color_hex[2:4], 16) / 255.0
56
+ b = int(color_hex[4:6], 16) / 255.0
57
+ return r, g, b
58
+
59
+
60
+ def rgb01_to_quantity(rgb: Tuple[float, float, float]) -> Quantity_Color:
61
+ return Quantity_Color(rgb[0], rgb[1], rgb[2], Quantity_TOC_RGB)
62
+
63
+
64
+ def text_color_for_bg(color_hex: str) -> str:
65
+ r, g, b = hex_to_rgb01(color_hex)
66
+ luminance = (0.299 * r + 0.587 * g + 0.114 * b)
67
+ return "#000000" if luminance > 0.6 else "#ffffff"
68
+
69
+
70
+ @dataclass
71
+ class FaceItem:
72
+ face: object
73
+ ais: object
74
+
75
+
76
+ class FaceLabeler(QtWidgets.QMainWindow):
77
+ def __init__(self, step_path: Optional[str] = None):
78
+ super().__init__()
79
+ self.setWindowTitle("BRep Face Labeler")
80
+ self.resize(1600, 1000)
81
+ self.setMinimumSize(1200, 800)
82
+
83
+ self.class_names = CLASS_NAMES
84
+ self.class_colors_rgb = [hex_to_rgb01(c) for c in CLASS_COLORS_HEX]
85
+ self.class_colors = [rgb01_to_quantity(c) for c in self.class_colors_rgb]
86
+ self.unlabeled_color = rgb01_to_quantity(hex_to_rgb01(UNLABELED_COLOR_HEX))
87
+
88
+ self.face_items: List[FaceItem] = []
89
+ self.labels: List[Optional[int]] = []
90
+ self.current_index: Optional[int] = None
91
+ self.highlight_enabled = True
92
+
93
+ self.highlight_color = rgb01_to_quantity(hex_to_rgb01(HIGHLIGHT_COLOR_HEX))
94
+
95
+ self._build_ui()
96
+
97
+ if step_path:
98
+ self.load_step(step_path)
99
+
100
+ def _build_ui(self) -> None:
101
+ central = QtWidgets.QWidget(self)
102
+ root_layout = QtWidgets.QHBoxLayout(central)
103
+ root_layout.setContentsMargins(8, 8, 8, 8)
104
+ root_layout.setSpacing(8)
105
+
106
+ self.viewer = qtViewer3d(central)
107
+ self.viewer.InitDriver()
108
+ self.display = self.viewer._display
109
+ try:
110
+ self.display.Context.SetAutomaticHilight(False)
111
+ except Exception:
112
+ pass
113
+ root_layout.addWidget(self.viewer, 1)
114
+
115
+ panel = QtWidgets.QWidget(central)
116
+ panel_layout = QtWidgets.QVBoxLayout(panel)
117
+ panel_layout.setContentsMargins(0, 0, 0, 0)
118
+ panel_layout.setSpacing(6)
119
+ root_layout.addWidget(panel, 0)
120
+
121
+ self.btn_import_step = QtWidgets.QPushButton("Import STEP")
122
+ self.btn_import_step.clicked.connect(self.on_import_step)
123
+ panel_layout.addWidget(self.btn_import_step)
124
+
125
+ self.btn_export_seg = QtWidgets.QPushButton("Export .seg")
126
+ self.btn_export_seg.clicked.connect(self.on_export_seg)
127
+ panel_layout.addWidget(self.btn_export_seg)
128
+
129
+ self.btn_review = QtWidgets.QPushButton("Review")
130
+ self.btn_review.clicked.connect(self.on_review)
131
+ panel_layout.addWidget(self.btn_review)
132
+
133
+ panel_layout.addSpacing(8)
134
+
135
+ nav_layout = QtWidgets.QHBoxLayout()
136
+ self.btn_prev = QtWidgets.QPushButton("<< Prev")
137
+ self.btn_prev.clicked.connect(self.on_prev)
138
+ self.btn_next = QtWidgets.QPushButton("Next >>")
139
+ self.btn_next.clicked.connect(self.on_next)
140
+ nav_layout.addWidget(self.btn_prev)
141
+ nav_layout.addWidget(self.btn_next)
142
+ panel_layout.addLayout(nav_layout)
143
+
144
+ self.info_label = QtWidgets.QLabel("No STEP loaded")
145
+ self.info_label.setWordWrap(True)
146
+ panel_layout.addWidget(self.info_label)
147
+
148
+ panel_layout.addSpacing(8)
149
+
150
+ legend_label = QtWidgets.QLabel("Assign Label")
151
+ legend_label.setStyleSheet("font-weight: bold;")
152
+ panel_layout.addWidget(legend_label)
153
+
154
+ grid = QtWidgets.QGridLayout()
155
+ grid.setSpacing(6)
156
+ for idx, name in enumerate(self.class_names):
157
+ btn = QtWidgets.QPushButton(f"{idx}: {name}")
158
+ bg = CLASS_COLORS_HEX[idx]
159
+ fg = text_color_for_bg(bg)
160
+ btn.setStyleSheet(f"background-color: {bg}; color: {fg};")
161
+ btn.clicked.connect(lambda checked=False, i=idx: self.assign_label(i))
162
+ grid.addWidget(btn, idx, 0)
163
+ panel_layout.addLayout(grid)
164
+
165
+ panel_layout.addStretch(1)
166
+
167
+ self.setCentralWidget(central)
168
+
169
+ def keyPressEvent(self, event) -> None: # pragma: no cover - UI only
170
+ if event.key() in (QtCore.Qt.Key_Right, QtCore.Qt.Key_D):
171
+ self.on_next()
172
+ return
173
+ if event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_A):
174
+ self.on_prev()
175
+ return
176
+ super().keyPressEvent(event)
177
+
178
+ def on_import_step(self) -> None:
179
+ path, _ = QtWidgets.QFileDialog.getOpenFileName(
180
+ self, "Open STEP", "", "STEP Files (*.stp *.step)"
181
+ )
182
+ if path:
183
+ self.load_step(path)
184
+
185
+ def on_export_seg(self) -> None:
186
+ if not self.labels:
187
+ QtWidgets.QMessageBox.warning(self, "Export", "No STEP loaded.")
188
+ return
189
+ if any(label is None for label in self.labels):
190
+ QtWidgets.QMessageBox.warning(
191
+ self,
192
+ "Export",
193
+ "Unlabeled faces remain. Label all faces before exporting.",
194
+ )
195
+ return
196
+ path, _ = QtWidgets.QFileDialog.getSaveFileName(
197
+ self, "Export .seg", "", "SEG Files (*.seg)"
198
+ )
199
+ if path:
200
+ self.save_seg(path)
201
+
202
+ def on_review(self) -> None:
203
+ if not self.labels:
204
+ QtWidgets.QMessageBox.information(self, "Review", "No STEP loaded.")
205
+ return
206
+ counts = [0 for _ in self.class_names]
207
+ unlabeled = []
208
+ for idx, label in enumerate(self.labels):
209
+ if label is None:
210
+ unlabeled.append(idx)
211
+ else:
212
+ counts[label] += 1
213
+
214
+ lines = [
215
+ f"Total faces: {len(self.labels)}",
216
+ f"Unlabeled: {len(unlabeled)}",
217
+ "",
218
+ ]
219
+ for idx, name in enumerate(self.class_names):
220
+ lines.append(f"{idx} {name}: {counts[idx]}")
221
+
222
+ if unlabeled:
223
+ preview = ", ".join(str(i) for i in unlabeled[:20])
224
+ if len(unlabeled) > 20:
225
+ preview += ", ..."
226
+ lines.append("")
227
+ lines.append(f"Unlabeled indices: {preview}")
228
+ lines.append("")
229
+ lines.append("Jump to first unlabeled?")
230
+ res = QtWidgets.QMessageBox.question(
231
+ self,
232
+ "Review",
233
+ "\n".join(lines),
234
+ QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
235
+ )
236
+ if res == QtWidgets.QMessageBox.Yes:
237
+ self.set_current_index(unlabeled[0])
238
+ else:
239
+ self.highlight_enabled = False
240
+ if self.current_index is not None:
241
+ self.set_face_color(
242
+ self.current_index, self.get_base_color(self.current_index)
243
+ )
244
+ QtWidgets.QMessageBox.information(self, "Review", "\n".join(lines))
245
+
246
+ def on_prev(self) -> None:
247
+ if self.current_index is None:
248
+ return
249
+ if self.current_index <= 0:
250
+ return
251
+ self.set_current_index(self.current_index - 1)
252
+
253
+ def on_next(self) -> None:
254
+ if self.current_index is None:
255
+ return
256
+ if self.current_index >= len(self.face_items) - 1:
257
+ return
258
+ self.set_current_index(self.current_index + 1)
259
+
260
+ def load_step(self, path: str) -> None:
261
+ reader = STEPControl_Reader()
262
+ status = reader.ReadFile(path)
263
+ if status != IFSelect_RetDone:
264
+ QtWidgets.QMessageBox.warning(
265
+ self, "Load STEP", f"Failed to read STEP file: {path}"
266
+ )
267
+ return
268
+ reader.TransferRoots()
269
+ shape = reader.OneShape()
270
+
271
+ self.display.EraseAll()
272
+ self.face_items.clear()
273
+ self.labels.clear()
274
+ self.highlight_enabled = True
275
+ self.current_index = None
276
+
277
+ explorer = TopExp_Explorer(shape, TopAbs_FACE)
278
+ while explorer.More():
279
+ face = topods.Face(explorer.Current())
280
+ ais = self.display.DisplayShape(face, update=False, color=self.unlabeled_color)
281
+ if isinstance(ais, list):
282
+ ais = ais[0]
283
+ try:
284
+ self.display.Context.SetDisplayMode(ais, 1, False)
285
+ except Exception:
286
+ pass
287
+ self.face_items.append(FaceItem(face=face, ais=ais))
288
+ self.labels.append(None)
289
+ explorer.Next()
290
+
291
+ if not self.face_items:
292
+ QtWidgets.QMessageBox.warning(
293
+ self, "Load STEP", "No faces found in STEP file."
294
+ )
295
+ self.display.Repaint()
296
+ return
297
+
298
+ self.display.FitAll()
299
+ self.set_current_index(0)
300
+
301
+ def save_seg(self, path: str) -> None:
302
+ with open(path, "w", encoding="utf-8") as handle:
303
+ for label in self.labels:
304
+ handle.write(f"{label}\n")
305
+ QtWidgets.QMessageBox.information(self, "Export", f"Saved: {path}")
306
+
307
+ def assign_label(self, label_index: int) -> None:
308
+ if self.current_index is None:
309
+ return
310
+ self.labels[self.current_index] = label_index
311
+ self.apply_current_highlight(self.current_index)
312
+ self.update_info()
313
+
314
+ def update_info(self) -> None:
315
+ if self.current_index is None:
316
+ self.info_label.setText("No STEP loaded")
317
+ return
318
+ label = self.labels[self.current_index]
319
+ label_text = "Unlabeled" if label is None else f"{label}: {self.class_names[label]}"
320
+ self.info_label.setText(
321
+ f"Face {self.current_index + 1}/{len(self.face_items)}\n"
322
+ f"Label: {label_text}"
323
+ )
324
+
325
+ def get_base_color(self, index: int) -> Quantity_Color:
326
+ label = self.labels[index]
327
+ return self.unlabeled_color if label is None else self.class_colors[label]
328
+
329
+ def get_highlight_color(self, index: int) -> Quantity_Color:
330
+ return self.highlight_color
331
+
332
+ def set_current_index(self, index: int) -> None:
333
+ if not self.face_items:
334
+ return
335
+ index = max(0, min(index, len(self.face_items) - 1))
336
+ if self.current_index is not None:
337
+ self.set_face_color(self.current_index, self.get_base_color(self.current_index))
338
+ self.current_index = index
339
+ self.apply_current_highlight(self.current_index)
340
+ self.update_info()
341
+
342
+ def apply_current_highlight(self, index: int) -> None:
343
+ if self.highlight_enabled:
344
+ self.set_face_color(index, self.get_highlight_color(index))
345
+ else:
346
+ self.set_face_color(index, self.get_base_color(index))
347
+
348
+ def set_face_color(self, index: int, color: Quantity_Color) -> None:
349
+ ais = self.face_items[index].ais
350
+ if isinstance(ais, list):
351
+ for item in ais:
352
+ self._set_ais_color(item, color)
353
+ else:
354
+ self._set_ais_color(ais, color)
355
+ self.display.Repaint()
356
+
357
+ def _set_ais_color(self, ais, color: Quantity_Color) -> None:
358
+ try:
359
+ ais.SetColor(color)
360
+ except Exception:
361
+ self.display.Context.SetColor(ais, color, False)
362
+ self.display.Context.Redisplay(ais, False)
363
+
364
+
365
+ def main() -> int:
366
+ app = QtWidgets.QApplication(sys.argv)
367
+ step_path = None
368
+ if len(sys.argv) > 1:
369
+ candidate = sys.argv[1]
370
+ if os.path.exists(candidate):
371
+ step_path = candidate
372
+ window = FaceLabeler(step_path=step_path)
373
+ window.show()
374
+ return app.exec_()
375
+
376
+
377
+ if __name__ == "__main__":
378
+ raise SystemExit(main())