py-alaska 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,730 @@
1
+ """tab_camera.py - 카메라 측정 탭"""
2
+
3
+ import os
4
+ import time
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import cv2
10
+ import numpy as np
11
+ from PySide6.QtCore import QPointF, QRectF, Qt, QTimer
12
+ from PySide6.QtGui import QColor, QFontMetrics, QImage, QMouseEvent, QPainter, QPen, QPixmap, QWheelEvent
13
+ from PySide6.QtWidgets import (
14
+ QGroupBox, QHBoxLayout, QLabel, QMessageBox, QPushButton,
15
+ QSlider, QSplitter, QVBoxLayout, QWidget
16
+ )
17
+
18
+ from ConfigReader import GlobalConfig
19
+ from utils.config_log import logger as log
20
+ from project.window_tab_browser import TabApp
21
+
22
+ from .ImageReader import CenterDetector
23
+ from .NCPointView import CamPointMap
24
+ from .SlideToggle import LabeledToggle
25
+ from .sim_camera import SimCameraDevice
26
+
27
+ # 상수
28
+ SIM_FLAG = False
29
+ ELAPSED_UPDATE_MS = 3000
30
+ MIN_ZOOM, MAX_ZOOM, ZOOM_DELTA = 0.1, 10.0, 0.1
31
+
32
+ BUTTON_STYLES = {
33
+ 'ready': ("#4CAF50", "#45a049", "#3d8b40"),
34
+ 'measuring': ("#FF9800", "#F57C00", "#E65100"),
35
+ 'complete': ("#f44336", "#da190b", "#c2170a")
36
+ }
37
+
38
+
39
+ class ZoomView(QWidget):
40
+ """확대/축소 이미지 뷰"""
41
+ WIDGET_STYLE = "ZoomView { background-color: #222; border: 2px solid #666; border-radius: 5px; color: #888; }"
42
+ BUTTON_STYLE = """QPushButton { background-color: rgba(85,85,85,180); color: white; border: 2px solid rgba(102,102,102,200);
43
+ border-radius: 5px; font-size: 12px; font-weight: bold; padding: 5px; }
44
+ QPushButton:hover { background-color: rgba(102,102,102,220); }
45
+ QPushButton:pressed { background-color: rgba(68,68,68,220); }"""
46
+
47
+ CAMERA_BUTTON_STYLE_CONNECTED = """QPushButton { background-color: rgba(76,175,80,180); color: white; border: 2px solid rgba(102,102,102,200);
48
+ border-radius: 5px; font-size: 14px; padding: 5px; }"""
49
+ CAMERA_BUTTON_STYLE_DISCONNECTED = """QPushButton { background-color: rgba(244,67,54,180); color: white; border: 2px solid rgba(102,102,102,200);
50
+ border-radius: 5px; font-size: 14px; padding: 5px; }"""
51
+
52
+ def __init__(self, parent=None):
53
+ super().__init__(parent)
54
+ self.setMinimumSize(640, 480)
55
+ self.setStyleSheet(self.WIDGET_STYLE)
56
+ self.setMouseTracking(True)
57
+
58
+ # 카메라 상태 버튼
59
+ self.camera_status_button = QPushButton("CAM", self)
60
+ self.camera_status_button.setFixedSize(70, 30)
61
+ self.camera_status_button.setFocusPolicy(Qt.NoFocus)
62
+ self.camera_status_button.setStyleSheet(self.CAMERA_BUTTON_STYLE_DISCONNECTED)
63
+ self.camera_status_button.setToolTip("카메라 연결 안됨")
64
+ self.camera_status_button.raise_()
65
+
66
+ self.full_button = QPushButton("Full", self)
67
+ self.full_button.setFixedSize(60, 30)
68
+ self.full_button.setCursor(Qt.PointingHandCursor)
69
+ self.full_button.setFocusPolicy(Qt.NoFocus)
70
+ self.full_button.setStyleSheet(self.BUTTON_STYLE)
71
+ self.full_button.clicked.connect(self.fit_to_screen)
72
+ self.full_button.raise_()
73
+
74
+ self.original_pixmap: Optional[QPixmap] = None
75
+ self.display_text = "카메라 연결 후 이미지가 표시됩니다"
76
+ self.show_crosshair = True
77
+ self.zoom_factor = 1.0
78
+ self.pan_offset = QPointF(0, 0)
79
+ self.is_panning = False
80
+ self.last_mouse_pos = QPointF()
81
+ self.is_camera_connected = False
82
+
83
+ def set_crosshair_visible(self, visible: bool):
84
+ if self.show_crosshair != visible:
85
+ self.show_crosshair = visible
86
+ self.update()
87
+
88
+ def set_image(self, image: np.ndarray):
89
+ if image is None or image.size == 0:
90
+ self.original_pixmap = None
91
+ else:
92
+ try:
93
+ img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) if len(image.shape) == 3 else image
94
+ h, w = img_rgb.shape[:2]
95
+ bpl = w * (3 if len(img_rgb.shape) == 3 else 1)
96
+ fmt = QImage.Format_RGB888 if len(img_rgb.shape) == 3 else QImage.Format_Grayscale8
97
+ self.original_pixmap = QPixmap.fromImage(QImage(img_rgb.data, w, h, bpl, fmt))
98
+ except Exception as e:
99
+ log.error(f"[ZoomView] 이미지 변환 실패: {e}")
100
+ self.update()
101
+
102
+ def reset_view(self):
103
+ self.zoom_factor = 1.0
104
+ self.pan_offset = QPointF(0, 0)
105
+ self.update()
106
+
107
+ def fit_to_screen(self):
108
+ if self.original_pixmap and not self.original_pixmap.isNull():
109
+ scale = min(self.width() / self.original_pixmap.width(), self.height() / self.original_pixmap.height())
110
+ self.zoom_factor = max(MIN_ZOOM, min(scale, MAX_ZOOM))
111
+ self.pan_offset = QPointF(0, 0)
112
+ self.update()
113
+
114
+ def resizeEvent(self, event):
115
+ super().resizeEvent(event)
116
+ self.full_button.move(self.width() - 70, 10)
117
+ self.camera_status_button.move(self.width() - 150, 10)
118
+
119
+ def set_camera_status(self, is_connected: bool):
120
+ """카메라 연결 상태 업데이트"""
121
+ self.is_camera_connected = is_connected
122
+ if is_connected:
123
+ self.camera_status_button.setStyleSheet(self.CAMERA_BUTTON_STYLE_CONNECTED)
124
+ self.camera_status_button.setToolTip("카메라 연결됨")
125
+ else:
126
+ self.camera_status_button.setStyleSheet(self.CAMERA_BUTTON_STYLE_DISCONNECTED)
127
+ self.camera_status_button.setToolTip("카메라 연결 안됨")
128
+ self.update()
129
+
130
+ def wheelEvent(self, event: QWheelEvent):
131
+ if not self.original_pixmap:
132
+ return
133
+ mouse_pos = event.position()
134
+ scene_before = self._widget_to_scene(mouse_pos)
135
+ zoom_delta = ZOOM_DELTA if event.angleDelta().y() > 0 else -ZOOM_DELTA
136
+ self.zoom_factor = max(MIN_ZOOM, min(self.zoom_factor + zoom_delta, MAX_ZOOM))
137
+ scene_after = self._widget_to_scene(mouse_pos)
138
+ self.pan_offset -= (scene_after - scene_before)
139
+ self.update()
140
+ event.accept()
141
+
142
+ def mousePressEvent(self, event: QMouseEvent):
143
+ if self.full_button.geometry().contains(event.pos()):
144
+ event.ignore()
145
+ return
146
+ if event.button() == Qt.LeftButton and self.original_pixmap:
147
+ self.is_panning = True
148
+ self.last_mouse_pos = event.position()
149
+ self.setCursor(Qt.ClosedHandCursor)
150
+ event.accept()
151
+
152
+ def mouseMoveEvent(self, event: QMouseEvent):
153
+ if self.full_button.geometry().contains(event.pos()):
154
+ return
155
+ if self.is_panning:
156
+ self.pan_offset += event.position() - self.last_mouse_pos
157
+ self.last_mouse_pos = event.position()
158
+ self.update()
159
+ event.accept()
160
+ elif self.original_pixmap:
161
+ self.setCursor(Qt.OpenHandCursor)
162
+
163
+ def mouseReleaseEvent(self, event: QMouseEvent):
164
+ if event.button() == Qt.LeftButton:
165
+ self.is_panning = False
166
+ self.setCursor(Qt.OpenHandCursor if self.original_pixmap else Qt.ArrowCursor)
167
+ event.accept()
168
+
169
+ def draw_crosshair(self, painter: QPainter, imgBox: QRectF):
170
+ if not self.original_pixmap or imgBox.isNull():
171
+ return
172
+ cfg = GlobalConfig.instance()
173
+ pixels_per_mm = cfg.getConfigValue("core.pixels_per_mm", 110.0)
174
+ circle_mark_size_mm = cfg.getConfigValue("core.circle_mark_size", 1.75)
175
+ center_offset = cfg.getConfigValue("core.center_offset", [0, 0])
176
+
177
+ circle_diameter = circle_mark_size_mm * pixels_per_mm * self.zoom_factor
178
+ cx = imgBox.center().x() + center_offset[0]
179
+ cy = imgBox.center().y() + center_offset[1]
180
+
181
+ painter.setPen(QPen(QColor(255, 0, 0), 2))
182
+ painter.drawEllipse(QPointF(cx, cy), circle_diameter / 2, circle_diameter / 2)
183
+
184
+ painter.setPen(QPen(QColor(255, 0, 0), 1))
185
+ cross_size = 30 * self.zoom_factor
186
+ painter.drawLine(cx - cross_size, cy, cx + cross_size, cy)
187
+ painter.drawLine(cx, cy - cross_size, cx, cy + cross_size)
188
+
189
+ def paintEvent(self, event):
190
+ painter = QPainter(self)
191
+ painter.setRenderHint(QPainter.Antialiasing)
192
+ painter.setRenderHint(QPainter.SmoothPixmapTransform)
193
+ painter.fillRect(self.rect(), QColor(34, 34, 34))
194
+
195
+ if not self.original_pixmap or self.original_pixmap.isNull():
196
+ painter.setPen(QColor(136, 136, 136))
197
+ painter.drawText(self.rect(), Qt.AlignCenter, self.display_text)
198
+ else:
199
+ center = QPointF(self.width() / 2, self.height() / 2)
200
+ w = self.original_pixmap.width() * self.zoom_factor
201
+ h = self.original_pixmap.height() * self.zoom_factor
202
+ x = center.x() - w / 2 + self.pan_offset.x()
203
+ y = center.y() - h / 2 + self.pan_offset.y()
204
+ imgBox = QRectF(x, y, w, h)
205
+ painter.drawPixmap(imgBox, self.original_pixmap, QRectF(self.original_pixmap.rect()))
206
+ if self.show_crosshair:
207
+ self.draw_crosshair(painter, imgBox)
208
+
209
+ # 카메라 연결 안됨 배너
210
+ if not self.is_camera_connected:
211
+ banner_text = "카메라가 연결되지 않았습니다."
212
+ font = painter.font()
213
+ font.setPointSize(16)
214
+ font.setBold(True)
215
+ painter.setFont(font)
216
+
217
+ # 배너 배경
218
+ fm = QFontMetrics(font)
219
+ text_width = fm.horizontalAdvance(banner_text)
220
+ text_height = fm.height()
221
+ padding_h, padding_v = 40, 20
222
+ border_radius = 10
223
+ banner_rect = QRectF(
224
+ (self.width() - text_width) / 2 - padding_h,
225
+ (self.height() - text_height) / 2 - padding_v,
226
+ text_width + padding_h * 2,
227
+ text_height + padding_v * 2
228
+ )
229
+ painter.setBrush(QColor(180, 50, 50, 200))
230
+ painter.setPen(QPen(QColor(255, 255, 255), 2))
231
+ painter.drawRoundedRect(banner_rect, border_radius, border_radius)
232
+
233
+ # 배너 텍스트
234
+ painter.setPen(QColor(255, 255, 255))
235
+ painter.drawText(self.rect(), Qt.AlignCenter, banner_text)
236
+
237
+ painter.end()
238
+
239
+ def _widget_to_scene(self, pos: QPointF) -> QPointF:
240
+ if not self.original_pixmap:
241
+ return QPointF()
242
+ center = QPointF(self.width() / 2, self.height() / 2)
243
+ offset = pos - center
244
+ return QPointF((offset.x() - self.pan_offset.x()) / self.zoom_factor,
245
+ (offset.y() - self.pan_offset.y()) / self.zoom_factor)
246
+
247
+
248
+ class CameraView(ZoomView):
249
+ pass
250
+
251
+
252
+ class TabCamera(TabApp):
253
+ """카메라 측정 탭"""
254
+
255
+ def __init__(self, parent=None, param=None):
256
+ super().__init__(parent)
257
+ self.measurement_data = param or {}
258
+ self.project_name = self.measurement_data.get('project', 'Unknown')
259
+ self.current_path = self.measurement_data.get('current_path', '')
260
+ self.nc_list = self.measurement_data.get('nc_list', [])
261
+
262
+ self.is_connected = False
263
+ self.save_enabled = False
264
+ self.trigger_mode = False
265
+ self.frame_count = 0
266
+ self.is_measuring = False
267
+ self.current_session_path = None
268
+ self.nc_points = []
269
+ self.measurement_start_time = None
270
+
271
+ log.info(f"[CameraTab] 초기화 ({self.project_name}, NC: {len(self.nc_list)}개)")
272
+
273
+ if self.measurement_data:
274
+ self.set_tab_title(f"측정: {self.project_name}")
275
+
276
+ self.detector = CenterDetector()
277
+ self._init_camera()
278
+ self._init_image_save()
279
+
280
+ self.elapsed_timer = QTimer(self)
281
+ self.elapsed_timer.setInterval(ELAPSED_UPDATE_MS)
282
+ self.elapsed_timer.timeout.connect(self._update_elapsed_time)
283
+
284
+ self._load_nc_points()
285
+ self._setup_ui()
286
+
287
+ # =========================================================================
288
+ # 초기화
289
+ # =========================================================================
290
+ def _init_camera(self):
291
+ if SIM_FLAG:
292
+ log.info("[CameraTab] 시뮬레이션 모드")
293
+ self.camera = SimCameraDevice()
294
+ else:
295
+ log.info("[CameraTab] 실제 카메라 모드")
296
+ from .imi_camera import imi_camera
297
+ self.camera = imi_camera()
298
+ self.camera.image_received.connect(self._on_camera_image_received)
299
+ self.camera.status_changed.connect(self._on_camera_status_changed)
300
+
301
+ def _init_image_save(self):
302
+ from .ImageSave import ImageSave
303
+ self.image_save = ImageSave()
304
+
305
+ def _load_nc_points(self):
306
+ if not self.nc_list:
307
+ return
308
+ try:
309
+ from nc_reader import nc_reader
310
+ reader = nc_reader(self.nc_list[0])
311
+ detailed_points = reader.read_detailed(firstOnly=True)
312
+ self.nc_points = [
313
+ {'x': p['x']['value'], 'y': p['y']['value'],
314
+ 'z': p.get('z', {}).get('value', 0.0),
315
+ 'a': p.get('a', {}).get('value', 0.0),
316
+ 'c': p.get('c', {}).get('value', 0.0)}
317
+ for p in detailed_points
318
+ ]
319
+ log.info(f"[CameraTab] NC 포인트: {len(self.nc_points)}개")
320
+ except Exception as e:
321
+ log.error(f"[CameraTab] NC 포인트 로드 실패: {e}")
322
+ self.nc_points = []
323
+
324
+ # =========================================================================
325
+ # UI 구성
326
+ # =========================================================================
327
+ def _setup_ui(self):
328
+ main_layout = QHBoxLayout(self)
329
+ main_layout.setContentsMargins(5, 5, 5, 5)
330
+ main_layout.setSpacing(5)
331
+
332
+ splitter = QSplitter(Qt.Horizontal)
333
+ splitter.addWidget(self._create_camera_view())
334
+ splitter.addWidget(self._create_right_panel())
335
+ splitter.setSizes([700, 300])
336
+ main_layout.addWidget(splitter)
337
+
338
+ self.setStyleSheet("""
339
+ QWidget { background-color: #333; color: white; }
340
+ QPushButton { background-color: #555; color: white; border: 1px solid #666; padding: 8px 15px; border-radius: 4px; font-size: 13px; }
341
+ QPushButton:hover { background-color: #666; }
342
+ QPushButton:disabled { background-color: #444; color: #888; }
343
+ QGroupBox { border: 1px solid #666; border-radius: 5px; margin-top: 10px; padding-top: 10px; font-weight: bold; font-size: 14px; }
344
+ QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 5px; color: #4CAF50; }
345
+ QLabel { color: white; font-size: 13px; }
346
+ QComboBox, QSpinBox, QLineEdit { background-color: #444; color: white; border: 1px solid #666; padding: 5px; border-radius: 3px; }
347
+ """)
348
+
349
+ def _create_camera_view(self) -> QWidget:
350
+ widget = QWidget()
351
+ layout = QVBoxLayout(widget)
352
+ layout.setContentsMargins(0, 0, 0, 0)
353
+ layout.setSpacing(5)
354
+ self.camera_view = CameraView()
355
+ layout.addWidget(self.camera_view, stretch=1)
356
+ return widget
357
+
358
+ def _create_right_panel(self) -> QWidget:
359
+ widget = QWidget()
360
+ layout = QVBoxLayout(widget)
361
+ layout.setContentsMargins(0, 0, 0, 0)
362
+ layout.setSpacing(5)
363
+
364
+ # 중심화면 그룹
365
+ center_group = QGroupBox("중심화면")
366
+ center_layout = QVBoxLayout(center_group)
367
+ center_layout.setContentsMargins(5, 10, 5, 5)
368
+ center_layout.setSpacing(3)
369
+
370
+ # 측정 버튼
371
+ self.measure_button = QPushButton("측정")
372
+ self._set_measure_button_style('ready')
373
+ self.measure_button.clicked.connect(self._on_measure_button_clicked)
374
+ center_layout.addWidget(self.measure_button)
375
+
376
+ # 경과 시간
377
+ self.elapsed_time_label = QLabel("")
378
+ self.elapsed_time_label.setStyleSheet("color: #FFA726; font-weight: bold; font-size: 12px;")
379
+ self.elapsed_time_label.setVisible(False)
380
+ center_layout.addWidget(self.elapsed_time_label)
381
+
382
+ # 프레임/트리거/3점촬영 컨트롤
383
+ control_layout = QHBoxLayout()
384
+ self.frame_count_label = QLabel("Frame: 0")
385
+ self.frame_count_label.setStyleSheet("color: white; font-weight: bold;")
386
+ control_layout.addWidget(self.frame_count_label)
387
+
388
+ control_layout.addStretch() # 좌측(Frame) / 우측(3점) 분리
389
+
390
+ self.cut3_toggle = LabeledToggle("3점:")
391
+ cfg = GlobalConfig.instance()
392
+ self.cut3_toggle.setChecked(cfg.getConfigValue("core.image_per_info", 1) == 3)
393
+ self.cut3_toggle.toggled.connect(self._on_cut3_toggle_changed)
394
+ control_layout.addWidget(self.cut3_toggle)
395
+ center_layout.addLayout(control_layout)
396
+
397
+ # 노출 슬라이더
398
+ expose_layout = QHBoxLayout()
399
+ self.expose_label = QLabel("Expose:")
400
+ self.expose_label.setMinimumWidth(60)
401
+ expose_layout.addWidget(self.expose_label)
402
+
403
+ self.expose_slider = QSlider(Qt.Horizontal)
404
+ self.expose_slider.setRange(5000, 30000)
405
+ self.expose_slider.setValue(5000)
406
+ self.expose_slider.setTickPosition(QSlider.TicksBelow)
407
+ self.expose_slider.setTickInterval(2000)
408
+ self.expose_slider.setEnabled(False)
409
+ self.expose_slider.valueChanged.connect(self._on_expose_changed)
410
+ expose_layout.addWidget(self.expose_slider)
411
+
412
+ self.expose_value_label = QLabel("5,000")
413
+ self.expose_value_label.setMinimumWidth(50)
414
+ self.expose_value_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
415
+ expose_layout.addWidget(self.expose_value_label)
416
+ center_layout.addLayout(expose_layout)
417
+
418
+ # 중심 확대 뷰
419
+ self.center_zoom_view = QLabel("중심 영역 확대")
420
+ self.center_zoom_view.setAlignment(Qt.AlignCenter)
421
+ self.center_zoom_view.setMinimumHeight(150)
422
+ self.center_zoom_view.setStyleSheet("QLabel { background-color: #222; border: 2px solid #666; border-radius: 5px; color: #888; }")
423
+ center_layout.addWidget(self.center_zoom_view, stretch=1)
424
+ layout.addWidget(center_group, stretch=1)
425
+
426
+ # 측정 정보 그룹
427
+ if self.measurement_data:
428
+ info_group = QGroupBox("측정 정보")
429
+ info_layout = QVBoxLayout(info_group)
430
+ info_layout.setSpacing(3)
431
+
432
+ project_label = QLabel(f"프로젝트: {self.project_name}")
433
+ project_label.setStyleSheet("color: #CCCCCC;")
434
+ info_layout.addWidget(project_label)
435
+
436
+ path_label = QLabel(f"경로: {self.current_path}")
437
+ path_label.setStyleSheet("color: #CCCCCC;")
438
+ path_label.setWordWrap(True)
439
+ info_layout.addWidget(path_label)
440
+
441
+ nc_filenames = [os.path.basename(nc) for nc in self.nc_list]
442
+ nc_label = QLabel(f"NC 파일: {', '.join(nc_filenames)}")
443
+ nc_label.setStyleSheet("color: #CCCCCC;")
444
+ nc_label.setWordWrap(True)
445
+ info_layout.addWidget(nc_label)
446
+ layout.addWidget(info_group)
447
+
448
+ # NC 포인트 뷰
449
+ nc_group = QGroupBox("Point")
450
+ nc_layout = QVBoxLayout(nc_group)
451
+ self.nc_point_view = CamPointMap()
452
+ if self.nc_points and self.nc_list:
453
+ self.nc_point_view.set_nc_points(self.nc_points, os.path.basename(self.nc_list[0]))
454
+ nc_layout.addWidget(self.nc_point_view)
455
+ layout.addWidget(nc_group, stretch=1)
456
+
457
+ return widget
458
+
459
+ # =========================================================================
460
+ # 탭 라이프사이클
461
+ # =========================================================================
462
+ def can_close(self) -> bool:
463
+ if self.is_measuring:
464
+ reply = QMessageBox.question(
465
+ self, '측정 중', '측정이 진행 중입니다. 탭을 닫으시겠습니까?',
466
+ QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
467
+ if reply == QMessageBox.No:
468
+ return False
469
+ self.image_save.end()
470
+ self._disconnect_camera()
471
+ return True
472
+
473
+ def on_tab_activated(self):
474
+ self.monitor_mode()
475
+
476
+ def on_tab_deactivated(self):
477
+ if self.is_connected and not self.is_measuring:
478
+ log.info('[CameraTab] 탭 비활성화: 카메라 연결 해제')
479
+ self._disconnect_camera()
480
+
481
+ def on_session_selected(self, project, nc_filename: str, session):
482
+ project_name = project.name if hasattr(project, 'name') else 'Unknown'
483
+ log.info(f"[CameraTab] 세션 선택: {project_name}/{nc_filename}/{session.session_name}")
484
+
485
+ self.current_session_path = None
486
+ if hasattr(session, 'session_path') and session.session_path:
487
+ session_dir = Path(session.session_path)
488
+ self.current_session_path = str(session_dir)
489
+ if session_dir.exists() and hasattr(self.camera, 'set_image_path'):
490
+ self.camera.set_image_path(str(session_dir))
491
+
492
+ # =========================================================================
493
+ # 측정 모드
494
+ # =========================================================================
495
+ def monitor_mode(self):
496
+ if self.is_measuring:
497
+ return
498
+ log.info('[CameraTab] 모니터 모드 진입')
499
+ if not self.is_connected and self._connect_camera():
500
+ self.trigger_mode = False
501
+ if not SIM_FLAG:
502
+ self.camera.set_trigger_mode(False)
503
+
504
+ def _on_measure_button_clicked(self):
505
+ if not self.is_measuring:
506
+ self._start_measurement()
507
+ else:
508
+ self._stop_measurement()
509
+
510
+ def _start_measurement(self):
511
+ self.is_measuring = True
512
+ self.frame_count = 0
513
+ self.frame_count_label.setText("Frame: 0")
514
+
515
+ if not self._connect_camera():
516
+ QMessageBox.warning(self, "연결 실패", "카메라 연결에 실패했습니다.")
517
+ self.is_measuring = False
518
+ self.image_save.end()
519
+ return
520
+
521
+ self.camera.image_received.disconnect(self._on_camera_image_received)
522
+ self.camera.stop_stream()
523
+ time.sleep(0.5)
524
+
525
+ self.trigger_mode = True
526
+ if not SIM_FLAG:
527
+ self.camera.set_trigger_mode(True)
528
+ self.camera.start_stream()
529
+ self.camera.image_received.connect(self._on_camera_image_received)
530
+
531
+ session_path = self._create_session_path()
532
+ if session_path:
533
+ self.current_session_path = session_path
534
+ if self.image_save.start(session_path):
535
+ self.save_enabled = True
536
+ log.info(f"[CameraTab] 측정 저장 시작: {session_path}")
537
+
538
+ self._set_exposure_visible(False)
539
+ if self.nc_point_view:
540
+ self.nc_point_view.set_current_count(0)
541
+
542
+ self.measurement_start_time = datetime.now()
543
+ self.elapsed_time_label.setText("경과시간: 00:00:00")
544
+ self.elapsed_time_label.setVisible(True)
545
+ self.elapsed_timer.start()
546
+
547
+ total = len(self.nc_points)
548
+ self.measure_button.setText(f"측정중(0/{total})")
549
+ self._set_measure_button_style('measuring')
550
+ log.info(f"[CameraTab] 측정 시작: {total}개")
551
+
552
+ def _stop_measurement(self):
553
+ self.is_measuring = False
554
+ self.measure_button.setText("측정")
555
+ self._set_measure_button_style('ready')
556
+ self._disconnect_camera()
557
+
558
+ self._set_exposure_visible(True)
559
+ self.elapsed_timer.stop()
560
+ self.elapsed_time_label.setVisible(False)
561
+ self.measurement_start_time = None
562
+ self.image_save.end()
563
+
564
+ log.info('[CameraTab] 측정 종료')
565
+ self.monitor_mode()
566
+
567
+ def _auto_stop_measurement(self):
568
+ if self.is_measuring:
569
+ self._stop_measurement()
570
+ log.info("[CameraTab] 측정 자동 종료")
571
+
572
+ # =========================================================================
573
+ # 카메라 제어
574
+ # =========================================================================
575
+ def _connect_camera(self) -> bool:
576
+ log.info(f"[CameraTab] 카메라 연결 (SIM={SIM_FLAG})")
577
+ try:
578
+ if self.camera.open():
579
+ self.is_connected = True
580
+ self._update_controls_enabled(True)
581
+ # 카메라에서 노출값 가져와서 슬라이더에 설정
582
+ if hasattr(self.camera, 'get_exposure'):
583
+ exposure = self.camera.get_exposure()
584
+ self.expose_slider.setValue(exposure)
585
+ self.expose_value_label.setText(f"{exposure:,}")
586
+ if SIM_FLAG and hasattr(self.camera, 'start_grabbing'):
587
+ self.camera.start_grabbing()
588
+ log.info("[CameraTab] 카메라 연결 성공")
589
+ return True
590
+ except Exception as e:
591
+ log.exception(f"[CameraTab] 카메라 연결 실패: {e}")
592
+ return False
593
+
594
+ def _disconnect_camera(self):
595
+ try:
596
+ self.camera.close()
597
+ self.is_connected = False
598
+ self._update_controls_enabled(False)
599
+ self.center_zoom_view.setText("중심 영역 확대")
600
+ self.center_zoom_view.setPixmap(QPixmap())
601
+ log.info("[CameraTab] 카메라 연결 해제")
602
+ except Exception as e:
603
+ log.exception(f"[CameraTab] 카메라 해제 실패: {e}")
604
+
605
+ # =========================================================================
606
+ # 이미지 콜백
607
+ # =========================================================================
608
+ def _on_camera_status_changed(self, status: str):
609
+ """카메라 상태 변경 처리"""
610
+ is_connected = (status == "connected")
611
+ if hasattr(self, 'camera_view'):
612
+ self.camera_view.set_camera_status(is_connected)
613
+
614
+ def _on_camera_image_received(self, image: np.ndarray):
615
+ self.camera_view.set_image(image)
616
+ self._update_center_zoom(image)
617
+
618
+ if not self.is_measuring:
619
+ return
620
+
621
+ self.image_info = self.detector.build_info(self.frame_count, image, self.current_session_path)
622
+
623
+ if self.save_enabled and self.image_info:
624
+ image_per_info = GlobalConfig.instance().getConfigValue("core.image_per_info", 1)
625
+ if self.image_save.save_image(image, self.image_info, image_per_info):
626
+ self._on_frame_saved()
627
+
628
+ def _on_frame_saved(self):
629
+ self.frame_count = self.image_save.frame_count
630
+ self.frame_count_label.setText(f"Frame: {self.frame_count}")
631
+
632
+ total = len(self.nc_points)
633
+ multiplier = GlobalConfig.instance().getConfigValue("core.image_per_info", 1)
634
+
635
+ if self.nc_point_view and self.frame_count <= len(self.nc_point_view.nc_points) * multiplier:
636
+ self.nc_point_view.set_current_count(self.frame_count // multiplier)
637
+
638
+ is_complete = self.frame_count >= total * multiplier
639
+ text = "측정종료" if is_complete else f"측정중({self.frame_count}/{total})"
640
+ self.measure_button.setText(text)
641
+ self._set_measure_button_style('complete' if is_complete else 'measuring')
642
+
643
+ if is_complete:
644
+ log.info(f"[CameraTab] 측정 완료: {self.frame_count}/{total}")
645
+ QTimer.singleShot(100, self._auto_stop_measurement)
646
+
647
+ def _update_center_zoom(self, image):
648
+ if image is None or image.size == 0:
649
+ return
650
+ try:
651
+ h, w = image.shape[:2]
652
+ cw, ch = w // 4, h // 4
653
+ x, y = (w - cw) // 2, (h - ch) // 2
654
+ zoomed = cv2.resize(image[y:y+ch, x:x+cw], (cw * 2, ch * 2), interpolation=cv2.INTER_LINEAR)
655
+ rgb = cv2.cvtColor(zoomed, cv2.COLOR_BGR2RGB) if len(zoomed.shape) == 3 else zoomed
656
+ zh, zw = rgb.shape[:2]
657
+ bpl = zw * (3 if len(rgb.shape) == 3 else 1)
658
+ fmt = QImage.Format_RGB888 if len(rgb.shape) == 3 else QImage.Format_Grayscale8
659
+ pixmap = QPixmap.fromImage(QImage(rgb.data, zw, zh, bpl, fmt))
660
+ self.center_zoom_view.setPixmap(pixmap.scaled(self.center_zoom_view.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
661
+ except Exception as e:
662
+ log.error(f"[CameraTab] 중심 확대 실패: {e}")
663
+
664
+ # =========================================================================
665
+ # UI 이벤트 핸들러
666
+ # =========================================================================
667
+ def _on_cut3_toggle_changed(self, checked: bool):
668
+ value = 3 if checked else 1
669
+ GlobalConfig.instance().setConfigValue("core.image_per_info", value)
670
+ log.info(f"[CameraTab] 3점촬영: {value}")
671
+
672
+ def _on_expose_changed(self, value: int):
673
+ self.expose_value_label.setText(f"{value:,}")
674
+ if self.is_connected:
675
+ self.camera.set_exposure(value)
676
+
677
+ # =========================================================================
678
+ # 유틸리티
679
+ # =========================================================================
680
+ def _create_session_path(self):
681
+ if not self.current_path:
682
+ return None
683
+ try:
684
+ path_parts = self.current_path.replace('\\', '/').split('/')
685
+ if 'input' in path_parts:
686
+ path_parts[path_parts.index('input')] = 'data'
687
+ data_path = '/'.join(path_parts)
688
+ else:
689
+ data_path = os.path.join(self.current_path, 'data')
690
+
691
+ os.makedirs(data_path, exist_ok=True)
692
+ session_name = datetime.now().strftime('%Y%m%d_%H%M%S')
693
+ session_path = os.path.join(data_path, session_name)
694
+ os.makedirs(session_path, exist_ok=True)
695
+ log.info(f"[CameraTab] 세션 경로: {session_path}")
696
+ return session_path
697
+ except Exception as e:
698
+ log.error(f"[CameraTab] 세션 경로 생성 실패: {e}")
699
+ return None
700
+
701
+ def _update_elapsed_time(self):
702
+ if not self.measurement_start_time or not self.is_measuring:
703
+ return
704
+ elapsed = datetime.now() - self.measurement_start_time
705
+ total_sec = int(elapsed.total_seconds())
706
+ h, m, s = total_sec // 3600, (total_sec % 3600) // 60, total_sec % 60
707
+ self.elapsed_time_label.setText(f"경과시간: {h:02d}:{m:02d}:{s:02d}")
708
+
709
+ def _set_measure_button_style(self, state: str):
710
+ color, hover, pressed = BUTTON_STYLES.get(state, BUTTON_STYLES['ready'])
711
+ self.measure_button.setStyleSheet(f"""
712
+ QPushButton {{ background-color: {color}; color: white; font-weight: bold; padding: 8px 20px; border-radius: 5px; }}
713
+ QPushButton:hover {{ background-color: {hover}; }}
714
+ QPushButton:pressed {{ background-color: {pressed}; }}
715
+ """)
716
+
717
+ def _set_exposure_visible(self, visible: bool):
718
+ self.expose_label.setVisible(visible)
719
+ self.expose_slider.setVisible(visible)
720
+ self.expose_slider.setEnabled(visible)
721
+ self.expose_value_label.setVisible(visible)
722
+
723
+ def _update_controls_enabled(self, enabled: bool):
724
+ self.expose_slider.setEnabled(enabled)
725
+ if not enabled:
726
+ self.frame_count = 0
727
+ self.frame_count_label.setText("Frame: 0")
728
+ self.camera_view.set_image(None)
729
+ self.camera_view.display_text = "카메라 연결 후 이미지가 표시됩니다"
730
+ self.camera_view.update()