動画対応完了

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

View File

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

View File

@ -1,7 +1,7 @@
import os import os
import numpy as np import numpy as np
import cv2 import cv2
from PySide6.QtCore import Qt from PySide6.QtCore import Qt, QTimer
from pathlib import Path from pathlib import Path
from PySide6.QtGui import QDragEnterEvent, QDropEvent from PySide6.QtGui import QDragEnterEvent, QDropEvent
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@ -33,31 +33,38 @@ class MediaViewer(QWidget):
self.btn_play = QPushButton("Play") self.btn_play = QPushButton("Play")
self.btn_pause = QPushButton("Pause") self.btn_pause = QPushButton("Pause")
self.btn_stop = QPushButton("Stop") self.btn_stop = QPushButton("Stop")
self.btn_clear = QPushButton("Clear")
self.btn_analyze = QPushButton("Analyze")
self.update_button_state() self.update_button_state()
for b in ( for b in (
self.btn_open, self.btn_play, self.btn_pause, self.btn_stop, 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.addWidget(b)
self.btn_bar.addStretch() self.btn_bar.addStretch()
self.btn_open.clicked.connect(self.open_file) self.btn_open.clicked.connect(self.open_file)
self.label = QLabel("Drop files here\n(画像/動画)") self.label = QLabel("Drop files here\n(画像/動画)")
self.label.setAlignment(Qt.AlignCenter) self.label.setAlignment(Qt.AlignCenter)
self.label.setStyleSheet("background:#222;color:#aaa;border:1px solid #444;") self.label.setStyleSheet("background:#222;color:#aaa;border:1px solid #444;")
self.label.setMinimumSize(640, 360) self.label.setMinimumSize(640, 360)
lay = QVBoxLayout(self) lay = QVBoxLayout(self)
lay.addLayout(self.btn_bar) lay.addLayout(self.btn_bar)
lay.addWidget(self.label, 1) 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 # 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): def load(self, path: str):
@ -85,11 +92,16 @@ class MediaViewer(QWidget):
# 画像解析パイプラインを実行 # 画像解析パイプラインを実行
pipeline = ImagePipeline() pipeline = ImagePipeline()
pipeline.run() pipeline.run()
self.on_analyze_view()
elif suffix in VIDEO_EXT: elif suffix in VIDEO_EXT:
logger.info(f"Loading video: {path}") logger.info(f"Loading video: {path}")
self.status.is_video = True 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: else:
logger.warning(f"Unsupported media type: {path}") logger.warning(f"Unsupported media type: {path}")
self.label.setText("未対応の形式です") self.label.setText("未対応の形式です")
@ -106,8 +118,22 @@ class MediaViewer(QWidget):
self.label.setPixmap(bgr_to_qpixmap(self.status.overlay_image).scaled( self.label.setPixmap(bgr_to_qpixmap(self.status.overlay_image).scaled(
self.label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) 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): def open_file(self):
"""ファイル選択ダイアログを開く""" """ファイル選択ダイアログを開く"""
@ -116,6 +142,27 @@ class MediaViewer(QWidget):
if path: if path:
self.load(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): def update_button_state(self):
"""ボタンの有効/無効を更新""" """ボタンの有効/無効を更新"""
if not self.status.current_media_path: if not self.status.current_media_path:
@ -124,21 +171,34 @@ class MediaViewer(QWidget):
self.btn_play.setEnabled(False) self.btn_play.setEnabled(False)
self.btn_pause.setEnabled(False) self.btn_pause.setEnabled(False)
self.btn_stop.setEnabled(False) self.btn_stop.setEnabled(False)
self.btn_clear.setEnabled(False)
self.btn_analyze.setEnabled(False)
return return
# メディア読み込み済み
if not self.status.is_video: if not self.status.is_video:
# 画像
logger.debug("image loaded, disabling video buttons") logger.debug("image loaded, disabling video buttons")
self.btn_open.setEnabled(True) self.btn_open.setEnabled(True)
self.btn_play.setEnabled(False) self.btn_play.setEnabled(False)
self.btn_pause.setEnabled(False) self.btn_pause.setEnabled(False)
self.btn_stop.setEnabled(False) self.btn_stop.setEnabled(False)
self.btn_clear.setEnabled(True) return
self.btn_analyze.setEnabled(True)
# 動画
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 ---- # ---- D&D ----
# mainwindowにも処理が必要 # mainwindowにも処理が必要

View File

@ -117,10 +117,10 @@ class JobDetectObjectsForCar(JobBase):
cv2.rectangle(out, (x1, y1), (x2, y2), (0, 200, 0), 2) 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.putText(out, f"{d.label} {d.score:.2f}", (x1, max(0, y1 - 6)),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 200, 0), 2) cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 200, 0), 2)
# バナー # # バナー
cv2.rectangle(out, (10, 10), (260, 52), (0, 0, 0), -1) # cv2.rectangle(out, (10, 10), (260, 52), (0, 0, 0), -1)
cv2.putText(out, f"Count: {len(dets)}", (18, 40), # cv2.putText(out, f"Count: {len(dets)}", (18, 40),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2) # cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2)
return out return out
# ---------- 実行本体 ---------- # ---------- 実行本体 ----------

View File

@ -7,14 +7,7 @@ from PySide6.QtWidgets import (
QVBoxLayout, QHBoxLayout, QSplitter, QTabWidget, QFormLayout, QSpinBox, QCheckBox 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): class MediaViewer(QWidget):
"""画像と動画を共通で扱う左側ビューワ""" """画像と動画を共通で扱う左側ビューワ"""
@ -60,16 +53,7 @@ class MediaViewer(QWidget):
self.current_path = path self.current_path = path
suffix = Path(path).suffix.lower() suffix = Path(path).suffix.lower()
if suffix in IMAGE_EXT: if suffix in VIDEO_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:
self._is_video = True self._is_video = True
self.cap = cv2.VideoCapture(path) self.cap = cv2.VideoCapture(path)
if not self.cap.isOpened(): if not self.cap.isOpened():
@ -91,7 +75,7 @@ class MediaViewer(QWidget):
return return
if self.cap is None: if self.cap is None:
return return
# 30fps程度で更新(必要に応じて調整) # 30fps程度で更新(必要に応じて調整)
self.timer.start(33) self.timer.start(33)
def pause(self): def pause(self):
@ -158,13 +142,13 @@ class RightPanel(QWidget):
a_lay.addWidget(QPushButton("Run Analysis")) a_lay.addWidget(QPushButton("Run Analysis"))
a_lay.addStretch() a_lay.addStretch()
# Preview タブ(分析結果の簡易表示など) # Preview タブ(分析結果の簡易表示など)
self.preview = QWidget() self.preview = QWidget()
pv_lay = QVBoxLayout(self.preview) pv_lay = QVBoxLayout(self.preview)
pv_lay.addWidget(QLabel("分析プレビュー(ここに結果サムネ/メトリクス等)")) pv_lay.addWidget(QLabel("分析プレビュー(ここに結果サムネ/メトリクス等)"))
pv_lay.addStretch() pv_lay.addStretch()
# Settings タブ(しきい値や切替など) # Settings タブ(しきい値や切替など)
self.settings = QWidget() self.settings = QWidget()
st_form = QFormLayout(self.settings) st_form = QFormLayout(self.settings)
self.spin_conf = QSpinBox(); self.spin_conf.setRange(10, 90); self.spin_conf.setValue(35) 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) st_form.addRow(self.chk_boxes)
tabs.addTab(self.analyser, "Analyser") tabs.addTab(self.analyser, "Analyser")
tabs.addTab(self.settings, "Settings")
tabs.addTab(self.preview, "Preview") tabs.addTab(self.preview, "Preview")
tabs.addTab(self.settings, "Settings")
lay = QVBoxLayout(self) lay = QVBoxLayout(self)
lay.addWidget(tabs) lay.addWidget(tabs)
@ -209,7 +193,7 @@ class MainWindow(QMainWindow):
open_act.triggered.connect(self.viewer.open_file) open_act.triggered.connect(self.viewer.open_file)
self.menuBar().addMenu("File").addAction(open_act) self.menuBar().addMenu("File").addAction(open_act)
# ルートウィンドウにもD&D(フォルダから直接落とせるように) # ルートウィンドウにもD&D(フォルダから直接落とせるように)
def dragEnterEvent(self, e: QDragEnterEvent): def dragEnterEvent(self, e: QDragEnterEvent):
if e.mimeData().hasUrls(): if e.mimeData().hasUrls():
e.acceptProposedAction() e.acceptProposedAction()