Spaces:
Running
Running
stouchinglhh
collector: ๅๆญฅ้้ๅทฅ็จ v1.2.1๏ผๅ
จ้จ LFS ๆจกๅผๆไปถๆ้ๅ๏ผไธ Space ็ฐๆๆไปถๅๅนถ๏ผ
d646dd7 Download collector/ui/camera_widget.py from Stouching-Robotics/DataFlowing: direct link, hf CLI and curl.
- Browser
- Download file 15.5 kB
-
https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/collector/ui/camera_widget.py
- Command line
-
hf download hf://spaces/Stouching-Robotics/DataFlowing/collector/ui/camera_widget.py
-
curl -L -o camera_widget.py https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/collector/ui/camera_widget.py
15.5 kB
| """ | |
| ๆๅๆบ็ป้ขๆงไปถ โโ ๆพ็คบไธ่ทฏๆๅๆบ็ๅฎๆถ็ป้ขๅๅฝๅถๆงๅถๆ้ฎใ | |
| ็ป้ขๆฏๆ้ผ ๆ ๆไฝ: | |
| - ๆป่ฝฎ็ผฉๆพ๏ผไปฅ็ป้ขไธญๅฟไธบๅบๅ๏ผ | |
| - ๅทฆ้ฎๆๆฝๅนณ็งป | |
| - ๅๅป่ฟๅ๏ผ้็ฝฎ็ผฉๆพๅๅนณ็งป๏ผ | |
| """ | |
| from __future__ import annotations | |
| from typing import Optional | |
| import cv2 | |
| import numpy as np | |
| from PyQt5.QtCore import Qt, pyqtSignal, QPointF, QRectF | |
| from PyQt5.QtGui import ( | |
| QImage, QPixmap, QFont, QColor, QPainter, | |
| ) | |
| from PyQt5.QtWidgets import ( | |
| QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, | |
| QFrame, QSizePolicy, QToolButton, | |
| ) | |
| from config import settings | |
| from config.i18n import tr, lang_manager | |
| from core.helpers import format_duration | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ๅฏ็ผฉๆพๆๆฝ็่ง้ขๆพ็คบๆงไปถ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class ZoomableVideoWidget(QWidget): | |
| """ | |
| ๆฏๆ้ผ ๆ ไบคไบ็่ง้ข็ป้ขๆงไปถใ | |
| ๆไฝๆนๅผ: | |
| - ๆป่ฝฎ โ ไปฅ็ป้ขไธญๅฟไธบๅบๅ็ผฉๆพ๏ผ1.1ร / 0.9ร ๆฏ็บง๏ผ | |
| - ๆๆฝ โ ๅนณ็งป็ป้ข | |
| - ๅๅป โ ่ฟๅๅฐ้ๅบ็ชๅฃๅคงๅฐ | |
| ็ผฉๆพ่ๅด: 0.1ร ~ 10ร | |
| """ | |
| ZOOM_MIN = 0.25 | |
| ZOOM_MAX = 8.0 | |
| ZOOM_STEP = 1.08 # ๆฏ็บงๆป่ฝฎๅ็๏ผๆดๅนณๆป๏ผ | |
| def __init__(self, parent: QWidget = None): | |
| super().__init__(parent) | |
| self._pixmap: Optional[QPixmap] = None | |
| self._zoom = 1.0 | |
| self._offset = QPointF(0, 0) | |
| self._dragging = False | |
| self._last_mouse_pos = QPointF() | |
| self._status_text = tr("ๆ ไฟกๅท") | |
| self._has_frame = False | |
| # ็ผฉๆพ็ต้ๆ็คบ๏ผๅณไธ่ง็ญๆๆพ็คบ๏ผ | |
| self._show_zoom_hint = False | |
| self._zoom_hint_timer = 0 | |
| self.setMouseTracking(True) | |
| self.setCursor(Qt.OpenHandCursor) | |
| self.setMinimumSize(160, 120) | |
| self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) | |
| self.setStyleSheet("background:#000000; border:none;") | |
| # โโ ๅ ฌๅผๆฅๅฃ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def reset_view(self): | |
| """Reset zoom and pan to default.""" | |
| self._zoom = 1.0 | |
| self._offset = QPointF(0, 0) | |
| self.update() | |
| def set_frame(self, bgr_frame: np.ndarray, flip_vertical: bool = False): | |
| """ไผ ๅ ฅ BGR ๅธงๅนถ่งฆๅ้็ปใ | |
| flip_vertical: ๆฏๅฆไธไธ็ฟป่ฝฌ็ป้ข๏ผๆๅๆบ่ง้ข้่ฆ๏ผไผ ๆๅจๆธฒๆไธ้่ฆ๏ผใ | |
| """ | |
| try: | |
| if flip_vertical: | |
| bgr_frame = cv2.flip(bgr_frame, 0) | |
| # BGR โ RGB ๅ็จ Format_RGB888๏ผFormat_BGR888 ๅจ Qt x86 ไธ็บข่ไบๆข๏ผ | |
| rgb_frame = cv2.cvtColor(bgr_frame, cv2.COLOR_BGR2RGB) | |
| h, w, ch = rgb_frame.shape | |
| qimg = QImage(rgb_frame.data, w, h, w * ch, QImage.Format_RGB888) | |
| self._pixmap = QPixmap.fromImage(qimg) | |
| self._has_frame = True | |
| except Exception: | |
| pass | |
| if self._show_zoom_hint: | |
| self._zoom_hint_timer -= 1 | |
| if self._zoom_hint_timer <= 0: | |
| self._show_zoom_hint = False | |
| self.update() | |
| def set_status_text(self, text: str): | |
| """่ฎพ็ฝฎๆ ็ป้ขๆถๆพ็คบ็ๆๅญใ""" | |
| self._status_text = text | |
| self._has_frame = False | |
| self._pixmap = None | |
| self.update() | |
| def reset_view(self): | |
| """่ฟๅ็ผฉๆพๅๅนณ็งปๅฐๅๅง็ถๆใ""" | |
| self._zoom = 1.0 | |
| self._offset = QPointF(0, 0) | |
| self.update() | |
| # โโ ็ปๅถ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def paintEvent(self, _event): | |
| painter = QPainter(self) | |
| painter.setRenderHint(QPainter.Antialiasing) | |
| painter.setRenderHint(QPainter.SmoothPixmapTransform) | |
| ww = self.width() | |
| wh = self.height() | |
| if self._pixmap is None or not self._has_frame: | |
| painter.fillRect(self.rect(), Qt.black) | |
| painter.setPen(QColor("#888888")) | |
| painter.setFont(QFont("Microsoft YaHei", 12)) | |
| painter.drawText(self.rect(), Qt.AlignCenter, self._status_text) | |
| return | |
| pw = self._pixmap.width() | |
| ph = self._pixmap.height() | |
| base_scale = min(ww / pw, wh / ph) if pw > 0 and ph > 0 else 1.0 | |
| scale = base_scale * self._zoom | |
| sw = pw * scale | |
| sh = ph * scale | |
| cx = (ww - sw) / 2.0 + self._offset.x() | |
| cy = (wh - sh) / 2.0 + self._offset.y() | |
| target = QRectF(cx, cy, sw, sh) | |
| source = QRectF(0, 0, pw, ph) | |
| painter.drawPixmap(target, self._pixmap, source) | |
| # ็ญๆ็็ผฉๆพ็พๅๆฏๆ็คบ๏ผๅณไธ่ง๏ผ | |
| if self._show_zoom_hint: | |
| pct = int(self._zoom * 100) | |
| hint = f"{pct}%" | |
| painter.setPen(Qt.NoPen) | |
| painter.setBrush(QColor(0, 0, 0, 160)) | |
| fm = painter.fontMetrics() | |
| tw = fm.horizontalAdvance(hint) + 16 | |
| th = fm.height() + 8 | |
| painter.drawRoundedRect( | |
| QRectF(ww - tw - 12, wh - th - 12, tw, th), 6, 6 | |
| ) | |
| painter.setPen(QColor("#FFFFFF")) | |
| painter.setFont(QFont("Segoe UI", 11, QFont.Bold)) | |
| painter.drawText( | |
| QRectF(ww - tw - 12, wh - th - 12, tw, th), | |
| Qt.AlignCenter, hint | |
| ) | |
| # โโ ้ผ ๆ ไบไปถ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def wheelEvent(self, event): | |
| """ๆป่ฝฎ็ผฉๆพ โโ ไปฅ้ผ ๆ ไฝ็ฝฎไธบๅบๅ็น็ผฉๆพใ""" | |
| if self._pixmap is None or not self._has_frame: | |
| return | |
| mouse_pos = QPointF(event.pos()) | |
| old_zoom = self._zoom | |
| if event.angleDelta().y() > 0: | |
| self._zoom = min(self.ZOOM_MAX, self._zoom * self.ZOOM_STEP) | |
| else: | |
| self._zoom = max(self.ZOOM_MIN, self._zoom / self.ZOOM_STEP) | |
| if abs(self._zoom - old_zoom) < 1e-6: | |
| return | |
| # ไปฅ้ผ ๆ ไฝ็ฝฎไธบๅบๅ่ฐๆดๅ็งป | |
| ratio = self._zoom / old_zoom | |
| self._offset = mouse_pos - ratio * (mouse_pos - self._offset) | |
| # ๆพ็คบ็ผฉๆพ็พๅๆฏๆ็คบ๏ผๆ็ปญ็บฆ 1 ็ง๏ผ | |
| self._show_zoom_hint = True | |
| self._zoom_hint_timer = 30 # ~30 ๅธง โ 1 ็ง | |
| self.update() | |
| def mousePressEvent(self, event): | |
| if event.button() == Qt.LeftButton: | |
| self._dragging = True | |
| self._last_mouse_pos = QPointF(event.pos()) | |
| self.setCursor(Qt.ClosedHandCursor) | |
| event.accept() | |
| def mouseMoveEvent(self, event): | |
| if self._dragging: | |
| delta = QPointF(event.pos()) - self._last_mouse_pos | |
| self._offset += delta | |
| self._last_mouse_pos = QPointF(event.pos()) | |
| self.update() | |
| event.accept() | |
| def mouseReleaseEvent(self, event): | |
| if event.button() == Qt.LeftButton and self._dragging: | |
| self._dragging = False | |
| self.setCursor(Qt.OpenHandCursor) | |
| event.accept() | |
| def mouseDoubleClickEvent(self, event): | |
| """ๅๅป่ฟๅ็ผฉๆพๅๅนณ็งปใ""" | |
| if event.button() == Qt.LeftButton: | |
| self.reset_view() | |
| event.accept() | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ๆๅๆบ้ขๆฟ๏ผ็ปๅ่ง้ขๆงไปถ + ๅฝๅถๆงๅถๆ ๏ผ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class CameraWidget(QFrame): | |
| """ | |
| ๅ่ทฏๆๅๆบ้ขๆฟ๏ผ่ง้ข็ป้ข + ็ถๆ่ฆ็ๆกใ | |
| ไฟกๅท | |
| ---- | |
| remove_requested(str) โ ่ฏทๆฑ็งป้คๆญคๆๅๆบ | |
| exposure_clicked(str) โ ็นๅปๆๅ ๆ้ฎ๏ผไธป็ชๅฃๆๆงฝไฝๆๅฑ่ฎพๅคๅผนๅฏน่ฏๆก๏ผ | |
| """ | |
| remove_requested = pyqtSignal(str) | |
| exposure_clicked = pyqtSignal(str) | |
| def __init__(self, slot_id: str, camera_name: str = "", | |
| parent: QWidget = None): | |
| super().__init__(parent) | |
| self.slot_id = slot_id | |
| self.camera_name = camera_name or f"Camera {slot_id}" | |
| self._fps = 0.0 | |
| self._setup_ui() | |
| # ็ๅฌ่ฏญ่จๅๆข | |
| lang_manager.language_changed.connect(self._refresh_texts) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # UI ๆๅปบ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _setup_ui(self): | |
| self.setMinimumSize(settings.FEED_MIN_WIDTH, settings.FEED_MIN_HEIGHT) | |
| self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) | |
| self.setStyleSheet(f"background:{settings.COLOR_BG_WIDGET}; " | |
| f"border:1px solid {settings.COLOR_BORDER};") | |
| main_layout = QVBoxLayout(self) | |
| main_layout.setContentsMargins(2, 2, 2, 2) | |
| main_layout.setSpacing(0) | |
| # โโ ่ง้ขๆพ็คบๅบๅ๏ผๅฏ็ผฉๆพๆๆฝ๏ผ โโโโโโโโโโโโโโโโ | |
| self.video_widget = ZoomableVideoWidget() | |
| main_layout.addWidget(self.video_widget, 1) | |
| # โโ ่ฆ็ๆก๏ผ่ง้ข็ป้ข้กถ้จ๏ผๅง็ปๅบๅฎๅจๅทฆไธ่ง๏ผ โโโ | |
| self.overlay = QFrame(self.video_widget) | |
| self.overlay.setStyleSheet( | |
| "background:rgba(0,0,0,160); border-radius:4px;" | |
| ) | |
| self.overlay.setFixedHeight(28) | |
| # ๆณจๆ๏ผ่ฆ็ๆก็ปไธ่ฝ่ฎพ WA_TransparentForMouseEventsโโQt ่ฏญไนๆฏ | |
| # ใ่ฏฅๆงไปถๅๅ ถๅ จ้จๅญๆงไปถไธๆฅๆถ้ผ ๆ ใ๏ผไผ่ฟ โ ๆ้ฎไธ่ตทๅฑ่ฝใ | |
| # ไฟกๆฏๆกๆๆฝ็ฑ CameraGrid ็ไบไปถ่ฟๆปคๅจ็ปไธๆฆๆช๏ผๆ ้็ฉฟ้ใ | |
| overlay_layout = QHBoxLayout(self.overlay) | |
| overlay_layout.setContentsMargins(8, 0, 8, 0) | |
| overlay_layout.setSpacing(12) | |
| self.name_label = QLabel(self.camera_name) | |
| self.name_label.setStyleSheet( | |
| "color:#FFFFFF; font-weight:bold; font-size:11px; background:transparent;" | |
| ) | |
| overlay_layout.addWidget(self.name_label) | |
| # โ ๆๅ ๆ้ฎ๏ผ้ป่ฎค้่๏ผไธป็ชๅฃๅชๅฏน่ฎพๅค็"ไธปๆงฝไฝ"ๆพ็คบใ | |
| # _no_drag ๅฑๆง่ฎฉ็ฝๆ ผๆๆฝ่ฟๆปคๅจๆพ่ก๏ผๆไธไธไผ่งฆๅ็ป้ขๆๆฝ/ๅๅป่ฟๅ๏ผ | |
| self.exposure_btn = QToolButton() | |
| self.exposure_btn.setText("โ") | |
| self.exposure_btn.setFixedSize(24, 22) | |
| self.exposure_btn.setCursor(Qt.PointingHandCursor) | |
| self.exposure_btn.setToolTip(tr("ๆๅ ่ฎพ็ฝฎ")) | |
| self.exposure_btn._no_drag = True | |
| self.exposure_btn.setStyleSheet( | |
| "QToolButton{background:transparent; color:#FFD54F; border:none;" | |
| "font-size:13px; padding:0;}" | |
| "QToolButton:hover{background:rgba(255,255,255,40); border-radius:3px;}" | |
| "QToolButton:disabled{color:#666666;}" | |
| ) | |
| self.exposure_btn.clicked.connect( | |
| lambda: self.exposure_clicked.emit(self.slot_id)) | |
| self.exposure_btn.setVisible(False) | |
| overlay_layout.addWidget(self.exposure_btn) | |
| overlay_layout.addStretch() | |
| self.fps_label = QLabel("FPS: --") | |
| self.fps_label.setStyleSheet( | |
| "color:#AAAAAA; font-size:10px; background:transparent;" | |
| ) | |
| overlay_layout.addWidget(self.fps_label) | |
| self.state_dot = QLabel("โ") | |
| self.state_dot.setStyleSheet( | |
| f"color:{settings.COLOR_STOPPED}; font-size:10px; background:transparent;" | |
| ) | |
| overlay_layout.addWidget(self.state_dot) | |
| self.overlay.move(4, 4) | |
| self.overlay.setFixedWidth(260) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ๅ ฌๅผๆฅๅฃ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def update_frame(self, bgr_frame: np.ndarray): | |
| """็จ็ธๆบ้้็ BGR ๅธงๆดๆฐ่ง้ขๆพ็คบ๏ผไธไธ็ฟป่ฝฌ้้ ๆๅๅคดๅฎ่ฃ ๆนๅ๏ผใ""" | |
| self.video_widget.set_frame(bgr_frame, flip_vertical=True) | |
| def update_fps(self, fps: float): | |
| """ๆดๆฐ FPS ๆพ็คบใ""" | |
| self._fps = fps | |
| self.fps_label.setText(f"FPS: {fps:.0f}") | |
| def set_frame_number(self, n: int): | |
| """ๅๆพ็จ๏ผๅจไฟกๆฏๆกๅณไพงๆพ็คบๅฝๅๅธงๅท๏ผๅค็จ FPS ๆ ็ญพไฝ๏ผใ""" | |
| self.fps_label.setText(f"#{n}") | |
| def set_camera_state(self, state: str): | |
| """ๆ นๆฎ็ธๆบ็ถๆๆดๆฐๆ็คบ็ฏ้ข่ฒๅ่ง้ขๅบๅๆๅญใ""" | |
| if state == "recording": | |
| self.state_dot.setStyleSheet( | |
| f"color:{settings.COLOR_RECORDING}; font-size:10px; background:transparent;" | |
| ) | |
| elif state == "error": | |
| self.state_dot.setStyleSheet( | |
| f"color:{settings.COLOR_ABNORMAL}; font-size:10px; background:transparent;" | |
| ) | |
| elif state == "disconnected": | |
| self.state_dot.setStyleSheet("color:#BDBDBD; font-size:10px; background:transparent;") | |
| self.video_widget.set_status_text(tr("ๅทฒๆญๅผ")) | |
| else: | |
| self.state_dot.setStyleSheet( | |
| f"color:{settings.COLOR_STOPPED}; font-size:10px; background:transparent;" | |
| ) | |
| if state == "idle": | |
| self.video_widget.set_status_text(tr("็ญๅพ ไธญโฆ")) | |
| # โโ ๆๅ ๅ ฅๅฃ๏ผโ ๆ้ฎ๏ผ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def set_exposure_button_visible(self, visible: bool): | |
| """ๆพ็คบ/้่ๆๅ ๆ้ฎ๏ผๅชๅฏน่ฎพๅค็"ไธปๆงฝไฝ"ๆพ็คบ๏ผใ""" | |
| self.exposure_btn.setVisible(visible) | |
| def set_exposure_enabled(self, enabled: bool): | |
| """ๅฏ็จ/็ฆ็จๆๅ ๆ้ฎ๏ผๅฝๅถไธญ็ฆ็จ๏ผใ""" | |
| self.exposure_btn.setEnabled(enabled) | |
| self.exposure_btn.setToolTip( | |
| tr("ๆๅ ่ฎพ็ฝฎ") if enabled else tr("ๅฝๅถไธญไธๅฏ่ฐๆดๆๅ ")) | |
| def _refresh_texts(self, _lang: str = ""): | |
| """่ฏญ่จๅๆขๆถๅทๆฐๆๅญใ""" | |
| self.exposure_btn.setToolTip( | |
| tr("ๆๅ ่ฎพ็ฝฎ") if self.exposure_btn.isEnabled() | |
| else tr("ๅฝๅถไธญไธๅฏ่ฐๆดๆๅ ")) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ไบไปถ้ๅ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def resizeEvent(self, event): | |
| """็ชๅฃๅคงๅฐๅๅๆถ่ฐๆด่ฆ็ๆกไฝ็ฝฎๅๅฎฝๅบฆใ""" | |
| super().resizeEvent(event) | |
| self.overlay.setFixedWidth(min(self.video_widget.width() - 8, 300)) | |
| self.overlay.move(4, 4) | |