動画対応完了
This commit is contained in:
parent
95d16825dc
commit
ba9e06869a
@ -1,9 +1,13 @@
|
||||
# image-analyzer-lab
|
||||
|
||||
画像認識及び検知、セクションなどの視覚研究ラボ
|
||||
|
||||
画像認識及び検知、セクションなどの視覚研究ラボ1
|
||||
|
||||

|
||||
|
||||
* 静止画の物体検知
|
||||
*
|
||||
|
||||
## Exec
|
||||
|
||||
```bat
|
||||
|
||||
@ -90,5 +90,3 @@ class AppStatus(Singleton):
|
||||
@overlay_image.setter
|
||||
def overlay_image(self, img: cv2.typing.MatLike):
|
||||
self.set_status('overlay_image', img)
|
||||
|
||||
|
||||
|
||||
@ -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にも処理が必要
|
||||
|
||||
@ -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
|
||||
|
||||
# ---------- 実行本体 ----------
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user