Added GUI
This commit is contained in:
parent
ae9774cf0d
commit
74df5cb3f0
23 changed files with 3174 additions and 2 deletions
32
README.md
32
README.md
|
@ -1,3 +1,31 @@
|
|||
# conan
|
||||
# ConAn
|
||||
This is the official repository for [ConAn: A Usable Tool for Multimodal <u>Con</u>versation <u>An</u>alysis](https://www.perceptualui.org/publications/penzkofer21_icmi.pdf) <br>
|
||||
ConAn – our graphical tool for multimodal conversation analysis – takes 360 degree videos recorded during multiperson group interactions as input. ConAn integrates state-of-the-art models for gaze estimation, active speaker detection,
|
||||
facial action unit detection, and body movement detection and can output quantitative reports both at individual and group
|
||||
level, as well as different visualizations that provide qualitative insights into group interaction.
|
||||
|
||||
ConAn: A Usable Tool for Multimodal <u>Con</u>versation <u>An</u>alysis
|
||||
## Installation
|
||||
For the graphical user interface (GUI) you need python>3.6 to install the [requirements](requirements.txt) via pip:
|
||||
```
|
||||
pip install requirements.txt
|
||||
```
|
||||
## Get Started
|
||||
To test the GUI you can download our example use case videos from googledrive: <br>
|
||||
As well as the respective processed ``.dat`` files which include all the analyses.
|
||||
Run [main.py](main.py) and import the video file you would like to analyze.
|
||||
## Processing
|
||||
|
||||
|
||||
|
||||
## Citation
|
||||
Please cite this paper if you use ConAn or parts of this publication in your research:
|
||||
```
|
||||
@inproceedings{penzkofer21_icmi,
|
||||
author = {Penzkofer, Anna and Müller, Philipp and Bühler, Felix and Mayer, Sven and Bulling, Andreas},
|
||||
title = {ConAn: A Usable Tool for Multimodal Conversation Analysis},
|
||||
booktitle = {Proc. ACM International Conference on Multimodal Interaction (ICMI)},
|
||||
year = {2021},
|
||||
doi = {10.1145/3462244.3479886},
|
||||
video = {https://www.youtube.com/watch?v=H2KfZNgx6CQ}
|
||||
}
|
||||
```
|
151
container.py
Normal file
151
container.py
Normal file
|
@ -0,0 +1,151 @@
|
|||
import numpy as np
|
||||
import pandas as pd
|
||||
import pickle as pkl
|
||||
import os.path
|
||||
import cv2
|
||||
from PyQt5 import QtCore, QtGui
|
||||
|
||||
|
||||
class DataContainer():
|
||||
|
||||
def __init__(self, movie_fileName, data_fileName):
|
||||
self.movie_fileName = movie_fileName
|
||||
self.data_fileName = data_fileName
|
||||
|
||||
self.frameCount = 0
|
||||
self.fps = 0
|
||||
self.frameSize = [0, 0]
|
||||
|
||||
self.dataGaze = None
|
||||
self.dataMovement = None
|
||||
self.number_ids = None
|
||||
self.dataRTGene = None
|
||||
self.image = None
|
||||
|
||||
def readData(self, verbose=False, rtgene=False):
|
||||
if (verbose):
|
||||
print("## Start Reading Data")
|
||||
|
||||
# Read Video Data
|
||||
f = self.movie_fileName
|
||||
print(f)
|
||||
if os.path.isfile(f):
|
||||
cap = cv2.VideoCapture(f)
|
||||
ret, frame = cap.read()
|
||||
self.image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
|
||||
h, w, ch = self.image.shape
|
||||
bytesPerLine = ch * w
|
||||
convertToQtFormat = QtGui.QImage(self.image.data, w, h, bytesPerLine, QtGui.QImage.Format_RGB888)
|
||||
|
||||
self.fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
self.frameCount = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
width = cap.get(cv2.CAP_PROP_FRAME_WIDTH) # float
|
||||
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) # float
|
||||
self.frameSize = [width, height]
|
||||
print(self.fps, self.frameCount)
|
||||
|
||||
if (verbose):
|
||||
print("Video frameCount %i" % self.frameCount)
|
||||
duration = self.frameCount / self.fps
|
||||
minutes = int(duration / 60)
|
||||
seconds = duration % 60
|
||||
print('Video duration (M:S) = ' + str(minutes) + ':' + str(seconds))
|
||||
else:
|
||||
print("WARNING: no video avaibale.")
|
||||
|
||||
# read data file
|
||||
with open(self.data_fileName, 'rb') as f:
|
||||
data = pkl.load(f)
|
||||
|
||||
# Read RT-Gene Gaze
|
||||
if rtgene:
|
||||
f = "%s_Gaze.pkl" % self.dataPath[0]
|
||||
df_gt = pd.read_pickle('exampledata/G2_VID1_GroundTruth.pkl')
|
||||
df_gt = df_gt[['Frame', 'ID0_target_spher', 'ID1_target_spher']]
|
||||
if os.path.isfile(f):
|
||||
df = pd.read_pickle(f)
|
||||
self.number_ids = len(df.PId.unique())
|
||||
|
||||
self.dataRTGene = df.pivot(index='Frame', columns="PId", values=["GazeTheta", "GazePhi", "HeadCenter",
|
||||
"HeadPoseYaw", "HeadPoseTheta",
|
||||
"Phi", "Theta"])
|
||||
lst = []
|
||||
for label in ["GazeTheta", "GazePhi", "Head", "HeadPoseYaw", "HeadPoseTheta", "Phi", "Theta"]:
|
||||
for head_id in range(self.number_ids):
|
||||
lst.append("ID%i_%s" % (head_id, label))
|
||||
self.dataRTGene.columns = lst
|
||||
self.dataRTGene = self.dataRTGene.reset_index()
|
||||
self.dataRTGene = pd.merge(self.dataRTGene, df_gt, on=['Frame'])
|
||||
self.dataRTGene = self.dataRTGene.rename(
|
||||
columns={'ID0_target_spher': 'ID1_target', 'ID1_target_spher': 'ID0_target'},
|
||||
errors='raise')
|
||||
|
||||
print('Detected %i IDs in video' % self.number_ids)
|
||||
if (verbose):
|
||||
print("Gaze sample count %i" % len(self.dataRTGene))
|
||||
else:
|
||||
print("WARNING: no RT-Gene data avaibale.")
|
||||
|
||||
# Read Gaze Data
|
||||
if "HeadPose" in data:
|
||||
self.dataGaze = data["HeadPose"]
|
||||
self.number_ids = len([col for col in self.dataGaze.columns if 'head' in col])
|
||||
|
||||
print('Detected %i IDs in video' % self.number_ids)
|
||||
if (verbose):
|
||||
print("Gaze sample count %i" % len(self.dataGaze))
|
||||
else:
|
||||
print("WARNING: no gaze data avaibale.")
|
||||
|
||||
"""
|
||||
# Read OpenPose Data
|
||||
f = self.__get_filename_with_substring("OpenPose")
|
||||
if os.path.isfile(f):
|
||||
self.dataPose = pd.read_pickle(f)
|
||||
if (verbose):
|
||||
print("Pose sample count %i" % len(self.dataPose))
|
||||
else:
|
||||
print("WARNING: no pose data avaibale.")
|
||||
"""
|
||||
|
||||
# Read Movement Data
|
||||
if "BodyMovement" in data:
|
||||
self.dataMovement = data["BodyMovement"]
|
||||
if verbose:
|
||||
print('Body movement sample count %i' % len(self.dataMovement))
|
||||
else:
|
||||
print('WARNING: no body movement data available.')
|
||||
|
||||
def getFrameCount(self):
|
||||
return self.frameCount
|
||||
|
||||
def getFrameSize(self):
|
||||
return self.frameSize
|
||||
|
||||
def getFPS(self):
|
||||
return self.fps
|
||||
|
||||
def getVideo(self):
|
||||
return self.movie_fileName
|
||||
|
||||
def getGazeData(self):
|
||||
return self.dataGaze
|
||||
|
||||
def getFrame(self, frameIdx):
|
||||
return frameIdx
|
||||
|
||||
def getFrameCurrent(self):
|
||||
return 1
|
||||
|
||||
def getNumberIDs(self):
|
||||
return self.number_ids
|
||||
|
||||
def getMovementData(self):
|
||||
return self.dataMovement
|
||||
|
||||
def getRTGeneData(self):
|
||||
return self.dataRTGene
|
||||
|
||||
def getImage(self):
|
||||
return self.image
|
183
gui.ui
Normal file
183
gui.ui
Normal file
|
@ -0,0 +1,183 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ConAn</class>
|
||||
<widget class="QMainWindow" name="ConAn">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>450</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QTabWidget" name="tab">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>4</horstretch>
|
||||
<verstretch>1</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<widget class="WidgetGaze" name="gaze">
|
||||
<attribute name="title">
|
||||
<string>Eye Gaze</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
<widget class="WidgetSpeaking" name="speak">
|
||||
<attribute name="title">
|
||||
<string>Speaking Activity</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
<widget class="WidgetPose" name="pose">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Ignored">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<attribute name="title">
|
||||
<string>Body Pose</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
<widget class="WidgetFacialExpression" name="face">
|
||||
<attribute name="title">
|
||||
<string>Facial Expression</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
<widget class="WidgetObject" name="object">
|
||||
<attribute name="title">
|
||||
<string>Object Detection</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="WidgetTimeLine" name="time" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>4</horstretch>
|
||||
<verstretch>1</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>60</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="WidgetPlayer" name="video" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>3</horstretch>
|
||||
<verstretch>1</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="WidgetVideoSettings" name="vsettings" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>1</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<action name="actionOpen">
|
||||
<property name="text">
|
||||
<string>Open</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionExit">
|
||||
<property name="text">
|
||||
<string>Exit</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>WidgetTimeLine</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>uiwidget/widgettimeline</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>WidgetPlayer</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>uiwidget/widgetplayer</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>WidgetPose</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>uiwidget/widgetpose</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>WidgetSpeaking</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>uiwidget/widgetspeaking</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>WidgetGaze</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>uiwidget/widgetgaze</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>WidgetFacialExpression</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>uiwidget/widgetfacialexpression</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>WidgetVideoSettings</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>uiwidget/widgetvideosettings</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>WidgetObject</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">uiwidget/widgetobject</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>tab</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
BIN
icons/logo.png
Normal file
BIN
icons/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
BIN
icons/pause.png
Normal file
BIN
icons/pause.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
icons/play.png
Normal file
BIN
icons/play.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
icons/stop.png
Normal file
BIN
icons/stop.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
287
main.py
Normal file
287
main.py
Normal file
|
@ -0,0 +1,287 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import json
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import os
|
||||
|
||||
import PyQt5
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5.QtCore import QSize, QUrl, pyqtSignal, QFile, QTextStream
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtMultimedia import QMediaContent
|
||||
from PyQt5.QtWidgets import QAction, QFileDialog, QStatusBar
|
||||
from PyQt5.QtGui import QPalette, QColor
|
||||
|
||||
print("Qt version {0}".format(QtCore.QT_VERSION_STR))
|
||||
from PyQt5 import QtWidgets
|
||||
import pyqtgraph as pg
|
||||
|
||||
import cv2
|
||||
print("OpenCV version {0} ".format(cv2.__version__))
|
||||
|
||||
|
||||
from uiwidget import widgetplayer, widgettimeline, widgetgaze, widgetspeaking, widgetpose, widgetfacialexpression, widgetobject, widgetvideosettings
|
||||
from processing import Processor
|
||||
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow):
|
||||
sendPath = pyqtSignal(str)
|
||||
sendSegments = pyqtSignal(np.ndarray)
|
||||
|
||||
def __init__(self, verbose=False, parent=None):
|
||||
super(MainWindow, self).__init__(parent)
|
||||
self.setWindowTitle('Conversation Analysis')
|
||||
|
||||
PyQt5.uic.loadUi('gui.ui', self) # Load the .ui file
|
||||
self.show() # Show the GUI
|
||||
|
||||
self.resize(1600, 900)
|
||||
self.dataContainer = None
|
||||
self.verbose = verbose
|
||||
self.movieDir = ''
|
||||
self.segments = []
|
||||
self.frames_bitmask = None
|
||||
self.frame = None
|
||||
self.frameCount = None
|
||||
self.fps = None
|
||||
|
||||
self._setupMenuBar()
|
||||
self.status = QStatusBar()
|
||||
self.setStatusBar(self.status)
|
||||
self.status.showMessage('Welcome', 1000)
|
||||
|
||||
self.wPlayer = self.findChild(widgetplayer.WidgetPlayer, "video")
|
||||
self.wTimeLine = self.findChild(widgettimeline.WidgetTimeLine, "time")
|
||||
self.wTab = self.findChild(QtWidgets.QTabWidget, "tab")
|
||||
self.wGaze = self.findChild(widgetgaze.WidgetGaze, "gaze")
|
||||
self.wSpeaking = self.findChild(widgetspeaking.WidgetSpeaking, "speak")
|
||||
self.wPose = self.findChild(widgetpose.WidgetPose, "pose")
|
||||
self.wFace = self.findChild(widgetfacialexpression.WidgetFacialExpression, "face")
|
||||
self.wObject = self.findChild(widgetobject.WidgetObject, "object")
|
||||
self.wVideoSettings = self.findChild(widgetvideosettings.WidgetVideoSettings, "vsettings")
|
||||
|
||||
self.wPlayer.mediaPlayer.positionChanged.connect(self.wTimeLine.updateSlider)
|
||||
self.wPlayer.sendState.connect(self.wTimeLine.mediaStateChanged)
|
||||
|
||||
self.wTimeLine.signalSetPosition.connect(self.wPlayer.mediaPlayer.setPosition)
|
||||
self.wTimeLine.signalPlay.connect(self.wPlayer.play)
|
||||
|
||||
|
||||
self.processor = Processor()
|
||||
self.processor.signalInit.connect(self.wPose.initMovementGraph)
|
||||
self.processor.signalInit.connect(self.wSpeaking.initSpeakingGraph)
|
||||
self.processor.signalInit.connect(self.wGaze.initGazeGraph)
|
||||
self.processor.signalInitTags.connect(self.wObject.setInit)
|
||||
|
||||
self.processor.signalInit.connect(self.wFace.setInit)
|
||||
self.processor.signalInit.connect(self.wVideoSettings.setInit)
|
||||
|
||||
self.wPlayer.frameAvailable.connect(self.processor.saveCurrentFrameData)
|
||||
|
||||
self.processor.signalPoseSetInit.connect(self.wPose.setInit)
|
||||
self.processor.signalGazeSetInit.connect(self.wGaze.setInit)
|
||||
self.processor.signalSpeakerSetInit.connect(self.wSpeaking.setInit)
|
||||
self.processor.signalUpdateHandVelocity.connect(self.wPose.updateHandVelocity)
|
||||
self.processor.signalUpdateMovementGraph.connect(self.wPose.updateMovementGraph)
|
||||
self.processor.signalUpdateSpeakGraph.connect(self.wSpeaking.updateSpeakingGraph)
|
||||
self.processor.signalUpdateFaceAus.connect(self.wFace.updateFrame)
|
||||
self.processor.signalUpdateFaceImgs.connect(self.wFace.updateImages)
|
||||
self.processor.signalUpdateTagGraph.connect(self.wObject.updateTagGraph)
|
||||
self.processor.signalVideoLabel.connect(self.wPlayer.draw_labels)
|
||||
self.processor.signalPosePoints.connect(self.wPlayer.draw_pose)
|
||||
self.processor.signalPoseChangedLabels.connect(self.wPose.updateLables)
|
||||
self.processor.signalSpeakChangedLabels.connect(self.wSpeaking.updateLables)
|
||||
self.processor.signalUpdateGazeGraph.connect(self.wGaze.updateGazeGraph)
|
||||
self.processor.signalUpdateGazeMap.connect(self.wPlayer.draw_gaze)
|
||||
self.processor.signalUpdateTags.connect(self.wPlayer.draw_tags)
|
||||
|
||||
self.wVideoSettings.signalVisualize.connect(self.processor.onVisualize)
|
||||
self.wVideoSettings.signalSelectID.connect(self.wPose.onSelectedID)
|
||||
self.wVideoSettings.signalSelectID.connect(self.wGaze.onSelectedID)
|
||||
self.wVideoSettings.signalSelectID.connect(self.wPlayer.onSelectedID)
|
||||
self.wVideoSettings.signalSelectID.connect(self.processor.onSelectedID)
|
||||
|
||||
self.processor.signalClearLabels.connect(self.wPlayer.clear_labels)
|
||||
self.processor.signalClearPose.connect(self.wPlayer.clear_pose)
|
||||
self.processor.signalClearGaze.connect(self.wPlayer.clear_gaze)
|
||||
self.processor.signalClearTags.connect(self.wPlayer.clear_tags)
|
||||
|
||||
self.processor.signalDeactivatePoseTab.connect(self.togglePoseTab)
|
||||
self.processor.signalDeactivateFaceTab.connect(self.toggleFaceTab)
|
||||
self.processor.signalDeactivateGazeTab.connect(self.toggleGazeTab)
|
||||
self.processor.signalDeactivateSpeakingTab.connect(self.toggleSpeakingTab)
|
||||
self.processor.signalDeactivateObjectTab.connect(self.toggleObjectTab)
|
||||
|
||||
self.wTab.setCurrentIndex(0)
|
||||
self.wTab.currentChanged.connect(self.processor.tabChanged)
|
||||
|
||||
def openProject(self, movie_fileName, data_fileName):
|
||||
self.status.showMessage('Reading data...')
|
||||
self.processor.readData(movie_fileName, data_fileName)
|
||||
self.processor.calculateMovementMeasures()
|
||||
self.processor.calculateSpeakingMeasures()
|
||||
self.wPlayer.mediaPlayer.positionChanged.connect(self.processor.updateFrame)
|
||||
self.wPlayer.mediaPlayer.positionChanged.connect(self.updateFrame)
|
||||
self.fps = self.processor.getFPS()
|
||||
|
||||
# Variables for segment updates --> bool array for all frames
|
||||
self.frame = 0
|
||||
self.frameCount = self.processor.getFrameCount()
|
||||
self.frames_bitmask = np.ones(self.frameCount)
|
||||
self.segments = [(0, self.frameCount)]
|
||||
|
||||
self.wTimeLine.setInit(self.processor.getFrameCount(), self.processor.getNumberIDs(), self.processor.getFPS())
|
||||
self.wPlayer.setInit(self.processor.getVideo(), self.processor.getFPS(),
|
||||
self.processor.getOriginalVideoResolution(), self.processor.getNumberIDs(),
|
||||
self.processor.getColors(), self.processor.getTags(), self.processor.getTagColors())
|
||||
|
||||
self.wTimeLine.rangeslider.segmentsChanged.connect(self.segmentsChanged)
|
||||
self.sendSegments.connect(self.processor._updateSegments)
|
||||
self.processor.setReady(True)
|
||||
self.status.showMessage('Ready')
|
||||
|
||||
def segmentsChanged(self, segments, last_segment_changed):
|
||||
self.segments = segments
|
||||
self.frames_bitmask = np.zeros(self.frameCount)
|
||||
for segment in segments:
|
||||
start, end = segment
|
||||
self.frames_bitmask[start:end] = np.ones((end - start))
|
||||
self.sendSegments.emit(self.frames_bitmask)
|
||||
|
||||
start, end = segments[last_segment_changed]
|
||||
|
||||
if start > self.frame:
|
||||
self.wPlayer.setFrame(start)
|
||||
|
||||
if end < self.frame:
|
||||
self.wPlayer.pause()
|
||||
self.wPlayer.setFrame(end)
|
||||
|
||||
"""
|
||||
def closeEvent(self, event):
|
||||
self.wPlayer.stop()
|
||||
|
||||
#quit_msg = "Are you sure you want to exit the program?"
|
||||
# reply = QtGui.QMessageBox.question(self, 'Message',
|
||||
# quit_msg, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
|
||||
#
|
||||
# if reply == QtGui.QMessageBox.Yes:
|
||||
# event.accept()
|
||||
# else:
|
||||
# event.ignore()
|
||||
"""
|
||||
|
||||
def open(self):
|
||||
movie_fileName, _ = QFileDialog.getOpenFileName(self, "Open Movie File", "", "Files (*.mp4)", self.movieDir)
|
||||
data_fileName, _ = QFileDialog.getOpenFileName(
|
||||
self, "Open Preprocessed File", "", "Files (*.dat)", self.movieDir)
|
||||
|
||||
if movie_fileName and data_fileName and os.path.isfile(movie_fileName) and os.path.isfile(data_fileName):
|
||||
self.status.showMessage('Loading data... ')
|
||||
self.openProject(movie_fileName, data_fileName)
|
||||
else:
|
||||
print("ERROR: select two files")
|
||||
|
||||
def export(self):
|
||||
self.status.showMessage('Exporting data... This might take some time.')
|
||||
# has to be defined in the main window, and then call the processing function
|
||||
self.processor.export()
|
||||
self.status.showMessage('Exported data successfully!')
|
||||
|
||||
def _setupMenuBar(self):
|
||||
self.mainMenu = self.menuBar()
|
||||
|
||||
fileMenu = self.mainMenu.addMenu('&File')
|
||||
|
||||
openAct = QAction('&Open', self)
|
||||
openAct.setStatusTip('Open files')
|
||||
openAct.setShortcut('Ctrl+O')
|
||||
openAct.triggered.connect(self.open)
|
||||
fileMenu.addAction(openAct)
|
||||
|
||||
exportAct = QAction('&Export', self)
|
||||
exportAct.setStatusTip('Export calculations')
|
||||
exportAct.setShortcut('Ctrl+E')
|
||||
exportAct.triggered.connect(self.export)
|
||||
fileMenu.addAction(exportAct)
|
||||
|
||||
exitAct = QAction('&Exit', self)
|
||||
exitAct.setStatusTip('Exit application')
|
||||
exitAct.setShortcut('Ctrl+Q')
|
||||
exitAct.triggered.connect(self.close)
|
||||
fileMenu.addAction(exitAct)
|
||||
|
||||
#@QtCore.pyqtSlot(str)
|
||||
# def statusUpdate(self, message):
|
||||
# self.status.showMessage(message)
|
||||
|
||||
def updateFrame(self, position):
|
||||
self.frame = int((position / 1000.0) * self.fps)
|
||||
if self.frames_bitmask[self.frame] == 0:
|
||||
for segment in self.segments:
|
||||
if segment[0] > self.frame:
|
||||
self.wPlayer.setFrame(segment[0] + 1)
|
||||
break
|
||||
|
||||
@QtCore.pyqtSlot(bool)
|
||||
def togglePoseTab(self, deactivate):
|
||||
self.wTab.setTabEnabled(2, not deactivate)
|
||||
|
||||
@QtCore.pyqtSlot(bool)
|
||||
def toggleGazeTab(self, deactivate):
|
||||
self.wTab.setTabEnabled(0, not deactivate)
|
||||
|
||||
@QtCore.pyqtSlot(bool)
|
||||
def toggleFaceTab(self, deactivate):
|
||||
self.wTab.setTabEnabled(3, not deactivate)
|
||||
|
||||
@QtCore.pyqtSlot(bool)
|
||||
def toggleSpeakingTab(self, deactivate):
|
||||
self.wTab.setTabEnabled(1, not deactivate)
|
||||
|
||||
@QtCore.pyqtSlot(bool)
|
||||
def toggleObjectTab(self, deactivate):
|
||||
self.wTab.setTabEnabled(4, not deactivate)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
# print(sys.argv)
|
||||
app.setWindowIcon(QtGui.QIcon('icons/logo.png'))
|
||||
|
||||
app.setStyle("Fusion")
|
||||
|
||||
# Now use a palette to switch to dark colors:
|
||||
palette = QPalette()
|
||||
palette.setColor(QPalette.Window, QColor(53, 53, 53))
|
||||
palette.setColor(QPalette.WindowText, QtCore.Qt.white)
|
||||
palette.setColor(QPalette.Base, QColor(25, 25, 25))
|
||||
palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
|
||||
palette.setColor(QPalette.ToolTipBase, QtCore.Qt.black)
|
||||
palette.setColor(QPalette.ToolTipText, QtCore.Qt.white)
|
||||
palette.setColor(QPalette.Text, QtCore.Qt.white)
|
||||
palette.setColor(QPalette.Button, QColor(53, 53, 53))
|
||||
palette.setColor(QPalette.ButtonText, QtCore.Qt.white)
|
||||
palette.setColor(QPalette.BrightText, QtCore.Qt.red)
|
||||
palette.setColor(QPalette.Link, QColor(42, 130, 218))
|
||||
palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
|
||||
palette.setColor(QPalette.HighlightedText, QtCore.Qt.black)
|
||||
|
||||
app.setPalette(palette)
|
||||
|
||||
|
||||
|
||||
if ("--verbose" in sys.argv):
|
||||
verbose = True
|
||||
print("### Verbose ENABLED")
|
||||
else:
|
||||
verbose = False
|
||||
|
||||
main = MainWindow(verbose=verbose)
|
||||
main.setWindowTitle('ConAn: A Usable Tool for Multimodal Conversation Analysis')
|
||||
main.show()
|
||||
|
||||
sys.exit(app.exec_())
|
1015
processing.py
Normal file
1015
processing.py
Normal file
File diff suppressed because it is too large
Load diff
5
requirements.txt
Normal file
5
requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
PyQt5==5.14.2
|
||||
opencv-python==4.2.0.34
|
||||
pandas
|
||||
pyqtgraph
|
||||
matplotlib
|
0
uiwidget/__init__.py
Normal file
0
uiwidget/__init__.py
Normal file
89
uiwidget/widgetfacialexpression.py
Normal file
89
uiwidget/widgetfacialexpression.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
import numpy as np
|
||||
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
from PyQt5.QtGui import QColor
|
||||
|
||||
lst_aus = ['AU1: Inner Brow Raiser', 'AU2: Outer Brow Raiser', 'AU4: Brow Lowerer', 'AU5: Upper Lid Raiser',
|
||||
'AU6: Cheek Raiser', 'AU9: Nose Wrinkler', 'AU12: Lip Corner Puller', 'AU15: Lip Corner Depressor',
|
||||
'AU17: Chin Raiser', 'AU20: Lip Stretcher', 'AU25: Lips Part', 'AU26: Jaw Drop']
|
||||
|
||||
class WidgetFacialExpression(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(WidgetFacialExpression, self).__init__(parent)
|
||||
|
||||
self.faceLayout = QtWidgets.QHBoxLayout()
|
||||
self.setLayout(self.faceLayout)
|
||||
|
||||
self.numberIDs = None
|
||||
self.valueLabels = dict()
|
||||
self.imgPlots = dict()
|
||||
|
||||
@QtCore.pyqtSlot(list, int)
|
||||
def setInit(self, colors, numberIDs):
|
||||
self.numberIDs = numberIDs
|
||||
|
||||
for id_no in range(numberIDs):
|
||||
idLayout = QtWidgets.QHBoxLayout()
|
||||
labelNameLayout = QtWidgets.QVBoxLayout()
|
||||
labelValueLayout = QtWidgets.QVBoxLayout()
|
||||
imageLayout = QtWidgets.QVBoxLayout()
|
||||
imageWidget = pg.PlotWidget(background=QColor(53, 53, 53))
|
||||
imageWidget.invertY()
|
||||
imageWidget.hideAxis('bottom'), imageWidget.hideAxis('left')
|
||||
imageWidget.setMaximumHeight(150), imageWidget.setMaximumWidth(150)
|
||||
imageWidget.setAspectLocked(True)
|
||||
self.imgPlots[id_no] = imageWidget
|
||||
|
||||
color = tuple([int(a * 255) for a in colors[id_no]])
|
||||
|
||||
labelID = QtWidgets.QLabel('ID%i' % id_no)
|
||||
labelID.setStyleSheet('font: bold 12px; color: black; background-color: rgb(%i,%i,%i)' % color)
|
||||
|
||||
|
||||
|
||||
#labelID.setFixedWidth(60)
|
||||
|
||||
labelNameLayout.addWidget(labelID)
|
||||
labelID = QtWidgets.QLabel(' ')
|
||||
|
||||
#labelID.setStyleSheet('background-color: rgb(%i,%i,%i)' % color)
|
||||
labelValueLayout.addWidget(labelID)
|
||||
lst = []
|
||||
for au in lst_aus:
|
||||
nLabel = QtWidgets.QLabel(au)
|
||||
labelNameLayout.addWidget(nLabel)
|
||||
|
||||
vLabel = QtWidgets.QLabel(' ')
|
||||
labelValueLayout.addWidget(vLabel)
|
||||
lst.append(vLabel)
|
||||
|
||||
self.valueLabels[id_no] = lst
|
||||
idLayout.addWidget(imageWidget)
|
||||
idLayout.addLayout(labelNameLayout)
|
||||
idLayout.addLayout(labelValueLayout)
|
||||
self.faceLayout.addLayout(idLayout)
|
||||
|
||||
@QtCore.pyqtSlot(dict, int)
|
||||
def updateImages(self, imgs, id_no):
|
||||
if imgs[id_no] is not None:
|
||||
img = np.moveaxis(imgs[id_no], 0, 1)
|
||||
img = pg.ImageItem(img)
|
||||
self.imgPlots[id_no].addItem(img)
|
||||
|
||||
|
||||
@QtCore.pyqtSlot(dict)
|
||||
def updateFrame(self, aus):
|
||||
if self.numberIDs is None:
|
||||
return
|
||||
|
||||
for id_no in range(self.numberIDs):
|
||||
if len(aus[id_no]) > 0:
|
||||
for i, label in enumerate(self.valueLabels[id_no]):
|
||||
if not np.any(np.isnan(np.array(aus[id_no].flatten()[0], dtype=np.float64))):
|
||||
label.setText('%.2f' % aus[id_no].flatten()[0][i])
|
||||
|
||||
|
||||
|
||||
|
||||
|
147
uiwidget/widgetgaze.py
Normal file
147
uiwidget/widgetgaze.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
import numpy as np
|
||||
import pyqtgraph
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Qt5Agg")
|
||||
import matplotlib.animation as animation
|
||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
||||
from matplotlib.figure import Figure
|
||||
from PyQt5 import QtWidgets, QtGui
|
||||
from PyQt5 import QtCore
|
||||
import pyqtgraph as pg
|
||||
import pyqtgraph.exporters
|
||||
|
||||
from utils.colors import random_colors
|
||||
from utils.util import sperical2equirec
|
||||
|
||||
|
||||
class WidgetGaze(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(WidgetGaze, self).__init__(parent)
|
||||
|
||||
layout = QtWidgets.QGridLayout()
|
||||
|
||||
# Setup gaze graph
|
||||
self.gazeGraph = pg.PlotWidget()
|
||||
self.gazeGraph.setBackground('w')
|
||||
self.gazeGraph.setYRange(-1.25, 1.25, padding=0)
|
||||
self.gazeGraph.setXRange(-1.25, 1.25, padding=0)
|
||||
self.gazeGraph.hideAxis('left')
|
||||
self.gazeGraph.hideAxis('bottom')
|
||||
self.gazeGraph.setAspectLocked()
|
||||
self.gazeGraph.getPlotItem().setTitle(title='Top-down View of Gaze')
|
||||
self.gazeGraphPlots = []
|
||||
|
||||
self.measures = QtWidgets.QWidget()
|
||||
self.measuresLayout = QtWidgets.QHBoxLayout()
|
||||
|
||||
# self.gazeMap = QtWidgets.QWidget()
|
||||
# self.heatmapSlider = HeatmapSlider()
|
||||
# self.heatmapSlider.signalSetThreshold.connect(self.setThreshold)
|
||||
# self.heatmapSlider.signalSaveImage.connect(self.gazeMap.saveImage)
|
||||
|
||||
# row, column, row span, column span
|
||||
layout.addWidget(self.measures, 0, 1, 2, 1)
|
||||
layout.addWidget(self.gazeGraph, 0, 0, 2, 1)
|
||||
layout.setColumnStretch(0, 1)
|
||||
layout.setColumnStretch(1, 1)
|
||||
|
||||
self.setLayout(layout)
|
||||
# layout.addWidget(self.gazeMap, 0, 0, 3, 1)
|
||||
# layout.addWidget(self.heatmapSlider, 3, 0, 1, 1)
|
||||
self.gazeLabels = []
|
||||
self.colors = None
|
||||
|
||||
@QtCore.pyqtSlot(dict, list, int)
|
||||
def setInit(self, measures, colors, numberIDs):
|
||||
"""Initialize measure widget with labels for all IDs"""
|
||||
self.colors = colors # Necessary for ID updates
|
||||
idLayout = QtWidgets.QVBoxLayout()
|
||||
labelID = QtWidgets.QLabel(' ')
|
||||
labelID.setFixedWidth(60)
|
||||
labelID.setFixedHeight(20)
|
||||
labelA = QtWidgets.QLabel('LookSomeone: ')
|
||||
labelNoLook = QtWidgets.QLabel('TotalNoLook: ')
|
||||
labelG = QtWidgets.QLabel('TotalWatched: ')
|
||||
labelRatio = QtWidgets.QLabel('RatioWatcherLookSOne: ')
|
||||
label = QtWidgets.QLabel('Tracked: ')
|
||||
# labelVel = QtWidgets.QLabel('totNoLook: ')
|
||||
idLayout.addWidget(labelID)
|
||||
idLayout.addWidget(labelA)
|
||||
idLayout.addWidget(labelNoLook)
|
||||
idLayout.addWidget(labelG)
|
||||
idLayout.addWidget(labelRatio)
|
||||
idLayout.addWidget(label)
|
||||
# idLayout.addWidget(labelVel)
|
||||
self.measuresLayout.insertLayout(-1, idLayout)
|
||||
|
||||
for id_no in range(numberIDs):
|
||||
idLayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
color = tuple([int(a * 255) for a in colors[id_no]])
|
||||
|
||||
labelID = QtWidgets.QLabel('ID%i' % id_no)
|
||||
labelID.setStyleSheet('font: bold 12px; color: black; background-color: rgb(%i,%i,%i)' % color)
|
||||
labelID.setFixedWidth(60)
|
||||
labelID.setFixedHeight(20)
|
||||
# Look Someone
|
||||
labelA = QtWidgets.QLabel('{:.2%}'.format(measures[id_no][1] / measures[id_no][2]))
|
||||
labelNoLook = QtWidgets.QLabel('{:.2%}'.format((measures[id_no][2] - measures[id_no][1]) / measures[id_no][2]))
|
||||
# Total Watched
|
||||
labelG = QtWidgets.QLabel('{:.2%}'.format(measures[id_no][0] / measures[id_no][2]))
|
||||
# ratio totWatcher / lookSomeone
|
||||
labelRatio = QtWidgets.QLabel('{:.2}'.format(measures[id_no][0] / measures[id_no][1]))
|
||||
label = QtWidgets.QLabel('%i frames' % measures[id_no][2])
|
||||
# labelVel = QtWidgets.QLabel('%.2f' % np.random.uniform(0, 1))
|
||||
idLayout.addWidget(labelID)
|
||||
idLayout.addWidget(labelA)
|
||||
idLayout.addWidget(labelNoLook)
|
||||
idLayout.addWidget(labelG)
|
||||
idLayout.addWidget(labelRatio)
|
||||
idLayout.addWidget(label)
|
||||
# idLayout.addWidget(labelVel)
|
||||
# self.gazeLabels.append(labelVel)
|
||||
self.measuresLayout.insertLayout(-1, idLayout)
|
||||
self.measures.setLayout(self.measuresLayout)
|
||||
|
||||
@QtCore.pyqtSlot(list, int)
|
||||
def initGazeGraph(self, colors, numberIDs):
|
||||
""" initialize gaze graph """
|
||||
# Big circle
|
||||
x1, y1 = self.get_circle(radius=1)
|
||||
self.gazeGraph.addItem(self.gazeGraph.plot(x1, y1, pen=pg.mkPen(0.5)))
|
||||
|
||||
# Camera
|
||||
x2, y2 = self.get_circle(radius=0.02)
|
||||
self.gazeGraph.addItem(self.gazeGraph.plot(x2, y2, pen=pg.mkPen(color=(0, 0, 0), width=3)))
|
||||
|
||||
for id_no in range(numberIDs):
|
||||
color = tuple([int(a * 255) for a in colors[id_no]])
|
||||
plt = self.gazeGraph.plot(x=[], y=[], pen=pg.mkPen(color=color, width=2))
|
||||
self.gazeGraphPlots.append(plt)
|
||||
|
||||
@QtCore.pyqtSlot(dict, int)
|
||||
def updateGazeGraph(self, data, numberIDs):
|
||||
""" frame updates for gaze graph """
|
||||
for id_no in range(numberIDs):
|
||||
if data and id_no in data:
|
||||
self.gazeGraphPlots[id_no].setData(data[id_no][0], data[id_no][1])
|
||||
|
||||
def get_circle(self, radius):
|
||||
""" helper function returns circle to x, y coordinates"""
|
||||
theta = np.linspace(0, 2 * np.pi, 100)
|
||||
x = radius * np.cos(theta)
|
||||
y = radius * np.sin(theta)
|
||||
return np.array(x), np.array(y)
|
||||
|
||||
@QtCore.pyqtSlot(list)
|
||||
def onSelectedID(self, lst):
|
||||
"""Change color to None of gaze graph plot if ID should not be visible"""
|
||||
for i, button in enumerate(lst):
|
||||
if not button.isChecked():
|
||||
self.gazeGraphPlots[i].setPen(None)
|
||||
else:
|
||||
color = tuple([int(a * 255) for a in self.colors[i]])
|
||||
pen = pg.mkPen(color=color)
|
||||
self.gazeGraphPlots[i].setPen(pen)
|
81
uiwidget/widgetobject.py
Normal file
81
uiwidget/widgetobject.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
import numpy as np
|
||||
import pyqtgraph
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QTextEdit
|
||||
import pyqtgraph as pg
|
||||
|
||||
|
||||
class WidgetObject(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(WidgetObject, self).__init__(parent)
|
||||
|
||||
self.frame = 0
|
||||
self.tagFields = QtWidgets.QWidget()
|
||||
self.tagLayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
# Setup Graph Plot Widget
|
||||
self.tagGraph = pg.PlotWidget()
|
||||
self.tagGraph.setBackground('w')
|
||||
self.tagGraph.setYRange(0, 400, padding=0)
|
||||
self.tagGraph.getPlotItem().getAxis('bottom').setTickSpacing(minor=50, major=100)
|
||||
self.tagGraph.getPlotItem().setTitle(title='Movement of Object Tags')
|
||||
self.tagPlots = dict()
|
||||
|
||||
self.tagTextFields = dict()
|
||||
self.plotText = dict()
|
||||
|
||||
layout = QtWidgets.QGridLayout()
|
||||
layout.addWidget(self.tagGraph, 0, 0)
|
||||
layout.addWidget(self.tagFields, 0, 1)
|
||||
self.setLayout(layout)
|
||||
|
||||
@QtCore.pyqtSlot(list, tuple, dict, list)
|
||||
def setInit(self, tags, frameSize, tracked, colors):
|
||||
|
||||
for i, tag in enumerate(tags):
|
||||
label = QtWidgets.QLabel('Object tag #%i:' % tag)
|
||||
label.setStyleSheet('color: black; background-color: rgb(%i,%i,%i)' % colors[i])
|
||||
label.setFixedHeight(20)
|
||||
field = QtWidgets.QTextEdit()
|
||||
field.setFixedHeight(20)
|
||||
field.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
field.textChanged.connect(self.tagTextChanged)
|
||||
|
||||
self.tagTextFields[tag] = field
|
||||
trackedLabel = QtWidgets.QLabel('Tracked: {:.0%}'.format(tracked[tag]))
|
||||
oneTagLayout = QtWidgets.QHBoxLayout()
|
||||
oneTagLayout.addWidget(label)
|
||||
oneTagLayout.addWidget(field)
|
||||
oneTagLayout.addWidget(trackedLabel)
|
||||
|
||||
self.tagLayout.insertLayout(-1, oneTagLayout)
|
||||
|
||||
x = list(range(-200, 0)) # 200 time points
|
||||
y = [0 for _ in range(200)] # 200 data points
|
||||
|
||||
dataLine = self.tagGraph.plot(x, y, pen=pg.mkPen(color=colors[i]))
|
||||
self.tagPlots[tag] = dataLine
|
||||
text = pg.TextItem(text='', color=colors[i])
|
||||
text.setAnchor((1, i + 1))
|
||||
|
||||
self.plotText[tag] = text
|
||||
self.tagGraph.addItem(text)
|
||||
|
||||
self.tagFields.setLayout(self.tagLayout)
|
||||
|
||||
@QtCore.pyqtSlot(dict)
|
||||
def updateTagGraph(self, tagData):
|
||||
for tag, values in tagData.items():
|
||||
self.tagPlots[tag].setData(x=values[1], y=values[0]) # Update the data.
|
||||
if tagData:
|
||||
self.tagGraph.setXRange(np.min(values[1]), np.max(values[1]))
|
||||
|
||||
def tagTextChanged(self):
|
||||
for tag, field in self.tagTextFields.items():
|
||||
self.plotText[tag].setText(field.toPlainText())
|
||||
x, y = self.tagPlots[tag].getData()
|
||||
if len(x) > 0 and len(y) > 0:
|
||||
#print(tag, x[-1], y[-1])
|
||||
self.plotText[tag].setPos(x[-1], y[-1])
|
||||
|
309
uiwidget/widgetplayer.py
Normal file
309
uiwidget/widgetplayer.py
Normal file
|
@ -0,0 +1,309 @@
|
|||
from PyQt5 import QtCore, QtGui, QtWidgets, QtMultimedia, QtMultimediaWidgets, Qt
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
|
||||
|
||||
|
||||
class WidgetPlayer(QtWidgets.QWidget):
|
||||
updateFrame = QtCore.pyqtSignal(int)
|
||||
# sendFileName = QtCore.pyqtSignal(str)
|
||||
sendState = QtCore.pyqtSignal(QtMultimedia.QMediaPlayer.State)
|
||||
frameAvailable = QtCore.pyqtSignal(QtGui.QImage)
|
||||
|
||||
labels = list()
|
||||
colors = list()
|
||||
pose_data = list()
|
||||
gaze_data = list()
|
||||
tag_data = dict()
|
||||
tags = list()
|
||||
tag_colors = dict()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(WidgetPlayer, self).__init__(parent)
|
||||
self.root = QtCore.QFileInfo(__file__).absolutePath()
|
||||
|
||||
# mediaplayer for decoding the video
|
||||
self.mediaPlayer = QtMultimedia.QMediaPlayer(self, QtMultimedia.QMediaPlayer.VideoSurface)
|
||||
#self.mediaPlayer.setMuted(True)
|
||||
|
||||
# top = graphicsscene, middle = graphiscview, bottom = graphicsvideoitem, lowest = graphisctextitems, ...
|
||||
self._scene = QtWidgets.QGraphicsScene(self)
|
||||
self._scene.setBackgroundBrush(QtGui.QBrush(QtGui.QColor('black')))
|
||||
self._gv = QtWidgets.QGraphicsView(self._scene)
|
||||
self._videoitem = QtMultimediaWidgets.QGraphicsVideoItem()
|
||||
self._videoitem.setPos(0, 0)
|
||||
self._videoitem.setZValue(-1000)
|
||||
self._scene.addItem(self._videoitem)
|
||||
|
||||
if os.name != 'nt':
|
||||
# grab frames to forward them to facial emotion tab
|
||||
probe = QtMultimedia.QVideoProbe(self)
|
||||
probe.videoFrameProbed.connect(self.on_videoFrameProbed)
|
||||
probe.setSource(self.mediaPlayer)
|
||||
|
||||
# disable scrollbars
|
||||
self._gv.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self._gv.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
|
||||
# just a holder for the graphics view to expand to maximum to use full size
|
||||
self.lay = QtWidgets.QVBoxLayout(self)
|
||||
self.lay.setContentsMargins(0, 0, 0, 0)
|
||||
self.lay.addWidget(self._gv)
|
||||
|
||||
self.errorLabel = QtWidgets.QLabel()
|
||||
self.errorLabel.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
|
||||
QtWidgets.QSizePolicy.Maximum)
|
||||
|
||||
self.mediaPlayer.setVideoOutput(self._videoitem)
|
||||
self.mediaPlayer.stateChanged.connect(self.on_stateChanged)
|
||||
self.mediaPlayer.positionChanged.connect(self.mediaChangedPosition)
|
||||
# self.mediaPlayer.durationChanged.connect(self.durationChanged)
|
||||
self.mediaPlayer.error.connect(self.handleError)
|
||||
|
||||
self.movieDir = ''
|
||||
self.duration = 0
|
||||
|
||||
def setInit(self, video, fps, originalVideoResolution, number_ids, colors, tags, tag_colors):
|
||||
self.fps = fps
|
||||
self.originalVideoResolution = originalVideoResolution
|
||||
|
||||
f = os.path.abspath(video)
|
||||
self.mediaPlayer.setMedia(QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile(f)))
|
||||
self.mediaPlayer.setNotifyInterval(1000 // self.fps)
|
||||
|
||||
# init pose data
|
||||
for i in range(number_ids):
|
||||
self.pose_data.append(self._scene.addPath(QtGui.QPainterPath()))
|
||||
|
||||
# init gaze data
|
||||
for i in range(number_ids):
|
||||
self.gaze_data.append(self._scene.addPath(QtGui.QPainterPath()))
|
||||
|
||||
# init label data
|
||||
for i in range(number_ids):
|
||||
self.labels.append(self._scene.addPath(QtGui.QPainterPath()))
|
||||
|
||||
# init tag data
|
||||
if tags:
|
||||
for i, tag in enumerate(tags):
|
||||
self.tag_data[tag] = self._scene.addPath(QtGui.QPainterPath())
|
||||
self.tag_colors[tag] = tag_colors[i]
|
||||
|
||||
|
||||
self.number_ids = number_ids
|
||||
self.colors = colors
|
||||
self.tags = tags
|
||||
|
||||
def play(self):
|
||||
if self.mediaPlayer.state() == QtMultimedia.QMediaPlayer.PlayingState:
|
||||
self.mediaPlayer.pause()
|
||||
else:
|
||||
self.mediaPlayer.play()
|
||||
self.sendState.emit(self.mediaPlayer.state())
|
||||
|
||||
def pause(self):
|
||||
if self.mediaPlayer.state() == QtMultimedia.QMediaPlayer.PlayingState:
|
||||
self.mediaPlayer.pause()
|
||||
self.sendState.emit(self.mediaPlayer.state())
|
||||
|
||||
def setFrame(self, frame):
|
||||
# RESPECT FPS! position is time in millisconds
|
||||
position = int(frame * 1000 / self.fps)
|
||||
# print("Received", position)
|
||||
self.mediaPlayer.setPosition(position)
|
||||
|
||||
def stop(self):
|
||||
self.mediaPlayer.stop()
|
||||
|
||||
@QtCore.pyqtSlot(QtMultimedia.QMediaPlayer.State)
|
||||
def on_stateChanged(self, state):
|
||||
self.focus_on_video()
|
||||
|
||||
def mediaChangedPosition(self, position):
|
||||
frame = int((position / 1000.0) * self.fps)
|
||||
# print("Video Running %i" % frame)
|
||||
self.updateFrame.emit(frame)
|
||||
self._gv.fitInView(self._videoitem, QtCore.Qt.KeepAspectRatio)
|
||||
|
||||
def handleError(self):
|
||||
# self.playButton.setEnabled(False)
|
||||
print("Error: " + self.mediaPlayer.errorString())
|
||||
|
||||
def createButtons(self):
|
||||
iconSize = QtCore.QSize(28, 28)
|
||||
|
||||
openButton = QtWidgets.QToolButton()
|
||||
openButton.setStyleSheet('border: none;')
|
||||
openButton.setIcon(QtGui.QIcon(self.root + '/icons/open.png'))
|
||||
openButton.setIconSize(iconSize)
|
||||
openButton.setToolTip("Open File")
|
||||
# openButton.clicked.connect(self.open)
|
||||
|
||||
self.playButton = QtWidgets.QToolButton()
|
||||
self.playButton.setStyleSheet('border: none;')
|
||||
self.playButton.setIcon(QtGui.QIcon(self.root + '/icons/play.png'))
|
||||
self.playButton.setIconSize(iconSize)
|
||||
self.playButton.setToolTip("Play movie")
|
||||
self.playButton.clicked.connect(self.play)
|
||||
self.playButton.setEnabled(False)
|
||||
|
||||
self.stopButton = QtWidgets.QToolButton()
|
||||
self.stopButton.setStyleSheet('border: none;')
|
||||
self.stopButton.setIcon(QtGui.QIcon(self.root + '/icons/stop.png'))
|
||||
self.stopButton.setIconSize(iconSize)
|
||||
self.stopButton.setToolTip("Stop movie")
|
||||
self.stopButton.clicked.connect(self.stop)
|
||||
self.stopButton.setEnabled(False)
|
||||
|
||||
@QtCore.pyqtSlot(QtMultimedia.QVideoFrame)
|
||||
def on_videoFrameProbed(self, frame):
|
||||
cloneFrame = QtMultimedia.QVideoFrame(frame)
|
||||
cloneFrame.map(QtMultimedia.QAbstractVideoBuffer.ReadOnly)
|
||||
image = QtGui.QImage(cloneFrame.bits(), cloneFrame.width(), cloneFrame.height(), cloneFrame.bytesPerLine(),
|
||||
QtMultimedia.QVideoFrame.imageFormatFromPixelFormat(cloneFrame.pixelFormat()))
|
||||
self.frameAvailable.emit(image)
|
||||
cloneFrame.unmap()
|
||||
|
||||
def focus_on_video(self):
|
||||
native_video_resolution = self.mediaPlayer.metaData("Resolution")
|
||||
# we also update the sceneview to zoom to the video
|
||||
if native_video_resolution is not None:
|
||||
self._videoitem.setSize(QtCore.QSizeF(native_video_resolution.width(), native_video_resolution.height()))
|
||||
self._gv.fitInView(self._videoitem, QtCore.Qt.KeepAspectRatio)
|
||||
|
||||
# set scale of video to bigger size
|
||||
if self.originalVideoResolution is not None:
|
||||
width_ratio = self.originalVideoResolution[0] / native_video_resolution.width()
|
||||
self._videoitem.setScale(width_ratio)
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def clear_tags(self):
|
||||
self.focus_on_video()
|
||||
# clear all tags
|
||||
for tag in self.tags:
|
||||
self._scene.removeItem(self.tag_data[tag])
|
||||
self.tag_data[tag] = self._scene.addPath(QtGui.QPainterPath())
|
||||
|
||||
@QtCore.pyqtSlot(int, list, list)
|
||||
def draw_tags(self, tag, lstX, lstY):
|
||||
# this is removing the old tag data
|
||||
self._scene.removeItem(self.tag_data[tag])
|
||||
|
||||
path = QtGui.QPainterPath()
|
||||
path.setFillRule(Qt.Qt.WindingFill)
|
||||
# set starting points
|
||||
for (x, y) in zip(lstX, lstY):
|
||||
path.addRect(x-50, y-50, 100, 100)
|
||||
|
||||
# by adding it gets converted into an QGraphicsPathItem
|
||||
# save it for later removal
|
||||
self.tag_data[tag] = self._scene.addPath(path)
|
||||
|
||||
# set colors
|
||||
color = self.tag_colors[tag]
|
||||
pen = QtGui.QPen(QtGui.QColor(color[0], color[1], color[2], 255), 2, QtCore.Qt.SolidLine)
|
||||
self.tag_data[tag].setPen(pen)
|
||||
# fill ellipses - alpha value is set to 50%
|
||||
# self.tag_data[tag].setBrush(QtGui.QColor(color[0], color[1], color[2], int(0.5 * 255)))
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def clear_labels(self):
|
||||
self.focus_on_video()
|
||||
# clear all labels
|
||||
for id_no in range(self.number_ids):
|
||||
self._scene.removeItem(self.labels[id_no])
|
||||
self.labels[id_no] = self._scene.addPath(QtGui.QPainterPath())
|
||||
|
||||
@QtCore.pyqtSlot(int, int, int)
|
||||
def draw_labels(self, id_no, x, y):
|
||||
# this is removing the old pose data
|
||||
self._scene.removeItem(self.labels[id_no])
|
||||
|
||||
path = QtGui.QPainterPath()
|
||||
# then draw text
|
||||
font = QtGui.QFont("Arial", 70)
|
||||
font.setStyleStrategy(QtGui.QFont.ForceOutline)
|
||||
# sadly there is no easy way to claculate the width of the text so minus 100 is fine, but not ideal
|
||||
# also moving the text up by 500, so that is does not cover the face
|
||||
path.addText(x - 100, y - 300, font, "ID " + str(id_no))
|
||||
|
||||
# by adding it gets converted into an QGraphicsPathItem
|
||||
# save it for later removal
|
||||
self.labels[id_no] = self._scene.addPath(path)
|
||||
|
||||
# set colors
|
||||
color = tuple([int(a * 255) for a in self.colors[id_no]])
|
||||
# alpha value is set to 70%
|
||||
pen = QtGui.QPen(QtGui.QColor(color[0], color[1], color[2], int(0.9 * 255)), 10, QtCore.Qt.SolidLine)
|
||||
self.labels[id_no].setPen(pen)
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def clear_pose(self):
|
||||
# empty pose data
|
||||
for id_no in range(self.number_ids):
|
||||
self._scene.removeItem(self.pose_data[id_no])
|
||||
self.pose_data[id_no] = self._scene.addPath(QtGui.QPainterPath())
|
||||
|
||||
@QtCore.pyqtSlot(int, list, list)
|
||||
def draw_pose(self, id_no, lstX, lstY):
|
||||
# this is removing the old pose data
|
||||
self._scene.removeItem(self.pose_data[id_no])
|
||||
|
||||
if len(lstX) > 0 and len(lstY) > 0:
|
||||
path = QtGui.QPainterPath()
|
||||
|
||||
# set starting points
|
||||
path.moveTo(lstX[0], lstY[0])
|
||||
# then draw remaing lines
|
||||
for (x, y) in zip(lstX[1:], lstY[1:]):
|
||||
path.lineTo(x, y)
|
||||
|
||||
# by adding it gets converted into an QGraphicsPathItem
|
||||
# save it for later removal
|
||||
self.pose_data[id_no] = self._scene.addPath(path)
|
||||
|
||||
# set colors
|
||||
color = tuple([int(a * 255) for a in self.colors[id_no]])
|
||||
# alpha value is set to 70%
|
||||
pen = QtGui.QPen(QtGui.QColor(color[0], color[1], color[2], int(0.7 * 255)), 10, QtCore.Qt.SolidLine)
|
||||
self.pose_data[id_no].setPen(pen)
|
||||
else:
|
||||
self.pose_data[id_no] = self._scene.addPath(QtGui.QPainterPath())
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def clear_gaze(self):
|
||||
# empty pose data
|
||||
for id_no in range(self.number_ids):
|
||||
self._scene.removeItem(self.gaze_data[id_no])
|
||||
self.gaze_data[id_no] = self._scene.addPath(QtGui.QPainterPath())
|
||||
|
||||
@QtCore.pyqtSlot(int, list, list)
|
||||
def draw_gaze(self, id_no, lstX, lstY):
|
||||
# this is removing the old pose data
|
||||
self._scene.removeItem(self.gaze_data[id_no])
|
||||
|
||||
path = QtGui.QPainterPath()
|
||||
path.setFillRule(Qt.Qt.WindingFill)
|
||||
# set starting points
|
||||
for (x, y) in zip(lstX, lstY):
|
||||
path.addEllipse(x, y, 100, 100)
|
||||
|
||||
# by adding it gets converted into an QGraphicsPathItem
|
||||
# save it for later removal
|
||||
self.gaze_data[id_no] = self._scene.addPath(path)
|
||||
|
||||
# set colors
|
||||
color = tuple([int(a * 255) for a in self.colors[id_no]])
|
||||
# alpha value is set to 50%
|
||||
pen = QtGui.QPen(QtGui.QColor(color[0], color[1], color[2], int(0.5 * 255)), 1, QtCore.Qt.SolidLine)
|
||||
self.gaze_data[id_no].setPen(pen)
|
||||
# fill ellipses
|
||||
self.gaze_data[id_no].setBrush(QtGui.QColor(color[0], color[1], color[2], int(0.5 * 255)))
|
||||
|
||||
@QtCore.pyqtSlot(list)
|
||||
def onSelectedID(self, lst):
|
||||
self.clear_labels()
|
||||
self.clear_gaze()
|
||||
self.clear_pose()
|
||||
|
147
uiwidget/widgetpose.py
Normal file
147
uiwidget/widgetpose.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtWidgets, QtGui, QtCore
|
||||
|
||||
class WidgetPose(QtWidgets.QWidget):
|
||||
|
||||
video_label_signal = QtCore.pyqtSignal(list)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(WidgetPose, self).__init__(parent)
|
||||
|
||||
layout = QtWidgets.QGridLayout()
|
||||
self.poseGraph = QtWidgets.QWidget()
|
||||
self.measures = QtWidgets.QWidget()
|
||||
self.measuresLayout = QtWidgets.QHBoxLayout()
|
||||
|
||||
# Setup Movement Graph Plot Widget
|
||||
self.movementGraph = pg.PlotWidget()
|
||||
self.movementGraph.setBackground('w')
|
||||
self.movementGraph.setYRange(0, 400, padding=0)
|
||||
self.movementGraph.getPlotItem().getAxis('bottom').setTickSpacing(minor=50, major=100)
|
||||
self.movementGraph.getPlotItem().setTitle(title='Body Movement over Time')
|
||||
self.movementPlots = []
|
||||
|
||||
layout.addWidget(self.movementGraph, 1, 0, 1, 1)
|
||||
layout.addWidget(self.measures, 1, 1, 1, 1)
|
||||
layout.setColumnStretch(0, 1)
|
||||
layout.setColumnStretch(1, 1)
|
||||
|
||||
self.setLayout(layout)
|
||||
self.colors = None
|
||||
self.labels = dict()
|
||||
|
||||
@QtCore.pyqtSlot(list)
|
||||
def onSelectedID(self, lst):
|
||||
"""Change color of movement graph plot if ID should not be visible"""
|
||||
for i, button in enumerate(lst):
|
||||
if not button.isChecked():
|
||||
self.movementPlots[i].setPen(None)
|
||||
elif self.colors is not None:
|
||||
color = tuple([int(a * 255) for a in self.colors[i]])
|
||||
pen = pg.mkPen(color=color)
|
||||
self.movementPlots[i].setPen(pen)
|
||||
|
||||
@QtCore.pyqtSlot(dict, list, int)
|
||||
def updateMovementGraph(self, data, colors, numberIDs):
|
||||
"""Plot ID specific movement data from processing class
|
||||
data[id: (movements, frames)]
|
||||
"""
|
||||
# handle NaN https://github.com/pyqtgraph/pyqtgraph/issues/1057
|
||||
# downgrade to 5.13 fixes the issue
|
||||
for id_no in range(numberIDs):
|
||||
if data.get(id_no):
|
||||
if not np.all(np.isnan(data.get(id_no)[0])):
|
||||
self.movementPlots[id_no].setData(data.get(id_no)[1], data.get(id_no)[0]) # Update the data.
|
||||
|
||||
self.movementGraph.setXRange(np.min(data.get(id_no)[1]), np.max(data.get(id_no)[1]))
|
||||
|
||||
@QtCore.pyqtSlot(dict, int)
|
||||
def updateHandVelocity(self, data, numberIDs):
|
||||
"""Update Velocity Label
|
||||
data[id: velocity for frame]"""
|
||||
for id_no in range(numberIDs):
|
||||
if data.get(id_no) is not None:
|
||||
self.labels['Velocity'][id_no].setText('%.2f' % data[id_no])
|
||||
else:
|
||||
self.labels['Velocity'][id_no].setText(' ')
|
||||
|
||||
@QtCore.pyqtSlot(list, int)
|
||||
def initMovementGraph(self, colors, numberIDs):
|
||||
"""Initialize plot lines with 0
|
||||
colors: plot color for each ID
|
||||
"""
|
||||
for i in range(numberIDs):
|
||||
x = list(range(-200, 0)) # 100 time points
|
||||
y = [0 for _ in range(200)] # 100 data points
|
||||
color = tuple([int(a * 255) for a in colors[i]])
|
||||
pen = pg.mkPen(color=color)
|
||||
dataLine = self.movementGraph.plot(x, y, pen=pen)
|
||||
self.movementPlots.append(dataLine)
|
||||
|
||||
@QtCore.pyqtSlot(dict, dict, list, int)
|
||||
def setInit(self, mostActivity, hand, colors, numberIDs):
|
||||
self.colors = colors
|
||||
idLayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
labelID = QtWidgets.QLabel(' ')
|
||||
labelID.setFixedWidth(60)
|
||||
labelID.setFixedHeight(20)
|
||||
label = QtWidgets.QLabel('Most body activity in frame: ')
|
||||
labelA = QtWidgets.QLabel('Hands above table (relative): ')
|
||||
labelG = QtWidgets.QLabel('Gestures (relative): ')
|
||||
labelVel = QtWidgets.QLabel('Hand velocity: ')
|
||||
idLayout.addWidget(labelID)
|
||||
idLayout.addWidget(label)
|
||||
idLayout.addWidget(labelA)
|
||||
idLayout.addWidget(labelG)
|
||||
idLayout.addWidget(labelVel)
|
||||
self.measuresLayout.insertLayout(-1, idLayout)
|
||||
|
||||
activityLabel = []
|
||||
aboveLabel = []
|
||||
gestureLabel = []
|
||||
velLabel = []
|
||||
for id_no in range(numberIDs):
|
||||
idLayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
[total, tracked, high_vel] = hand.get(id_no)
|
||||
|
||||
color = tuple([int(a * 255) for a in colors[id_no]])
|
||||
|
||||
labelID = QtWidgets.QLabel('ID%i' % id_no)
|
||||
labelID.setStyleSheet('font: bold 12px; color: black; background-color: rgb(%i,%i,%i)' % color)
|
||||
labelID.setFixedWidth(60)
|
||||
labelID.setFixedHeight(20)
|
||||
|
||||
label = QtWidgets.QLabel('%i' % mostActivity.get(id_no))
|
||||
activityLabel.append(label)
|
||||
labelG = QtWidgets.QLabel('%.2f' % (high_vel / total))
|
||||
gestureLabel.append(labelG)
|
||||
labelA = QtWidgets.QLabel('%.2f' % (tracked / total))
|
||||
aboveLabel.append(labelA)
|
||||
labelVel = QtWidgets.QLabel(' ')
|
||||
velLabel.append(labelVel)
|
||||
idLayout.addWidget(labelID)
|
||||
idLayout.addWidget(label)
|
||||
idLayout.addWidget(labelA)
|
||||
idLayout.addWidget(labelG)
|
||||
idLayout.addWidget(labelVel)
|
||||
|
||||
self.measuresLayout.insertLayout(-1, idLayout)
|
||||
|
||||
# Velocity will be updated each frame, rest is updated in _updateLabels
|
||||
self.labels['Velocity'] = velLabel
|
||||
self.labels['Above'] = aboveLabel
|
||||
self.labels['Gesture'] = gestureLabel
|
||||
self.labels['Activity'] = activityLabel
|
||||
self.measures.setLayout(self.measuresLayout)
|
||||
|
||||
@QtCore.pyqtSlot(dict, dict, int)
|
||||
def updateLables(self, mostActivity, hand, numberIDs):
|
||||
""" Update above hands, gestures and most activity labels when segment was changed"""
|
||||
for id_no in range(numberIDs):
|
||||
[total, tracked, high_vel] = hand[id_no]
|
||||
self.labels['Activity'][id_no].setText('%i' % mostActivity[id_no])
|
||||
self.labels['Above'][id_no].setText('%.2f' % (tracked / total))
|
||||
self.labels['Gesture'][id_no].setText('%.2f' % (high_vel / total))
|
157
uiwidget/widgetspeaking.py
Normal file
157
uiwidget/widgetspeaking.py
Normal file
|
@ -0,0 +1,157 @@
|
|||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtWidgets, QtCore
|
||||
from utils.util import get_circle
|
||||
|
||||
|
||||
class WidgetSpeaking(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(WidgetSpeaking, self).__init__(parent)
|
||||
|
||||
layout = QtWidgets.QGridLayout()
|
||||
|
||||
self.measures = QtWidgets.QWidget()
|
||||
self.measuresLayout = QtWidgets.QHBoxLayout()
|
||||
|
||||
# Setup Speaking Graph Plot Widget
|
||||
self.speakingGraph = pg.PlotWidget()
|
||||
self.speakingGraph.setBackground('w')
|
||||
self.speakingGraph.hideAxis('left')
|
||||
self.speakingGraph.hideAxis('bottom')
|
||||
self.speakingGraph.setAspectLocked()
|
||||
self.speakingGraph.setYRange(-2.25, 2.25, padding=0)
|
||||
self.speakingGraph.setXRange(-2.25, 2.25, padding=0)
|
||||
self.speakingGraph.getPlotItem().setTitle(title='Speaking Distribution')
|
||||
|
||||
self.speakingGraphPlots = []
|
||||
|
||||
layout.addWidget(self.speakingGraph, 1, 0, 1, 1)
|
||||
layout.addWidget(self.measures, 1, 1, 1, 2)
|
||||
layout.setColumnStretch(0, 1)
|
||||
layout.setColumnStretch(1, 2)
|
||||
|
||||
self.setLayout(layout)
|
||||
self.colors = None
|
||||
self.labels = dict()
|
||||
self.speakBlob = None
|
||||
self.positions = dict()
|
||||
self.idBlobs = dict()
|
||||
|
||||
@QtCore.pyqtSlot(dict, int)
|
||||
def updateLables(self, speak, numberIDs):
|
||||
""" Update labels when segment was changed"""
|
||||
for id_no in range(numberIDs):
|
||||
self.labels['RST'][id_no].setText('%.2f' % speak[id_no][1])
|
||||
# self.labels['Turns'][id_no].setText('%i' % speak[id_no][2])
|
||||
# self.labels['TurnLength'][id_no].setText('%.2f sec' % speak[id_no][3])
|
||||
# self.labels['TurnPerMin'][id_no].setText('%.2f /min' % speak[id_no][4])
|
||||
|
||||
@QtCore.pyqtSlot(dict, list, int)
|
||||
def setInit(self, speak, colors, numberIDs):
|
||||
self.colors = colors
|
||||
idLayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
labelID = QtWidgets.QLabel(' ')
|
||||
labelID.setFixedWidth(60)
|
||||
labelID.setFixedHeight(20)
|
||||
labelRST = QtWidgets.QLabel('Relative speaking time:')
|
||||
labelTurn = QtWidgets.QLabel('Number of speaking turns:')
|
||||
labelTurnLen = QtWidgets.QLabel('Average length of turn:')
|
||||
labelTurnMin = QtWidgets.QLabel('Average number of turns:')
|
||||
idLayout.addWidget(labelID)
|
||||
idLayout.addWidget(labelRST)
|
||||
idLayout.addWidget(labelTurn)
|
||||
idLayout.addWidget(labelTurnLen)
|
||||
idLayout.addWidget(labelTurnMin)
|
||||
|
||||
self.measuresLayout.insertLayout(-1, idLayout)
|
||||
|
||||
rstLabels = []
|
||||
turnLabels = []
|
||||
turnLenLabels = []
|
||||
turnMinLabels = []
|
||||
for id_no in range(numberIDs):
|
||||
idLayout = QtWidgets.QVBoxLayout()
|
||||
color = tuple([int(a * 255) for a in colors[id_no]])
|
||||
|
||||
labelID = QtWidgets.QLabel('ID%i' % id_no)
|
||||
labelID.setFixedWidth(60)
|
||||
labelID.setFixedHeight(20)
|
||||
labelID.setStyleSheet('font: bold 12px; color: black; background-color: rgb(%i,%i,%i)' % color)
|
||||
|
||||
labelRST = QtWidgets.QLabel('%.2f' % speak[id_no][1])
|
||||
rstLabels.append(labelRST)
|
||||
labelTurn = QtWidgets.QLabel('%i' % speak[id_no][2])
|
||||
turnLabels.append(labelTurn)
|
||||
labelTurnLen = QtWidgets.QLabel('%.2f sec' % speak[id_no][3])
|
||||
turnLenLabels.append(labelTurnLen)
|
||||
labelTurnMin = QtWidgets.QLabel('%.2f /min' % speak[id_no][4])
|
||||
turnMinLabels.append(labelTurnMin)
|
||||
|
||||
idLayout.addWidget(labelID)
|
||||
idLayout.addWidget(labelRST)
|
||||
idLayout.addWidget(labelTurn)
|
||||
idLayout.addWidget(labelTurnLen)
|
||||
idLayout.addWidget(labelTurnMin)
|
||||
|
||||
self.measuresLayout.insertLayout(-1, idLayout)
|
||||
|
||||
self.labels['RST'] = rstLabels
|
||||
self.labels['Turns'] = turnLabels
|
||||
self.labels['TurnLength'] = turnLenLabels
|
||||
self.labels['TurnPerMin'] = turnMinLabels
|
||||
|
||||
self.measures.setLayout(self.measuresLayout)
|
||||
|
||||
@QtCore.pyqtSlot(list, int)
|
||||
def initSpeakingGraph(self, colors, numberIDs):
|
||||
""" initialize speaking graph """
|
||||
|
||||
self.speakBlob = pg.ScatterPlotItem([0], [0], size=0.3, pxMode=False,
|
||||
pen=pg.mkPen(color=0.5, width=2), brush=pg.mkBrush(255, 255, 255))
|
||||
self.speakBlob.setZValue(100)
|
||||
self.speakingGraph.addItem(self.speakBlob)
|
||||
|
||||
pos = 360 // numberIDs
|
||||
for id_no in range(numberIDs):
|
||||
color = tuple([int(a * 255) for a in colors[id_no]])
|
||||
idPos = pos * id_no
|
||||
x = 2 * np.cos(np.deg2rad(idPos))
|
||||
y = 2 * np.sin(np.deg2rad(idPos))
|
||||
scatterPlot = pg.ScatterPlotItem([x], [y], size=0.2, pxMode=False,
|
||||
pen=pg.mkPen(color=color, width=2), brush=pg.mkBrush(*color))
|
||||
self.speakingGraph.addItem(scatterPlot)
|
||||
self.positions[id_no] = [x, y]
|
||||
self.idBlobs[id_no] = scatterPlot
|
||||
|
||||
plt = self.speakingGraph.plot(x=[], y=[], pen=pg.mkPen(color=color, width=1))
|
||||
self.speakingGraphPlots.append(plt)
|
||||
|
||||
@QtCore.pyqtSlot(dict, int, int)
|
||||
def updateSpeakingGraph(self, rst, active_speaker, numberIDs):
|
||||
""" frame updates for gaze graph """
|
||||
blobPos = np.array([0, 0])
|
||||
for id_no in range(numberIDs):
|
||||
diff = rst[id_no] - (1.0 / numberIDs)
|
||||
if diff > 0:
|
||||
blobPos = blobPos + 2 * diff * np.array(self.positions[id_no])
|
||||
self.speakBlob.setData([blobPos[0]], [blobPos[1]])
|
||||
|
||||
for id_no in range(numberIDs):
|
||||
color = tuple([int(a * 255) for a in self.colors[id_no]])
|
||||
pen = pg.mkPen(color=color, width=(rst[id_no]*4))
|
||||
|
||||
x = [self.positions[id_no][0], blobPos[0]]
|
||||
y = [self.positions[id_no][1], blobPos[1]]
|
||||
self.speakingGraphPlots[id_no].setData(x, y)
|
||||
self.speakingGraphPlots[id_no].setPen(pen)
|
||||
|
||||
if id_no == active_speaker:
|
||||
self.idBlobs[id_no].setData([self.positions[id_no][0]], [self.positions[id_no][1]],
|
||||
pen=pg.mkPen(color=(0, 0, 0), width=2))
|
||||
else:
|
||||
self.idBlobs[id_no].setData([self.positions[id_no][0]], [self.positions[id_no][1]],
|
||||
pen=pg.mkPen(color=color, width=2))
|
||||
|
||||
|
||||
|
15
uiwidget/widgetstatic.py
Normal file
15
uiwidget/widgetstatic.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import numpy as np
|
||||
import pyqtgraph
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt5.QtWidgets import QLabel
|
||||
|
||||
|
||||
class WidgetStatic(QtWidgets.QWidget):
|
||||
labels = list()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(WidgetStatic, self).__init__(parent)
|
||||
|
||||
self.frame = 0
|
||||
|
||||
layout = QtWidgets.QGridLayout()
|
451
uiwidget/widgettimeline.py
Normal file
451
uiwidget/widgettimeline.py
Normal file
|
@ -0,0 +1,451 @@
|
|||
from PyQt5 import QtWidgets
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5.QtCore import QSize, QFileInfo, pyqtSlot
|
||||
from PyQt5.QtGui import QIcon, QKeySequence
|
||||
from PyQt5.QtMultimedia import QMediaPlayer
|
||||
|
||||
import sys
|
||||
|
||||
__all__ = ['QRangeSlider']
|
||||
|
||||
|
||||
def scale(val, src, dst):
|
||||
"""
|
||||
Scale the given value from the scale of src to the scale of dst.
|
||||
"""
|
||||
return int(((val - src[0]) / float(src[1] - src[0])) * (dst[1] - dst[0]) + dst[0])
|
||||
|
||||
|
||||
class WidgetTimeLine(QtWidgets.QWidget):
|
||||
|
||||
signalSetPosition = QtCore.pyqtSignal(int)
|
||||
signalPlay = QtCore.pyqtSignal()
|
||||
signalStop = QtCore.pyqtSignal()
|
||||
signalSelectID = QtCore.pyqtSignal(list)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(WidgetTimeLine, self).__init__(parent)
|
||||
self.root = QFileInfo(__file__).absolutePath()
|
||||
|
||||
self.setFixedHeight(110)
|
||||
self.frame = 0
|
||||
|
||||
layout = QtWidgets.QGridLayout()
|
||||
|
||||
iconSize = QSize(28, 28)
|
||||
|
||||
self.playButton = QtWidgets.QToolButton()
|
||||
self.playButton.setStyleSheet('border: none;')
|
||||
self.playButton.setIcon(QIcon(self.root + '/../icons/play.png'))
|
||||
self.playButton.setIconSize(iconSize)
|
||||
self.playButton.setToolTip("Play movie")
|
||||
self.playButton.clicked.connect(self.play)
|
||||
|
||||
# See https://stackoverflow.com/questions/50880660/how-to-change-space-bar-behaviour-in-pyqt5-python3
|
||||
# self.shortcut_play = QtWidgets.QShortcut(QKeySequence(Qt.Key_Space), self)
|
||||
# self.shortcut_play.activated.connect(self.play)
|
||||
|
||||
self.labelFrame = QtWidgets.QLabel('Frame\n')
|
||||
self.labelFrame.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.labelFrame.setFixedWidth(40)
|
||||
self.sl = QtWidgets.QSlider(QtCore.Qt.Horizontal)
|
||||
# Directly connect: Slider value changed to player set position
|
||||
self.sl.valueChanged.connect(self.signalSetPosition.emit)
|
||||
|
||||
self.rangeslider = QMultiRangeSlider()
|
||||
self.rangeslider.setFixedHeight(30)
|
||||
self.rangeslider.setMin(0)
|
||||
self.rangeslider.setMax(200)
|
||||
self.rangeslider.setRanges([(5, 25), (30, 50), (70, 90)])
|
||||
# self.rangeslider.setBackgroundStyle(
|
||||
# 'background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #222, stop:1 #333);')
|
||||
# szlf.rangeslider.setHandleStyle(
|
||||
# 'background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #289, stop:1 #289);')
|
||||
|
||||
layout.addWidget(self.rangeslider, 1, 1, 1, 6)
|
||||
layout.addWidget(self.labelFrame, 1, 0, 2, 1)
|
||||
layout.addWidget(self.sl, 2, 1, 1, 6)
|
||||
layout.addWidget(self.playButton, 2, 0, 1, 1)
|
||||
|
||||
self.setLayout(layout)
|
||||
self.number_ids = None
|
||||
self.fps = None
|
||||
|
||||
def setInit(self, frames, number_ids, fps):
|
||||
self.fps = fps
|
||||
self.frame = 0
|
||||
self.sl.setMinimum(0)
|
||||
# Use position = (frame * 1000) / FPS for all slider and player values!
|
||||
self.sl.setMaximum(int((frames * 1000) / self.fps))
|
||||
self.sl.setValue(0)
|
||||
self.sl.setTickPosition(QtWidgets.QSlider.TicksBelow)
|
||||
self.sl.setTickInterval(5)
|
||||
self.labelFrame.setText('Frame\n %i' % self.frame)
|
||||
self.playButton.setEnabled(True)
|
||||
self.number_ids = number_ids
|
||||
|
||||
self.rangeslider.setMin(0)
|
||||
self.rangeslider.setMax(frames)
|
||||
self.rangeslider.setRanges([(0, frames)])
|
||||
|
||||
@pyqtSlot(QMediaPlayer.State)
|
||||
def mediaStateChanged(self, state):
|
||||
if state == QMediaPlayer.PlayingState:
|
||||
print('MediaState changed...')
|
||||
self.playButton.setIcon(QIcon(self.root + '/../icons/pause.png'))
|
||||
self.playButton.setToolTip('Pause movie')
|
||||
else:
|
||||
self.playButton.setIcon(QIcon(self.root + '/../icons/play.png'))
|
||||
self.playButton.setToolTip('Play movie')
|
||||
|
||||
def play(self):
|
||||
self.signalPlay.emit()
|
||||
|
||||
def stop(self):
|
||||
self.signalStop.emit()
|
||||
|
||||
def updateSlider(self, position):
|
||||
# Directly connect Player position changed to slider set value
|
||||
frame = int((position / 1000.0) * self.fps)
|
||||
self.frame = frame
|
||||
# Disable the events to prevent updating triggering a setPosition event (can cause stuttering).
|
||||
self.sl.blockSignals(True)
|
||||
self.sl.setValue(position)
|
||||
self.sl.blockSignals(False)
|
||||
|
||||
self.labelFrame.setText('Frame\n %i' % frame)
|
||||
|
||||
|
||||
class QMultiRangeSlider(QtWidgets.QWidget):
|
||||
|
||||
minValueChanged = QtCore.pyqtSignal(int)
|
||||
maxValueChanged = QtCore.pyqtSignal(int)
|
||||
|
||||
segmentsChanged = QtCore.pyqtSignal(list, int)
|
||||
|
||||
segments = []
|
||||
last_update_segment = 0
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Create a new QRangeSlider instance.
|
||||
|
||||
:param parent: QWidget parent
|
||||
:return: New QRangeSlider instance.
|
||||
|
||||
"""
|
||||
super(QMultiRangeSlider, self).__init__(parent)
|
||||
self.setMinimumSize(1, 30)
|
||||
self.padding = 5
|
||||
self.hande_width = 4
|
||||
|
||||
self.selected_segment = None
|
||||
|
||||
self.setMin(0)
|
||||
self.setMax(99)
|
||||
self.setRanges([(20, 30), [40, 90]])
|
||||
|
||||
def setMin(self, value):
|
||||
"""sets minimum value"""
|
||||
assert type(value) is int
|
||||
setattr(self, '__min', value)
|
||||
self.minValueChanged.emit(value)
|
||||
self.segments = []
|
||||
|
||||
def setMax(self, value):
|
||||
"""sets maximum value"""
|
||||
assert type(value) is int
|
||||
setattr(self, '__max', value)
|
||||
self.maxValueChanged.emit(value)
|
||||
self.segments = []
|
||||
|
||||
def initSegment(self):
|
||||
self.segments = []
|
||||
|
||||
if (self.min() != None & self.max() != None):
|
||||
self.setRanges([(self.min(), self.max())])
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""overrides key press event to move range left and right"""
|
||||
key = event.key()
|
||||
if key == QtCore.Qt.Key_Left:
|
||||
s = self.start() - 1
|
||||
e = self.end() - 1
|
||||
elif key == QtCore.Qt.Key_Right:
|
||||
s = self.start() + 1
|
||||
e = self.end() + 1
|
||||
else:
|
||||
event.ignore()
|
||||
return
|
||||
event.accept()
|
||||
if s >= self.parent().min() and e <= self.max():
|
||||
self.setRange(s, e)
|
||||
|
||||
def min(self):
|
||||
""":return: minimum value"""
|
||||
return getattr(self, '__min', None)
|
||||
|
||||
def max(self):
|
||||
""":return: maximum value"""
|
||||
return getattr(self, '__max', None)
|
||||
|
||||
def _setStart(self, value):
|
||||
"""stores the start value only"""
|
||||
setattr(self, '__start', value)
|
||||
self.startValueChanged.emit(value)
|
||||
# self.segmentChanged.emit(value, self.end())
|
||||
|
||||
def setStart(self, values):
|
||||
"""sets the range slider start value"""
|
||||
# assert type(value) is int
|
||||
|
||||
for i, s in enumerate(self.segments):
|
||||
s.setStart(values[i])
|
||||
|
||||
def setEnd(self, values):
|
||||
"""set the range slider end value"""
|
||||
# assert type(value) is int
|
||||
|
||||
for i, s in enumerate(self.segments):
|
||||
s.setEnd(values[i])
|
||||
|
||||
def getRanges(self):
|
||||
""":return: the start and end values as a tuple"""
|
||||
ret = []
|
||||
for i, s in enumerate(self.segments):
|
||||
ret.append(s.getRange())
|
||||
|
||||
return ret
|
||||
|
||||
def setRanges(self, values):
|
||||
"""set the start and end values"""
|
||||
|
||||
while len(self.segments) < len(values):
|
||||
self.segments.append(QRangeSliderSegment(self))
|
||||
|
||||
while len(self.segments) > len(values):
|
||||
self.segments.remove(-1)
|
||||
|
||||
for i, (s, e) in enumerate(values):
|
||||
self.segments[i].setStart(s)
|
||||
self.segments[i].setEnd(e)
|
||||
|
||||
self._trigger_refresh(0)
|
||||
|
||||
def mouseDoubleClickEvent(self, e):
|
||||
self.selected_segment = None
|
||||
|
||||
d_width = self.width() - (self.padding * 2)
|
||||
step_size = d_width / (self.max() - self.min())
|
||||
pos = (e.x() - (self.padding)) / step_size
|
||||
|
||||
# removing existing segment
|
||||
for (i, s) in enumerate(self.segments):
|
||||
if (s.start() < pos) and (pos < s.end()):
|
||||
# always keep one segment
|
||||
if len(self.segments) > 1:
|
||||
self.segments.remove(s)
|
||||
# since one segement exists all the time the first can not be -1
|
||||
if i == 0:
|
||||
self._trigger_refresh(0)
|
||||
else:
|
||||
self._trigger_refresh(i - 1)
|
||||
return
|
||||
|
||||
# if new segment in front
|
||||
if (pos < self.segments[0].end()):
|
||||
start = 0
|
||||
end = self.segments[0].start()
|
||||
diff = end - start
|
||||
start += .25 * diff
|
||||
end -= .25 * diff
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
seg = QRangeSliderSegment(self)
|
||||
seg.setStart(start)
|
||||
seg.setEnd(end)
|
||||
self.segments.insert(0, seg)
|
||||
self._trigger_refresh(0)
|
||||
return
|
||||
|
||||
# if new segment in back
|
||||
if (pos > self.segments[-1].end()):
|
||||
start = self.segments[-1].end()
|
||||
end = self.max()
|
||||
diff = end - start
|
||||
start += .25 * diff
|
||||
end -= .25 * diff
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
seg = QRangeSliderSegment(self)
|
||||
seg.setStart(start)
|
||||
seg.setEnd(end)
|
||||
self.segments.append(seg)
|
||||
self._trigger_refresh(len(self.segments) - 1)
|
||||
return
|
||||
|
||||
# if new segment in between
|
||||
for i in range(len(self.segments) - 1):
|
||||
if (self.segments[i].end() < pos) and (pos < self.segments[i + 1].start()):
|
||||
start = self.segments[i].end()
|
||||
end = self.segments[i + 1].start()
|
||||
diff = end - start
|
||||
start += .25 * diff
|
||||
end -= .25 * diff
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
seg = QRangeSliderSegment(self)
|
||||
seg.setStart(start)
|
||||
seg.setEnd(end)
|
||||
self.segments.insert(i + 1, seg)
|
||||
self._trigger_refresh(i + 1)
|
||||
return
|
||||
|
||||
def mousePressEvent(self, e):
|
||||
# d_height = painter.device().height() - (self.padding * 2)
|
||||
d_width = self.width() - (self.padding * 2)
|
||||
step_size = d_width / (self.max() - self.min())
|
||||
pos = (e.x() - (self.padding)) / step_size
|
||||
|
||||
distance = sys.maxsize
|
||||
|
||||
for i, s in enumerate(self.segments):
|
||||
if (abs(s.start() - pos) < distance):
|
||||
distance = abs(s.start() - pos)
|
||||
if (distance * step_size < 10):
|
||||
self.selected_segment = {"Seg": s, "Type": "start", "Index": i}
|
||||
|
||||
if (abs(s.end() - pos) < distance):
|
||||
distance = abs(s.end() - pos)
|
||||
if (distance * step_size < 10):
|
||||
self.selected_segment = {"Seg": s, "Type": "end", "Index": i}
|
||||
|
||||
self._trigger_refresh(self.last_update_segment)
|
||||
|
||||
def mouseMoveEvent(self, e):
|
||||
d_width = self.width() - (self.padding * 2)
|
||||
step_size = d_width / (self.max() - self.min())
|
||||
pos = int(round((e.x() - (self.padding)) / step_size))
|
||||
|
||||
if self.selected_segment is None:
|
||||
return
|
||||
|
||||
s = self.selected_segment["Seg"]
|
||||
if (self.selected_segment["Type"] == "start"):
|
||||
if ((self.selected_segment["Index"] == 0) & (0 <= pos)):
|
||||
s.setStart(pos)
|
||||
elif (self.segments[self.selected_segment["Index"] - 1].end() < pos - 5) & (s.end() > pos + 5):
|
||||
s.setStart(pos)
|
||||
elif (self.selected_segment["Type"] == "end"):
|
||||
if (self.selected_segment["Index"] == len(self.segments) - 1) & (pos <= self.max()):
|
||||
s.setEnd(pos)
|
||||
elif self.selected_segment["Index"] + 1 < len(self.segments) and (self.segments[self.selected_segment["Index"] + 1].start() > pos + 5) and (s.start() < pos - 5):
|
||||
s.setEnd(pos)
|
||||
|
||||
self._trigger_refresh(self.selected_segment["Index"])
|
||||
|
||||
def mouseReleaseEvent(self, e):
|
||||
self.selected_segment = None
|
||||
|
||||
def _trigger_refresh(self, last_update_segment):
|
||||
self.last_update_segment = last_update_segment
|
||||
self.update()
|
||||
self.segmentsChanged.emit(self.getRanges(), last_update_segment)
|
||||
|
||||
def paintEvent(self, e):
|
||||
painter = QtGui.QPainter(self)
|
||||
brush = QtGui.QBrush()
|
||||
brush.setColor(QtGui.QColor(53, 53, 53))
|
||||
brush.setStyle(QtCore.Qt.SolidPattern)
|
||||
rect = QtCore.QRect(0, 0, painter.device().width(), painter.device().height())
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
# Define our canvas.
|
||||
d_height = painter.device().height() - (self.padding * 2)
|
||||
d_width = painter.device().width() - (self.padding * 2)
|
||||
step_size = d_width / (self.max() - self.min())
|
||||
|
||||
for s in self.segments:
|
||||
|
||||
brush.setColor(QtGui.QColor('white'))
|
||||
rect = QtCore.QRect(
|
||||
self.padding + s.start() * step_size,
|
||||
self.padding * 2,
|
||||
(s.end() - s.start()) * step_size,
|
||||
d_height - 2 * self.padding
|
||||
)
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
brush.setColor(QtGui.QColor('red'))
|
||||
rect = QtCore.QRect(
|
||||
self.padding + s.end() * step_size - (self.hande_width / 2),
|
||||
self.padding,
|
||||
(self.hande_width / 2),
|
||||
d_height
|
||||
)
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
brush.setColor(QtGui.QColor('green'))
|
||||
rect = QtCore.QRect(
|
||||
self.padding + s.start() * step_size - (self.hande_width / 2),
|
||||
self.padding,
|
||||
(self.hande_width / 2),
|
||||
d_height
|
||||
)
|
||||
painter.fillRect(rect, brush)
|
||||
|
||||
|
||||
class QRangeSliderSegment(QtWidgets.QWidget):
|
||||
|
||||
# signals
|
||||
# startValueChanged = QtCore.pyqtSignal(int)
|
||||
# endValueChanged = QtCore.pyqtSignal(int)
|
||||
# segmentChanged = QtCore.pyqtSignal(int, int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Create a new QRangeSlider instance.
|
||||
|
||||
:param parent: QWidget parent
|
||||
:return: New QRangeSlider instance.
|
||||
|
||||
"""
|
||||
super(QRangeSliderSegment, self).__init__(parent)
|
||||
|
||||
def start(self):
|
||||
""":return: range slider start value"""
|
||||
return getattr(self, '__start', None)
|
||||
|
||||
def end(self):
|
||||
""":return: range slider end value"""
|
||||
return getattr(self, '__end', None)
|
||||
|
||||
def getRange(self):
|
||||
""":return: the start and end values as a tuple"""
|
||||
return (self.start(), self.end())
|
||||
|
||||
def setRange(self, start, end):
|
||||
"""set the start and end values"""
|
||||
self.setStart(start)
|
||||
self.setEnd(end)
|
||||
|
||||
def _setEnd(self, value):
|
||||
"""stores the end value only"""
|
||||
setattr(self, '__end', value)
|
||||
# self.endValueChanged.emit(value)
|
||||
# self.segmentChanged.emit(self.start(), value)
|
||||
|
||||
def setEnd(self, value):
|
||||
"""set the range slider end value"""
|
||||
assert type(value) is int
|
||||
self._setEnd(value)
|
||||
|
||||
def _setStart(self, value):
|
||||
"""stores the start value only"""
|
||||
setattr(self, '__start', value)
|
||||
# self.startValueChanged.emit(value)
|
||||
# self.segmentChanged.emit(value, self.end())
|
||||
|
||||
def setStart(self, value):
|
||||
"""sets the range slider start value"""
|
||||
assert type(value) is int
|
||||
self._setStart(value)
|
63
uiwidget/widgetvideosettings.py
Normal file
63
uiwidget/widgetvideosettings.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
|
||||
|
||||
class WidgetVideoSettings(QtWidgets.QWidget):
|
||||
signalVisualize = QtCore.pyqtSignal(dict)
|
||||
signalSelectID = QtCore.pyqtSignal(list)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(WidgetVideoSettings, self).__init__(parent)
|
||||
|
||||
self.setFixedWidth(200)
|
||||
|
||||
layout = QtWidgets.QGridLayout()
|
||||
|
||||
iconSize = QtCore.QSize(28, 28)
|
||||
|
||||
self.checkBoxes = QtWidgets.QWidget()
|
||||
self.checkBoxesLayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
label = QtWidgets.QLabel('Visualize')
|
||||
label.setFixedWidth(100)
|
||||
|
||||
self.checkBoxes.setLayout(self.checkBoxesLayout)
|
||||
self.checkBoxesList = dict()
|
||||
|
||||
for data in ['Pose', 'Gaze', 'Label', 'Tags']:
|
||||
box = QtWidgets.QCheckBox(data)
|
||||
box.setChecked(True)
|
||||
box.stateChanged.connect(lambda: self.boxstate(box))
|
||||
self.checkBoxesLayout.addWidget(box)
|
||||
self.checkBoxesList[data] = box
|
||||
|
||||
self.idButtons = QtWidgets.QWidget()
|
||||
self.buttonsLayout = QtWidgets.QVBoxLayout()
|
||||
|
||||
labelID = QtWidgets.QLabel('Showing')
|
||||
labelID.setFixedWidth(100)
|
||||
|
||||
self.idButtons.setLayout(self.buttonsLayout)
|
||||
self.buttonsList = []
|
||||
|
||||
layout.addWidget(label, 0, 0)
|
||||
layout.addWidget(labelID, 0, 1)
|
||||
layout.addWidget(self.checkBoxes, 1, 0)
|
||||
layout.addWidget(self.idButtons, 1, 1)
|
||||
self.setLayout(layout)
|
||||
|
||||
@QtCore.pyqtSlot(list, int)
|
||||
def setInit(self, colors, numberIDs):
|
||||
# Emit initial state
|
||||
self.signalVisualize.emit(self.checkBoxesList)
|
||||
for id_no in range(numberIDs):
|
||||
button = QtWidgets.QCheckBox("ID%i" % id_no)
|
||||
button.setChecked(True)
|
||||
button.stateChanged.connect(lambda: self.btnstate(button))
|
||||
self.buttonsLayout.addWidget(button)
|
||||
self.buttonsList.append(button)
|
||||
|
||||
def btnstate(self, b):
|
||||
self.signalSelectID.emit(self.buttonsList)
|
||||
|
||||
def boxstate(self, b):
|
||||
self.signalVisualize.emit(self.checkBoxesList)
|
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
13
utils/colors.py
Normal file
13
utils/colors.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
import colorsys
|
||||
|
||||
|
||||
def random_colors(N, bright=True):
|
||||
"""
|
||||
Generate random colors.
|
||||
To get visually distinct colors, generate them in HSV space then
|
||||
convert to RGB.
|
||||
"""
|
||||
brightness = 1.0 if bright else 0.7
|
||||
hsv = [(i / N, 1, brightness) for i in range(N)]
|
||||
colors = list(map(lambda c: colorsys.hsv_to_rgb(*c), hsv))
|
||||
return colors
|
31
utils/util.py
Normal file
31
utils/util.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
import numpy as np
|
||||
|
||||
|
||||
def sperical2equirec(theta, phi, img_w, img_h):
|
||||
u = np.deg2rad(theta) / (2 * np.pi)
|
||||
v = np.deg2rad(phi) / np.pi
|
||||
|
||||
x = u * img_w
|
||||
y = v * img_h
|
||||
return x, y
|
||||
|
||||
|
||||
def equirec2spherical(x, y, img_w, img_h):
|
||||
# Normalize coordinates
|
||||
u = x / img_w
|
||||
v = y / img_h
|
||||
|
||||
# Calculate spherical coordinates from normalized coordinates
|
||||
# theta is horizontal angle, phi is polar angle
|
||||
theta = u * 2 * np.pi
|
||||
phi = v * np.pi
|
||||
|
||||
return np.rad2deg(theta), np.rad2deg(phi)
|
||||
|
||||
|
||||
def get_circle(radius):
|
||||
""" helper function returns circle to x, y coordinates"""
|
||||
theta = np.linspace(0, 2 * np.pi, 100)
|
||||
x = radius * np.cos(theta)
|
||||
y = radius * np.sin(theta)
|
||||
return np.array(x), np.array(y)
|
Loading…
Reference in a new issue