動画対応完了

This commit is contained in:
ry.yamafuji 2025-09-12 11:52:39 +09:00
parent 95d16825dc
commit ba9e06869a
5 changed files with 94 additions and 48 deletions

View File

@ -1,9 +1,13 @@
# image-analyzer-lab
画像認識及び検知、セクションなどの視覚研究ラボ
画像認識及び検知、セクションなどの視覚研究ラボ1
![APP_TOP](docs/images/app01.png)
* 静止画の物体検知
*
## Exec
```bat

View File

@ -90,5 +90,3 @@ class AppStatus(Singleton):
@overlay_image.setter
def overlay_image(self, img: cv2.typing.MatLike):
self.set_status('overlay_image', img)

View File

@ -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_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にも処理が必要

View File

@ -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
# ---------- 実行本体 ----------

View File

@ -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()