diff --git a/README.md b/README.md index 9ef2dd9..4516ec7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ # image-analyzer-lab -画像認識及び検知、セクションなどの視覚研究ラボ + +画像認識及び検知、セクションなどの視覚研究ラボ1 ![APP_TOP](docs/images/app01.png) +* 静止画の物体検知 +* + ## Exec ```bat diff --git a/src/app_status.py b/src/app_status.py index 36dad2a..72e829d 100644 --- a/src/app_status.py +++ b/src/app_status.py @@ -90,5 +90,3 @@ class AppStatus(Singleton): @overlay_image.setter def overlay_image(self, img: cv2.typing.MatLike): self.set_status('overlay_image', img) - - diff --git a/src/gui/widgets/media_viewr.py b/src/gui/widgets/media_viewr.py index 6ced6c3..8b527ff 100644 --- a/src/gui/widgets/media_viewr.py +++ b/src/gui/widgets/media_viewr.py @@ -1,7 +1,7 @@ import os import numpy as np import cv2 -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, QTimer from pathlib import Path from PySide6.QtGui import QDragEnterEvent, QDropEvent from PySide6.QtWidgets import ( @@ -33,31 +33,38 @@ class MediaViewer(QWidget): self.btn_play = QPushButton("Play") self.btn_pause = QPushButton("Pause") self.btn_stop = QPushButton("Stop") - self.btn_clear = QPushButton("Clear") - self.btn_analyze = QPushButton("Analyze") self.update_button_state() for b in ( - self.btn_open, self.btn_play, self.btn_pause, self.btn_stop, - self.btn_clear, self.btn_analyze): + self.btn_open, self.btn_play, self.btn_pause, self.btn_stop, + ): self.btn_bar.addWidget(b) self.btn_bar.addStretch() self.btn_open.clicked.connect(self.open_file) - - self.label = QLabel("Drop files here\n(画像/動画)") self.label.setAlignment(Qt.AlignCenter) self.label.setStyleSheet("background:#222;color:#aaa;border:1px solid #444;") self.label.setMinimumSize(640, 360) + lay = QVBoxLayout(self) lay.addLayout(self.btn_bar) lay.addWidget(self.label, 1) + # 動画用 + self.timer = QTimer(self) + self.timer.timeout.connect(self._next_frame) + self.cap = None # cv2.VideoCapture + # self.fps = 33 # (30fps相当) + self.fps = 66 # (30fps相当) + # events - self.btn_analyze.clicked.connect(self.on_analyze_view) + self.btn_play.clicked.connect(self.play) + self.btn_pause.clicked.connect(self.pause) + self.btn_stop.clicked.connect(self.stop) + def load(self, path: str): @@ -85,11 +92,16 @@ class MediaViewer(QWidget): # 画像解析パイプラインを実行 pipeline = ImagePipeline() pipeline.run() - - + self.on_analyze_view() elif suffix in VIDEO_EXT: logger.info(f"Loading video: {path}") self.status.is_video = True + self.cap = cv2.VideoCapture(path) + if not self.cap.isOpened(): + self.status.is_video = False + QMessageBox.critical(self, "Error", f"動画を開けませんでした:\n{path}") + return + self.play() else: logger.warning(f"Unsupported media type: {path}") self.label.setText("未対応の形式です") @@ -106,8 +118,22 @@ class MediaViewer(QWidget): self.label.setPixmap(bgr_to_qpixmap(self.status.overlay_image).scaled( self.label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) - - + # Movie + def _next_frame(self): + """動画の次フレームを表示""" + if self.cap is None: + return + ok, frame = self.cap.read() + if not ok: + self.pause() + return + # self.status.original_image = cv2.imdecode(frame, cv2.IMREAD_COLOR) + self.status.original_image = frame + # 画像解析パイプラインを実行 + pipeline = ImagePipeline() + pipeline.run() + self.label.setPixmap(bgr_to_qpixmap(self.status.overlay_image).scaled( + self.label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) def open_file(self): """ファイル選択ダイアログを開く""" @@ -116,6 +142,27 @@ class MediaViewer(QWidget): if path: self.load(path) + def play(self): + """動画再生""" + if not self.status.is_video: + return + if self.cap is None: + return + self.timer.start(self.fps) + self.update_button_state() + + def pause(self): + self.timer.stop() + self.update_button_state() + + def stop(self): + self.timer.stop() + if self.cap is not None: + self.cap.release() + self.cap = None + self.label.setText("動画を停止しました\n(画像/動画を開いてください)") + self.update_button_state() + def update_button_state(self): """ボタンの有効/無効を更新""" if not self.status.current_media_path: @@ -124,21 +171,34 @@ class MediaViewer(QWidget): self.btn_play.setEnabled(False) self.btn_pause.setEnabled(False) self.btn_stop.setEnabled(False) - self.btn_clear.setEnabled(False) - self.btn_analyze.setEnabled(False) return + # メディア読み込み済み if not self.status.is_video: + # 画像 logger.debug("image loaded, disabling video buttons") self.btn_open.setEnabled(True) self.btn_play.setEnabled(False) self.btn_pause.setEnabled(False) self.btn_stop.setEnabled(False) - self.btn_clear.setEnabled(True) - self.btn_analyze.setEnabled(True) - - + return + # 動画 + logger.debug(f"video playing state: cap={self.cap}, timer active={self.timer.isActive()}") + if self.cap is not None and self.timer.isActive(): + # 再生中 + logger.debug("video playing") + self.btn_open.setEnabled(True) + self.btn_play.setEnabled(False) + self.btn_pause.setEnabled(True) + self.btn_stop.setEnabled(True) + elif self.cap is not None and not self.timer.isActive(): + # 停止中 + logger.debug("video paused") + self.btn_open.setEnabled(True) + self.btn_play.setEnabled(True) + self.btn_pause.setEnabled(False) + self.btn_stop.setEnabled(True) # ---- D&D ---- # mainwindowにも処理が必要 diff --git a/src/jobs/Job_detect_objects_for_car.py b/src/jobs/Job_detect_objects_for_car.py index 187aa2e..495aebc 100644 --- a/src/jobs/Job_detect_objects_for_car.py +++ b/src/jobs/Job_detect_objects_for_car.py @@ -117,10 +117,10 @@ class JobDetectObjectsForCar(JobBase): cv2.rectangle(out, (x1, y1), (x2, y2), (0, 200, 0), 2) cv2.putText(out, f"{d.label} {d.score:.2f}", (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 200, 0), 2) - # バナー - cv2.rectangle(out, (10, 10), (260, 52), (0, 0, 0), -1) - cv2.putText(out, f"Count: {len(dets)}", (18, 40), - cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2) + # # バナー + # cv2.rectangle(out, (10, 10), (260, 52), (0, 0, 0), -1) + # cv2.putText(out, f"Count: {len(dets)}", (18, 40), + # cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2) return out # ---------- 実行本体 ---------- diff --git a/src/video_player.py b/src/video_player.py index 7d1c40b..71a29c5 100644 --- a/src/video_player.py +++ b/src/video_player.py @@ -7,14 +7,7 @@ from PySide6.QtWidgets import ( QVBoxLayout, QHBoxLayout, QSplitter, QTabWidget, QFormLayout, QSpinBox, QCheckBox ) -IMAGE_EXT = {".jpg", ".jpeg", ".png", ".bmp", ".gif"} -VIDEO_EXT = {".mp4", ".mov", ".avi", ".mkv", ".m4v"} -def bgr_to_qpixmap(bgr): - rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) - h, w, ch = rgb.shape - qimg = QImage(rgb.data, w, h, ch*w, QImage.Format.Format_RGB888) - return QPixmap.fromImage(qimg) class MediaViewer(QWidget): """画像と動画を共通で扱う左側ビューワ""" @@ -60,16 +53,7 @@ class MediaViewer(QWidget): self.current_path = path suffix = Path(path).suffix.lower() - if suffix in IMAGE_EXT: - self._is_video = False - data = np.fromfile(path, dtype=np.uint8) - img = cv2.imdecode(data, cv2.IMREAD_COLOR) - if img is None: - self.label.setText("画像を開けませんでした") - return - self.label.setPixmap(bgr_to_qpixmap(img).scaled( - self.label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) - elif suffix in VIDEO_EXT: + if suffix in VIDEO_EXT: self._is_video = True self.cap = cv2.VideoCapture(path) if not self.cap.isOpened(): @@ -91,7 +75,7 @@ class MediaViewer(QWidget): return if self.cap is None: return - # 30fps程度で更新(必要に応じて調整) + # 30fps程度で更新(必要に応じて調整) self.timer.start(33) def pause(self): @@ -158,13 +142,13 @@ class RightPanel(QWidget): a_lay.addWidget(QPushButton("Run Analysis")) a_lay.addStretch() - # Preview タブ(分析結果の簡易表示など) + # Preview タブ(分析結果の簡易表示など) self.preview = QWidget() pv_lay = QVBoxLayout(self.preview) - pv_lay.addWidget(QLabel("分析プレビュー(ここに結果サムネ/メトリクス等)")) + pv_lay.addWidget(QLabel("分析プレビュー(ここに結果サムネ/メトリクス等)")) pv_lay.addStretch() - # Settings タブ(しきい値や切替など) + # Settings タブ(しきい値や切替など) self.settings = QWidget() st_form = QFormLayout(self.settings) self.spin_conf = QSpinBox(); self.spin_conf.setRange(10, 90); self.spin_conf.setValue(35) @@ -173,8 +157,8 @@ class RightPanel(QWidget): st_form.addRow(self.chk_boxes) tabs.addTab(self.analyser, "Analyser") - tabs.addTab(self.settings, "Settings") tabs.addTab(self.preview, "Preview") + tabs.addTab(self.settings, "Settings") lay = QVBoxLayout(self) lay.addWidget(tabs) @@ -209,7 +193,7 @@ class MainWindow(QMainWindow): open_act.triggered.connect(self.viewer.open_file) self.menuBar().addMenu("File").addAction(open_act) - # ルートウィンドウにもD&D(フォルダから直接落とせるように) + # ルートウィンドウにもD&D(フォルダから直接落とせるように) def dragEnterEvent(self, e: QDragEnterEvent): if e.mimeData().hasUrls(): e.acceptProposedAction()