Spaces:
Running on Zero
Running on Zero
Download app/region_selector.py from hoangtaiii/DRIPPY4: direct link, hf CLI and curl.
- Browser
- Download file 17.5 kB
-
https://huggingface.co/spaces/hoangtaiii/DRIPPY4/resolve/main/app/region_selector.py
- Command line
-
hf download hf://spaces/hoangtaiii/DRIPPY4/app/region_selector.py
-
curl -L -o region_selector.py https://huggingface.co/spaces/hoangtaiii/DRIPPY4/resolve/main/app/region_selector.py
17.5 kB
| """ | |
| RegionSelectorDialog β Dialog chα»n vΓΉng che sub vα»i scrubber toΓ n video. | |
| NΓ’ng cαΊ₯p so vα»i bαΊ£n cΕ©: | |
| - Thanh scrubber QSlider trượt toΓ n bα» video Δα» kiα»m tra vΓΉng che α» mα»i frame | |
| - NΓΊt Play/Pause Δα» xem tα»± Δα»ng (25fps qua QTimer + seek, khΓ΄ng decode realtime) | |
| - PhΓm tαΊ―t: Space=Play/Dα»«ng, β=LΓΉi frame, β=TiαΊΏn frame, Enter=LΖ°u | |
| - Overlay: vΓΉng ΔΓ£ lΖ°u fill Δα» mα» + label "β VΓΉng che"; Δang vαΊ½ fill vΓ ng nΓ©t Δα»©t | |
| - Giα»― VideoCapture mα» xuyΓͺn suα»t dialog β seek nhanh; release khi ΔΓ³ng | |
| Backward compat: API giα»ng bαΊ£n cΕ© (constructor, self.region, accept/reject). | |
| """ | |
| import cv2 | |
| from PyQt6.QtWidgets import ( | |
| QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSlider, QMessageBox | |
| ) | |
| from PyQt6.QtCore import Qt, QTimer | |
| from PyQt6.QtGui import QImage, QPixmap, QPainter, QPen, QColor | |
| class RegionSelectorDialog(QDialog): | |
| """ | |
| Dialog chα»n vΓΉng che sub. | |
| KαΊΏt quαΊ£: | |
| self.region = (x, y, w, h, orig_video_w, orig_video_h) β pixel gα»c video | |
| self.region = None nαΊΏu chΖ°a vαΊ½ hoαΊ·c bα» qua | |
| """ | |
| def __init__( | |
| self, | |
| video_path, | |
| parent=None, | |
| is_batch: bool = False, | |
| current_idx: int = 1, | |
| total_videos: int = 1, | |
| ): | |
| super().__init__(parent) | |
| if is_batch: | |
| self.setWindowTitle( | |
| f"[{current_idx}/{total_videos}] KΓ©o thαΊ£ chα»n vΓΉng che Sub " | |
| "β Trượt / PhΓ‘t Δα» kiα»m tra toΓ n video" | |
| ) | |
| else: | |
| self.setWindowTitle( | |
| "KΓ©o thαΊ£ chuα»t Δα» chα»n vΓΉng che Sub " | |
| "β Trượt / PhΓ‘t Δα» kiα»m tra toΓ n video (X Δα» lΖ°u)" | |
| ) | |
| self.setModal(True) | |
| self.video_path = str(video_path) | |
| self.region = None | |
| # ββ Mα» VideoCapture (giα»― mα» suα»t dialog) ββββββββββββββββββββββββ | |
| self._cap = cv2.VideoCapture(self.video_path) | |
| if not self._cap.isOpened(): | |
| QMessageBox.critical(self, "Lα»i", f"KhΓ΄ng mα» Δược video:\n{self.video_path}") | |
| super().reject() | |
| return | |
| self._total_frames = max(1, int(self._cap.get(cv2.CAP_PROP_FRAME_COUNT))) | |
| self._fps = self._cap.get(cv2.CAP_PROP_FPS) or 25.0 | |
| self.orig_w = int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
| self.orig_h = int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
| # Load frame giα»―a video | |
| mid = self._total_frames // 2 | |
| frame = self._read_frame(mid) | |
| if frame is None: | |
| QMessageBox.critical(self, "Lα»i", "KhΓ΄ng thα» Δα»c khung hΓ¬nh video!") | |
| self._cap.release() | |
| super().reject() | |
| return | |
| # ββ Display state ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| self.MAX_W = 1280 | |
| self.MAX_H = 720 | |
| self._current_frame_idx = mid | |
| self.disp_w = 1 | |
| self.disp_h = 1 | |
| self.scale_x = 1.0 | |
| self.scale_y = 1.0 | |
| # ββ Drawing state ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| self.start_point = None | |
| self.end_point = None | |
| self.drawing = False | |
| # ββ Widgets ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| self.label = QLabel() | |
| self.label.setAlignment(Qt.AlignmentFlag.AlignCenter) | |
| self.label.setCursor(Qt.CursorShape.CrossCursor) | |
| self.label.mousePressEvent = self.mouse_press | |
| self.label.mouseMoveEvent = self.mouse_move | |
| self.label.mouseReleaseEvent = self.mouse_release | |
| self.label.paintEvent = self.paint_label | |
| hint = QLabel( | |
| "π’ KΓ©o chuα»t vαΊ½ vΓΉng che (hΓ¬nh chα»― nhαΊt Δα»). " | |
| "βΈ Trượt thanh Δα» kiα»m tra mα»i frame. " | |
| "βΆ NΓΊt PhΓ‘t xem tα»± Δα»ng. " | |
| "[Space]=Play/Dα»«ng [ββ]=LΓΉi/TiαΊΏn frame [Enter]=LΖ°u" | |
| ) | |
| hint.setWordWrap(True) | |
| hint.setStyleSheet("font-size:12px; padding:4px; color:#ccc; background:#1a1a1e;") | |
| # ββ Scrubber row βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| self._playing = False | |
| self.btn_play = QPushButton("βΆ PhΓ‘t") | |
| self.btn_play.setFixedWidth(100) | |
| self._style_play_btn(playing=False) | |
| self.btn_play.clicked.connect(self._toggle_play) | |
| self.lbl_time = QLabel("00:00:00") | |
| self.lbl_time.setFixedWidth(72) | |
| self.lbl_time.setAlignment(Qt.AlignmentFlag.AlignCenter) | |
| self.lbl_time.setStyleSheet("font-family:monospace; font-size:12px; color:#aaa;") | |
| self.slider = QSlider(Qt.Orientation.Horizontal) | |
| self.slider.setMinimum(0) | |
| self.slider.setMaximum(self._total_frames - 1) | |
| self.slider.setValue(mid) | |
| self.slider.setStyleSheet(""" | |
| QSlider::groove:horizontal { | |
| height: 6px; background: #3a3a3a; border-radius: 3px; | |
| } | |
| QSlider::handle:horizontal { | |
| background: #e74c3c; border: none; | |
| width: 14px; height: 14px; margin: -4px 0; border-radius: 7px; | |
| } | |
| QSlider::sub-page:horizontal { | |
| background: #c0392b; border-radius: 3px; | |
| } | |
| """) | |
| self.slider.valueChanged.connect(self._on_slider_changed) | |
| self.slider.sliderPressed.connect(self._pause) | |
| total_sec = self._total_frames / self._fps | |
| mm, ss = divmod(int(total_sec), 60) | |
| hh, mm = divmod(mm, 60) | |
| lbl_total = QLabel(f"{hh:02d}:{mm:02d}:{ss:02d}") | |
| lbl_total.setAlignment(Qt.AlignmentFlag.AlignCenter) | |
| lbl_total.setStyleSheet("font-family:monospace; font-size:12px; color:#555;") | |
| lbl_total.setFixedWidth(72) | |
| scrub_row = QHBoxLayout() | |
| scrub_row.addWidget(self.btn_play) | |
| scrub_row.addWidget(self.lbl_time) | |
| scrub_row.addWidget(self.slider, stretch=1) | |
| scrub_row.addWidget(lbl_total) | |
| # ββ Play timer ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| self._timer = QTimer(self) | |
| self._timer.setInterval(int(1000 / min(self._fps, 25))) | |
| self._timer.timeout.connect(self._advance_frame) | |
| # ββ Main layout βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| layout = QVBoxLayout(self) | |
| layout.setSpacing(6) | |
| layout.addWidget(hint) | |
| layout.addWidget(self.label) | |
| layout.addLayout(scrub_row) | |
| # ββ Batch mode buttons ββββββββββββββββββββββββββββββββββββββββββββ | |
| self.is_batch = is_batch | |
| if is_batch: | |
| btn_layout = QHBoxLayout() | |
| self.btn_skip = QPushButton("βοΈ Bα» qua (KhΓ΄ng che)") | |
| self.btn_skip.setStyleSheet( | |
| "background-color:#7F8C8D; color:white; padding:10px; " | |
| "font-weight:bold; font-size:14px;" | |
| ) | |
| self.btn_skip.clicked.connect(self._reject_and_stop) | |
| label_next = "β OK, Video cuα»i!" if current_idx == total_videos else "β OK, ΔΓ£ che xong β Next" | |
| self.btn_next = QPushButton(label_next) | |
| self.btn_next.setStyleSheet( | |
| "background-color:#27AE60; color:white; padding:10px; " | |
| "font-weight:bold; font-size:14px;" | |
| ) | |
| self.btn_next.clicked.connect(self._accept_and_stop) | |
| btn_layout.addWidget(self.btn_skip) | |
| btn_layout.addWidget(self.btn_next) | |
| layout.addLayout(btn_layout) | |
| # Load frame ΔαΊ§u tiΓͺn | |
| self._set_frame(mid) | |
| # ββ Video helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _read_frame(self, frame_idx: int): | |
| """Seek vΓ Δα»c frame, trαΊ£ vα» numpy RGB hoαΊ·c None.""" | |
| self._cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx) | |
| ret, frame = self._cap.read() | |
| if not ret or frame is None: | |
| return None | |
| return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) | |
| def _set_frame(self, frame_idx: int): | |
| """Seek ΔαΊΏn frame_idx, cαΊp nhαΊt label + slider + timestamp.""" | |
| frame_idx = max(0, min(frame_idx, self._total_frames - 1)) | |
| self._current_frame_idx = frame_idx | |
| frame = self._read_frame(frame_idx) | |
| if frame is None: | |
| return | |
| h, w, ch = frame.shape | |
| scaled = QPixmap.fromImage( | |
| QImage(frame.data, w, h, ch * w, QImage.Format.Format_RGB888) | |
| ).scaled( | |
| self.MAX_W, self.MAX_H, | |
| Qt.AspectRatioMode.KeepAspectRatio, | |
| Qt.TransformationMode.SmoothTransformation, | |
| ) | |
| self.disp_w = scaled.width() | |
| self.disp_h = scaled.height() | |
| self.scale_x = self.orig_w / self.disp_w | |
| self.scale_y = self.orig_h / self.disp_h | |
| self.label.setPixmap(scaled) | |
| self.label.setFixedSize(self.disp_w, self.disp_h) | |
| self.label.update() | |
| self.slider.blockSignals(True) | |
| self.slider.setValue(frame_idx) | |
| self.slider.blockSignals(False) | |
| sec = frame_idx / self._fps | |
| mm, ss = divmod(int(sec), 60) | |
| hh, mm = divmod(mm, 60) | |
| self.lbl_time.setText(f"{hh:02d}:{mm:02d}:{ss:02d}") | |
| # ββ Playback ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _style_play_btn(self, playing: bool): | |
| if playing: | |
| self.btn_play.setText("βΈ TαΊ‘m dα»«ng") | |
| self.btn_play.setStyleSheet( | |
| "background:#e67e22; color:#000; font-weight:bold; border-radius:4px; padding:4px;" | |
| ) | |
| else: | |
| self.btn_play.setText("βΆ PhΓ‘t") | |
| self.btn_play.setStyleSheet( | |
| "background:#2ecc71; color:#000; font-weight:bold; border-radius:4px; padding:4px;" | |
| ) | |
| def _on_slider_changed(self, value: int): | |
| self._set_frame(value) | |
| def _toggle_play(self): | |
| if self._playing: | |
| self._pause() | |
| else: | |
| self._play() | |
| def _play(self): | |
| self._playing = True | |
| self._style_play_btn(playing=True) | |
| self._timer.start() | |
| def _pause(self): | |
| self._playing = False | |
| self._timer.stop() | |
| self._style_play_btn(playing=False) | |
| def _advance_frame(self): | |
| """Timer tick: tiαΊΏn 1 frame, loop khi hαΊΏt.""" | |
| nxt = self._current_frame_idx + 1 | |
| if nxt >= self._total_frames: | |
| nxt = 0 | |
| self._set_frame(nxt) | |
| # ββ Coordinate normalization ββββββββββββββββββββββββββββββββββββββββββ | |
| def _display_geometry(self): | |
| """Return (off_x, off_y, disp_w, disp_h) β where the video pixmap actually | |
| sits inside the QLabel, accounting for any centered letterbox/pillarbox | |
| margin so mouse coordinates are never measured against the black bars.""" | |
| label_w = self.label.width() | |
| label_h = self.label.height() | |
| off_x = max(0, (label_w - self.disp_w) // 2) | |
| off_y = max(0, (label_h - self.disp_h) // 2) | |
| return off_x, off_y, self.disp_w, self.disp_h | |
| def _ui_to_native(self, ux, uy): | |
| """Convert UI (label-local) coords -> normalized (0.0..1.0) coords -> | |
| native video frame coords. Logs the full chain for debugging: | |
| [UI Cords] -> [Normalized Cords] -> [Native Frame Cords].""" | |
| off_x, off_y, dw, dh = self._display_geometry() | |
| px = max(0.0, min(float(ux - off_x), float(dw))) | |
| py = max(0.0, min(float(uy - off_y), float(dh))) | |
| norm_x = px / dw if dw > 0 else 0.0 | |
| norm_y = py / dh if dh > 0 else 0.0 | |
| real_x = int(round(norm_x * self.orig_w)) | |
| real_y = int(round(norm_y * self.orig_h)) | |
| print( | |
| f"[UI Cords] ({ux:.1f}, {uy:.1f}) -> " | |
| f"[Normalized Cords] ({norm_x:.4f}, {norm_y:.4f}) -> " | |
| f"[Native Frame Cords] ({real_x}, {real_y})" | |
| ) | |
| return real_x, real_y | |
| def _native_to_display(self, nx, ny): | |
| """Convert native frame coords back to label-local display coords.""" | |
| off_x, off_y, dw, dh = self._display_geometry() | |
| dx = off_x + int(round(nx / self.orig_w * dw)) if self.orig_w else off_x | |
| dy = off_y + int(round(ny / self.orig_h * dh)) if self.orig_h else off_y | |
| return dx, dy | |
| # ββ Drawing βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def mouse_press(self, event): | |
| self._pause() | |
| self.start_point = event.position().toPoint() | |
| self.end_point = self.start_point | |
| self.drawing = True | |
| self.label.update() | |
| def mouse_move(self, event): | |
| if self.drawing: | |
| self.end_point = event.position().toPoint() | |
| self.label.update() | |
| def mouse_release(self, event): | |
| self.end_point = event.position().toPoint() | |
| self.drawing = False | |
| self.label.update() | |
| off_x, off_y, dw, dh = self._display_geometry() | |
| x1 = max(off_x, min(min(self.start_point.x(), self.end_point.x()), off_x + dw)) | |
| y1 = max(off_y, min(min(self.start_point.y(), self.end_point.y()), off_y + dh)) | |
| x2 = max(off_x, min(max(self.start_point.x(), self.end_point.x()), off_x + dw)) | |
| y2 = max(off_y, min(max(self.start_point.y(), self.end_point.y()), off_y + dh)) | |
| orig_x, orig_y = self._ui_to_native(x1, y1) | |
| rx2, ry2 = self._ui_to_native(x2, y2) | |
| orig_rw = max(1, rx2 - orig_x) | |
| orig_rh = max(1, ry2 - orig_y) | |
| if orig_rw > 0 and orig_rh > 0: | |
| self.region = (orig_x, orig_y, orig_rw, orig_rh, self.orig_w, self.orig_h) | |
| print( | |
| f"[REGION] stored native region: x={orig_x}, y={orig_y}, " | |
| f"w={orig_rw}, h={orig_rh} (frame {self.orig_w}x{self.orig_h})" | |
| ) | |
| def keyPressEvent(self, event): | |
| k = event.key() | |
| if k in (Qt.Key.Key_Return, Qt.Key.Key_Enter): | |
| self._accept_and_stop() | |
| elif k == Qt.Key.Key_Space: | |
| self._toggle_play() | |
| elif k == Qt.Key.Key_Left: | |
| self._pause() | |
| self._set_frame(self._current_frame_idx - 1) | |
| elif k == Qt.Key.Key_Right: | |
| self._pause() | |
| self._set_frame(self._current_frame_idx + 1) | |
| else: | |
| super().keyPressEvent(event) | |
| def paint_label(self, event): | |
| """Overlay: vΓΉng ΔΓ£ lΖ°u (Δα» mα») + Δang vαΊ½ (vΓ ng nΓ©t Δα»©t).""" | |
| QLabel.paintEvent(self.label, event) | |
| painter = QPainter(self.label) | |
| _off_x, _off_y, _dw, _dh = self._display_geometry() | |
| # VΓΉng ΔΓ£ lΖ°u | |
| if self.region: | |
| rx, ry, rw, rh, _vw, _vh = self.region | |
| dx, dy = self._native_to_display(rx, ry) | |
| dx2, dy2 = self._native_to_display(rx + rw, ry + rh) | |
| dw = dx2 - dx | |
| dh = dy2 - dy | |
| painter.fillRect(dx, dy, dw, dh, QColor(220, 0, 0, 70)) | |
| painter.setPen(QPen(QColor(255, 60, 60), 2, Qt.PenStyle.SolidLine)) | |
| painter.drawRect(dx, dy, dw, dh) | |
| painter.setPen(QColor(255, 220, 0)) | |
| painter.drawText(dx + 4, dy + 15, "β VΓΉng che") | |
| # Δang vαΊ½ | |
| if self.start_point and self.end_point and self.drawing: | |
| x1 = min(self.start_point.x(), self.end_point.x()) | |
| y1 = min(self.start_point.y(), self.end_point.y()) | |
| w = abs(self.start_point.x() - self.end_point.x()) | |
| h = abs(self.start_point.y() - self.end_point.y()) | |
| painter.fillRect(x1, y1, w, h, QColor(255, 255, 0, 45)) | |
| painter.setPen(QPen(QColor(255, 230, 0), 1, Qt.PenStyle.DashLine)) | |
| painter.drawRect(x1, y1, w, h) | |
| painter.end() | |
| # ββ Lifecycle βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _release(self): | |
| self._pause() | |
| if hasattr(self, "_cap") and self._cap.isOpened(): | |
| self._cap.release() | |
| def _accept_and_stop(self): | |
| self._release() | |
| super().accept() | |
| def _reject_and_stop(self): | |
| self._release() | |
| super().reject() | |
| def closeEvent(self, event): | |
| self._release() | |
| super().closeEvent(event) | |
| def accept(self): | |
| self._release() | |
| super().accept() | |
| def reject(self): | |
| self._release() | |
| super().reject() | |