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

|

|
||||||
|
|
||||||
|
* 静止画の物体検知
|
||||||
|
*
|
||||||
|
|
||||||
## Exec
|
## Exec
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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にも処理が必要
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
# ---------- 実行本体 ----------
|
# ---------- 実行本体 ----------
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user