QVideo 3.2.3__tar.gz → 3.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qvideo-3.2.3 → qvideo-3.4.0}/PKG-INFO +2 -2
- {qvideo-3.2.3 → qvideo-3.4.0}/QVideo.egg-info/PKG-INFO +2 -2
- {qvideo-3.2.3 → qvideo-3.4.0}/QVideo.egg-info/SOURCES.txt +7 -1
- {qvideo-3.2.3 → qvideo-3.4.0}/README.md +1 -1
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/OpenCV/_resolution_tree.py +4 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/demos/__init__.py +7 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/demos/demo.py +4 -2
- {qvideo-3.2.3 → qvideo-3.4.0}/demos/filterdemo.py +1 -11
- qvideo-3.4.0/demos/trackpydemo.py +53 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/dvr/QDVRWidget.py +3 -2
- {qvideo-3.2.3 → qvideo-3.4.0}/filters/QBlurFilter.py +2 -2
- {qvideo-3.2.3 → qvideo-3.4.0}/filters/QEdgeFilter.py +6 -6
- {qvideo-3.2.3 → qvideo-3.4.0}/filters/QRGBFilter.py +2 -2
- {qvideo-3.2.3 → qvideo-3.4.0}/filters/QSampleHold.py +7 -5
- qvideo-3.4.0/filters/QYOLOFilter.py +89 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/filters/_MedianBase.py +1 -1
- {qvideo-3.2.3 → qvideo-3.4.0}/filters/__init__.py +2 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/QFilterBank.py +10 -3
- qvideo-3.2.3/lib/VideoFilter.py → qvideo-3.4.0/lib/QVideoFilter.py +14 -14
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/QVideoScreen.py +14 -8
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/__init__.py +3 -3
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/resolutions.py +3 -1
- qvideo-3.4.0/overlays/__init__.py +5 -0
- qvideo-3.4.0/overlays/trackpy.py +244 -0
- qvideo-3.4.0/overlays/yolo.py +21 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/pyproject.toml +3 -1
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_demo.py +8 -3
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qblurfilter.py +1 -1
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qdvrwidget.py +2 -3
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qedgefilter.py +1 -1
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qfilterbank.py +3 -3
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qopencvresolutiontree.py +15 -1
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qrgbfilter.py +1 -1
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qsamplehold.py +1 -1
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_resolutions.py +20 -4
- qvideo-3.4.0/tests/test_trackpy_overlay.py +346 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_videofilter.py +6 -6
- {qvideo-3.2.3 → qvideo-3.4.0}/LICENSE.md +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/QCamcorder.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/QVideo.egg-info/dependency_links.txt +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/QVideo.egg-info/requires.txt +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/QVideo.egg-info/top_level.txt +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/__init__.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Basler/__init__.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Basler/_camera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Basler/_tree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Flir/QListFlirCameras.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Flir/__init__.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Flir/_camera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Flir/_tree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Genicam/__init__.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Genicam/_camera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Genicam/_tree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/IDS/__init__.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/IDS/_camera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/IDS/_tree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/MV/__init__.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/MV/_camera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/MV/_tree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Noise/__init__.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Noise/_camera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Noise/_tree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/OpenCV/QListCVCameras.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/OpenCV/__init__.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/OpenCV/_camera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/OpenCV/_tree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Picamera/__init__.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Picamera/_camera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Picamera/_tree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Vimbax/__init__.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Vimbax/_camera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/Vimbax/_tree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/cameras/__init__.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/demos/ROIdemo.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/dvr/QHDF5Reader.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/dvr/QHDF5Writer.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/dvr/QOpenCVReader.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/dvr/QOpenCVWriter.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/dvr/__init__.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/dvr/icons_rc.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/dvr/icons_rc_qt6.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/filters/Median.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/filters/MoMedian.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/filters/Normalize.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/QCamera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/QCameraTree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/QFPSMeter.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/QListCameras.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/QVideoReader.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/QVideoSource.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/QVideoWriter.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/chooser.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/clickable.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/lib/types.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/setup.cfg +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_chooser.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_clickable.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_filterdemo.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_icons_rc.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_median.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_momedian.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_normalize.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qbaslercamera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qcamcorder.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qcamera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qcameratree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qflircamera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qfpsmeter.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qgenicamcamera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qgenicamtree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qhdf5reader.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qhdf5writer.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qidscamera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qlistcameras.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qlistcvcameras.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qmvcamera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qnoisecamera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qnoisetree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qopencvcamera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qopencvreader.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qopencvtree.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qopencvwriter.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qpicamera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qvideoreader.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qvideoscreen.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qvideosource.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qvideowriter.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_qvimbaxcamera.py +0 -0
- {qvideo-3.2.3 → qvideo-3.4.0}/tests/test_roidemo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: QVideo
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: PyQt-based framework for integrating video cameras into research applications
|
|
5
5
|
Author-email: "David G. Grier" <david.grier@nyu.edu>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -157,5 +157,5 @@ additional code needed.
|
|
|
157
157
|
## Acknowledgements
|
|
158
158
|
|
|
159
159
|
Work on this project at New York University is supported by the National
|
|
160
|
-
Science Foundation of the United States under award number DMR-
|
|
160
|
+
Science Foundation of the United States under award number DMR-2428983 and
|
|
161
161
|
by an award from the TAC Program of New York University.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: QVideo
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: PyQt-based framework for integrating video cameras into research applications
|
|
5
5
|
Author-email: "David G. Grier" <david.grier@nyu.edu>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -157,5 +157,5 @@ additional code needed.
|
|
|
157
157
|
## Acknowledgements
|
|
158
158
|
|
|
159
159
|
Work on this project at New York University is supported by the National
|
|
160
|
-
Science Foundation of the United States under award number DMR-
|
|
160
|
+
Science Foundation of the United States under award number DMR-2428983 and
|
|
161
161
|
by an award from the TAC Program of New York University.
|
|
@@ -45,6 +45,7 @@ demos/ROIdemo.py
|
|
|
45
45
|
demos/__init__.py
|
|
46
46
|
demos/demo.py
|
|
47
47
|
demos/filterdemo.py
|
|
48
|
+
demos/trackpydemo.py
|
|
48
49
|
dvr/QDVRWidget.py
|
|
49
50
|
dvr/QHDF5Reader.py
|
|
50
51
|
dvr/QHDF5Writer.py
|
|
@@ -60,6 +61,7 @@ filters/QBlurFilter.py
|
|
|
60
61
|
filters/QEdgeFilter.py
|
|
61
62
|
filters/QRGBFilter.py
|
|
62
63
|
filters/QSampleHold.py
|
|
64
|
+
filters/QYOLOFilter.py
|
|
63
65
|
filters/_MedianBase.py
|
|
64
66
|
filters/__init__.py
|
|
65
67
|
lib/QCamera.py
|
|
@@ -67,16 +69,19 @@ lib/QCameraTree.py
|
|
|
67
69
|
lib/QFPSMeter.py
|
|
68
70
|
lib/QFilterBank.py
|
|
69
71
|
lib/QListCameras.py
|
|
72
|
+
lib/QVideoFilter.py
|
|
70
73
|
lib/QVideoReader.py
|
|
71
74
|
lib/QVideoScreen.py
|
|
72
75
|
lib/QVideoSource.py
|
|
73
76
|
lib/QVideoWriter.py
|
|
74
|
-
lib/VideoFilter.py
|
|
75
77
|
lib/__init__.py
|
|
76
78
|
lib/chooser.py
|
|
77
79
|
lib/clickable.py
|
|
78
80
|
lib/resolutions.py
|
|
79
81
|
lib/types.py
|
|
82
|
+
overlays/__init__.py
|
|
83
|
+
overlays/trackpy.py
|
|
84
|
+
overlays/yolo.py
|
|
80
85
|
tests/test_chooser.py
|
|
81
86
|
tests/test_clickable.py
|
|
82
87
|
tests/test_demo.py
|
|
@@ -120,4 +125,5 @@ tests/test_qvideowriter.py
|
|
|
120
125
|
tests/test_qvimbaxcamera.py
|
|
121
126
|
tests/test_resolutions.py
|
|
122
127
|
tests/test_roidemo.py
|
|
128
|
+
tests/test_trackpy_overlay.py
|
|
123
129
|
tests/test_videofilter.py
|
|
@@ -114,5 +114,5 @@ additional code needed.
|
|
|
114
114
|
## Acknowledgements
|
|
115
115
|
|
|
116
116
|
Work on this project at New York University is supported by the National
|
|
117
|
-
Science Foundation of the United States under award number DMR-
|
|
117
|
+
Science Foundation of the United States under award number DMR-2428983 and
|
|
118
118
|
by an award from the TAC Program of New York University.
|
|
@@ -117,8 +117,12 @@ class QOpenCVResolutionTree(QCameraTree):
|
|
|
117
117
|
if change == 'value' and param.name() == 'resolution':
|
|
118
118
|
if value in self._resolutionMap:
|
|
119
119
|
w, h = self._resolutionMap[value]
|
|
120
|
+
fps = (self.camera.fps
|
|
121
|
+
if 'fps' in self.camera._properties else None)
|
|
120
122
|
self.camera.set('width', w)
|
|
121
123
|
self.camera.set('height', h)
|
|
124
|
+
if fps is not None:
|
|
125
|
+
self.camera.set('fps', fps)
|
|
122
126
|
return
|
|
123
127
|
super()._sync(root, changes)
|
|
124
128
|
|
|
@@ -21,6 +21,11 @@ ROIdemo
|
|
|
21
21
|
rectangular ROI overlay. Only the cropped region is saved when
|
|
22
22
|
the DVR records, making it easy to capture a sub-region of interest.
|
|
23
23
|
|
|
24
|
+
trackpydemo
|
|
25
|
+
Extends :mod:`demo` with a :class:`~QVideo.overlays.trackpy.QTrackpyWidget`
|
|
26
|
+
panel so that trackpy particle positions are overlaid on the live feed
|
|
27
|
+
in real time and locate results are available via a signal.
|
|
28
|
+
|
|
24
29
|
Running
|
|
25
30
|
-------
|
|
26
31
|
Each demo can be launched directly::
|
|
@@ -28,6 +33,7 @@ Each demo can be launched directly::
|
|
|
28
33
|
python -m QVideo.demos.demo
|
|
29
34
|
python -m QVideo.demos.filterdemo
|
|
30
35
|
python -m QVideo.demos.ROIdemo
|
|
36
|
+
python -m QVideo.demos.trackpydemo
|
|
31
37
|
|
|
32
38
|
Camera selection
|
|
33
39
|
----------------
|
|
@@ -59,4 +65,5 @@ Example::
|
|
|
59
65
|
from .demo import Demo
|
|
60
66
|
from .filterdemo import FilterDemo
|
|
61
67
|
from .ROIdemo import ROIFilter, ROIDemo
|
|
68
|
+
from .trackpydemo import TrackpyDemo
|
|
62
69
|
|
|
@@ -7,7 +7,7 @@ Run directly::
|
|
|
7
7
|
'''
|
|
8
8
|
|
|
9
9
|
from QVideo.lib import QVideoScreen, QCameraTree
|
|
10
|
-
from pyqtgraph.Qt.QtWidgets import QWidget, QHBoxLayout
|
|
10
|
+
from pyqtgraph.Qt.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
__all__ = ['Demo']
|
|
@@ -40,9 +40,11 @@ class Demo(QWidget):
|
|
|
40
40
|
def _setupUi(self) -> None:
|
|
41
41
|
layout = QHBoxLayout(self)
|
|
42
42
|
layout.addWidget(self.screen)
|
|
43
|
-
|
|
43
|
+
self._controls = QVBoxLayout()
|
|
44
|
+
layout.addLayout(self._controls)
|
|
44
45
|
layout.setStretch(0, 1) # screen takes all surplus horizontal space
|
|
45
46
|
layout.setStretch(1, 0) # controls stay at their natural width
|
|
47
|
+
self._controls.addWidget(self.cameraTree)
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
def main() -> None: # pragma: no cover
|
|
@@ -8,7 +8,6 @@ Run directly::
|
|
|
8
8
|
|
|
9
9
|
from QVideo.demos.demo import Demo
|
|
10
10
|
from QVideo.lib import QCameraTree
|
|
11
|
-
from pyqtgraph.Qt.QtWidgets import QHBoxLayout, QVBoxLayout
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
__all__ = ['FilterDemo']
|
|
@@ -38,15 +37,6 @@ class FilterDemo(Demo):
|
|
|
38
37
|
super().__init__(cameraTree, **kwargs)
|
|
39
38
|
self.addFilters(filters)
|
|
40
39
|
|
|
41
|
-
def _setupUi(self) -> None:
|
|
42
|
-
layout = QHBoxLayout(self)
|
|
43
|
-
layout.addWidget(self.screen)
|
|
44
|
-
self._controls = QVBoxLayout()
|
|
45
|
-
layout.addLayout(self._controls)
|
|
46
|
-
layout.setStretch(0, 1) # screen takes all surplus horizontal space
|
|
47
|
-
layout.setStretch(1, 0) # controls stay at their natural width
|
|
48
|
-
self._controls.addWidget(self.cameraTree)
|
|
49
|
-
|
|
50
40
|
def addFilters(self, filters: list[str]) -> None:
|
|
51
41
|
'''Register filters by name and show the filter bank.
|
|
52
42
|
|
|
@@ -68,7 +58,7 @@ def main() -> None: # pragma: no cover
|
|
|
68
58
|
|
|
69
59
|
pg.mkQApp()
|
|
70
60
|
camera = choose_camera().start()
|
|
71
|
-
filters = 'QRGBFilter QBlurFilter
|
|
61
|
+
filters = 'QYOLOFilter QRGBFilter QSampleHold QBlurFilter QEdgeFilter'.split()
|
|
72
62
|
widget = FilterDemo(camera, filters)
|
|
73
63
|
widget.show()
|
|
74
64
|
pg.exec()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
'''Demo combining a live video feed, camera controls, and a trackpy overlay.
|
|
3
|
+
|
|
4
|
+
Run directly::
|
|
5
|
+
|
|
6
|
+
python -m QVideo.demos.trackpydemo
|
|
7
|
+
'''
|
|
8
|
+
|
|
9
|
+
from QVideo.demos.demo import Demo
|
|
10
|
+
from QVideo.lib import QCameraTree
|
|
11
|
+
from QVideo.overlays import QTrackpyWidget
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = ['TrackpyDemo']
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TrackpyDemo(Demo):
|
|
18
|
+
'''Extends :class:`~QVideo.demos.demo.Demo` with a trackpy overlay.
|
|
19
|
+
|
|
20
|
+
Adds a :class:`~QVideo.overlays.trackpy.QTrackpyWidget` control panel
|
|
21
|
+
below the camera control tree. Detected particle positions are
|
|
22
|
+
rendered in real time as a scatter-plot overlay on the video screen.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
cameraTree : QCameraTree
|
|
27
|
+
The camera control tree widget to display alongside the video feed.
|
|
28
|
+
**kwargs :
|
|
29
|
+
Additional keyword arguments forwarded to :class:`~QVideo.demos.demo.Demo`.
|
|
30
|
+
'''
|
|
31
|
+
|
|
32
|
+
def __init__(self, cameraTree: QCameraTree, **kwargs) -> None:
|
|
33
|
+
super().__init__(cameraTree, **kwargs)
|
|
34
|
+
self.trackpy = QTrackpyWidget(self)
|
|
35
|
+
self.trackpy.source = self.screen.source
|
|
36
|
+
self.trackpy.attachTo(self.screen)
|
|
37
|
+
self._controls.addWidget(self.trackpy)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main() -> None: # pragma: no cover
|
|
41
|
+
'''Launch the trackpy demo with an interactively chosen camera.'''
|
|
42
|
+
import pyqtgraph as pg
|
|
43
|
+
from QVideo.lib import choose_camera
|
|
44
|
+
|
|
45
|
+
pg.mkQApp()
|
|
46
|
+
camera = choose_camera().start()
|
|
47
|
+
widget = TrackpyDemo(camera)
|
|
48
|
+
widget.show()
|
|
49
|
+
pg.exec()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == '__main__': # pragma: no cover
|
|
53
|
+
main()
|
|
@@ -120,7 +120,7 @@ class QDVRWidget(QtWidgets.QFrame):
|
|
|
120
120
|
def __init__(self,
|
|
121
121
|
*args,
|
|
122
122
|
source: QVideoSource | None = None,
|
|
123
|
-
filename: str
|
|
123
|
+
filename: str = '',
|
|
124
124
|
**kwargs) -> None:
|
|
125
125
|
super().__init__(*args, **kwargs)
|
|
126
126
|
self._source = None
|
|
@@ -130,7 +130,7 @@ class QDVRWidget(QtWidgets.QFrame):
|
|
|
130
130
|
self._setupUi()
|
|
131
131
|
self._connectSignals()
|
|
132
132
|
self.source = source
|
|
133
|
-
self.filename = filename if filename
|
|
133
|
+
self.filename = filename if filename else str(
|
|
134
134
|
Path.home() / self.FILENAME)
|
|
135
135
|
|
|
136
136
|
def _setupUi(self) -> None:
|
|
@@ -335,6 +335,7 @@ class QDVRWidget(QtWidgets.QFrame):
|
|
|
335
335
|
return
|
|
336
336
|
if not (self.isRecording() or self.isPlaying()):
|
|
337
337
|
self.saveEdit.setText(filename)
|
|
338
|
+
self.playname = filename
|
|
338
339
|
|
|
339
340
|
@QtCore.pyqtProperty(str)
|
|
340
341
|
def playname(self) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pyqtgraph.Qt import QtCore, QtWidgets
|
|
2
2
|
from pyqtgraph import SpinBox
|
|
3
|
-
from QVideo.lib.
|
|
3
|
+
from QVideo.lib.QVideoFilter import VideoFilter, QVideoFilter
|
|
4
4
|
from QVideo.lib.types import Image
|
|
5
5
|
import cv2
|
|
6
6
|
|
|
@@ -67,7 +67,7 @@ class QBlurFilter(QVideoFilter):
|
|
|
67
67
|
'''
|
|
68
68
|
|
|
69
69
|
def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
|
|
70
|
-
super().__init__('Gaussian Blur',
|
|
70
|
+
super().__init__(parent, 'Gaussian Blur', BlurFilter())
|
|
71
71
|
|
|
72
72
|
def _setupUi(self) -> None:
|
|
73
73
|
super()._setupUi()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pyqtgraph.Qt import QtCore, QtWidgets
|
|
2
2
|
from pyqtgraph import SpinBox
|
|
3
|
-
from QVideo.lib.
|
|
3
|
+
from QVideo.lib.QVideoFilter import VideoFilter, QVideoFilter
|
|
4
4
|
from QVideo.lib.types import Image
|
|
5
5
|
import cv2
|
|
6
6
|
import logging
|
|
@@ -49,8 +49,8 @@ class EdgeFilter(VideoFilter):
|
|
|
49
49
|
def low(self, low: int) -> None:
|
|
50
50
|
low = max(1, int(low))
|
|
51
51
|
if low >= self._high:
|
|
52
|
-
logger.warning('low (
|
|
53
|
-
'
|
|
52
|
+
logger.warning(f'low ({low}) must be less than '
|
|
53
|
+
f'high ({self._high}); ignoring')
|
|
54
54
|
return
|
|
55
55
|
self._low = low
|
|
56
56
|
|
|
@@ -63,8 +63,8 @@ class EdgeFilter(VideoFilter):
|
|
|
63
63
|
def high(self, high: int) -> None:
|
|
64
64
|
high = max(2, int(high))
|
|
65
65
|
if high <= self._low:
|
|
66
|
-
logger.warning('high (
|
|
67
|
-
'
|
|
66
|
+
logger.warning(f'high ({high}) must be greater than '
|
|
67
|
+
f'low ({self._low}); ignoring')
|
|
68
68
|
return
|
|
69
69
|
self._high = high
|
|
70
70
|
|
|
@@ -106,7 +106,7 @@ class QEdgeFilter(QVideoFilter):
|
|
|
106
106
|
'''
|
|
107
107
|
|
|
108
108
|
def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
|
|
109
|
-
super().__init__('Canny Edge Detection',
|
|
109
|
+
super().__init__(parent, 'Canny Edge Detection', EdgeFilter())
|
|
110
110
|
|
|
111
111
|
def _setupUi(self) -> None:
|
|
112
112
|
super()._setupUi()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from pyqtgraph.Qt import QtCore, QtWidgets
|
|
2
|
-
from QVideo.lib.
|
|
2
|
+
from QVideo.lib.QVideoFilter import VideoFilter, QVideoFilter
|
|
3
3
|
from QVideo.lib.types import Image
|
|
4
4
|
|
|
5
5
|
|
|
@@ -61,7 +61,7 @@ class QRGBFilter(QVideoFilter):
|
|
|
61
61
|
'''
|
|
62
62
|
|
|
63
63
|
def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
|
|
64
|
-
super().__init__('Color Channel',
|
|
64
|
+
super().__init__(parent, 'Color Channel', RGBFilter())
|
|
65
65
|
|
|
66
66
|
def _setupUi(self) -> None:
|
|
67
67
|
super()._setupUi()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from pyqtgraph.Qt import QtCore, QtWidgets
|
|
2
|
-
from QVideo.lib.VideoFilter import QVideoFilter
|
|
3
|
-
from QVideo.lib.types import Image
|
|
4
2
|
from QVideo.filters.Normalize import Normalize
|
|
3
|
+
from QVideo.lib.QVideoFilter import QVideoFilter
|
|
4
|
+
from QVideo.lib.types import Image
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
__all__ = ['SampleHold', 'QSampleHold']
|
|
@@ -87,14 +87,16 @@ class QSampleHold(QVideoFilter):
|
|
|
87
87
|
'''
|
|
88
88
|
|
|
89
89
|
def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
|
|
90
|
-
super().__init__('Sample and Hold',
|
|
90
|
+
super().__init__(parent, 'Sample and Hold', SampleHold())
|
|
91
91
|
|
|
92
92
|
def _setupUi(self) -> None:
|
|
93
93
|
super()._setupUi()
|
|
94
94
|
self._layout.addWidget(QtWidgets.QLabel('order'))
|
|
95
|
-
self._orderButtons = [
|
|
95
|
+
self._orderButtons = [
|
|
96
|
+
QtWidgets.QRadioButton(str(n)) for n in (1, 2, 3)]
|
|
96
97
|
for n, button in enumerate(self._orderButtons, start=1):
|
|
97
|
-
button.toggled.connect(
|
|
98
|
+
button.toggled.connect(
|
|
99
|
+
lambda checked, n=n: self.setOrder(checked, n))
|
|
98
100
|
self._layout.addWidget(button)
|
|
99
101
|
self._orderButtons[self.filter.order - 1].setChecked(True)
|
|
100
102
|
self._resetButton = QtWidgets.QPushButton('Reset', self)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'''Real-time object detection with YOLO.'''
|
|
2
|
+
|
|
3
|
+
from pyqtgraph.Qt import QtCore, QtWidgets
|
|
4
|
+
from QVideo.lib.QVideoFilter import VideoFilter, QVideoFilter
|
|
5
|
+
from QVideo.lib.types import Image
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from ultralytics import YOLO
|
|
10
|
+
except ImportError:
|
|
11
|
+
YOLO = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = ['YOLOFilter', 'QYOLOFilter']
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class YOLOFilter(VideoFilter):
|
|
18
|
+
|
|
19
|
+
'''YOLO object detection filter.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
model_name: str
|
|
24
|
+
Name of the YOLO model weights file.
|
|
25
|
+
Default: ``'yolo11n.pt'``.
|
|
26
|
+
passthrough: bool
|
|
27
|
+
If True, return the input image.
|
|
28
|
+
If False, mark up the image with bounding box data
|
|
29
|
+
'''
|
|
30
|
+
|
|
31
|
+
#: Emitted with bounding boxes of detected features.
|
|
32
|
+
featuresReady = QtCore.pyqtSignal(np.ndarray)
|
|
33
|
+
|
|
34
|
+
def __init__(self,
|
|
35
|
+
model_name: str = 'yolo11n.pt',
|
|
36
|
+
passthrough: bool = False) -> None:
|
|
37
|
+
super().__init__()
|
|
38
|
+
if YOLO is None:
|
|
39
|
+
raise ImportError(
|
|
40
|
+
'YOLO is required for QYOLOFilter.'
|
|
41
|
+
'\n\tInstall it with: pip install ultralytics'
|
|
42
|
+
'\n\tSee https://docs.ultralytics.com/ '
|
|
43
|
+
'for more information.')
|
|
44
|
+
try:
|
|
45
|
+
self.model = YOLO(model_name)
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
raise FileNotFoundError(
|
|
48
|
+
f'YOLO model "{model_name}" not found.'
|
|
49
|
+
'\n\tProvide the name of a pretrained ultralytics model'
|
|
50
|
+
'\n\tor the full path to a custom YOLO weights file.'
|
|
51
|
+
'\n\tSee https://docs.ultralytics.com/models/ '
|
|
52
|
+
'for available pretrained models.')
|
|
53
|
+
self._passthrough = passthrough
|
|
54
|
+
|
|
55
|
+
def add(self, image: Image) -> None:
|
|
56
|
+
'''Performs YOLO feature detection on image
|
|
57
|
+
|
|
58
|
+
Emits featuresReady with bounding-box data
|
|
59
|
+
'''
|
|
60
|
+
results = self.model(image, verbose=False)
|
|
61
|
+
boxes = results[0].boxes.xyxy.numpy()
|
|
62
|
+
self.featuresReady.emit(boxes)
|
|
63
|
+
self.data = image if self._passthrough else results[0].plot()
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def passthrough(self) -> bool:
|
|
67
|
+
return self._passthrough
|
|
68
|
+
|
|
69
|
+
@passthrough.setter
|
|
70
|
+
def passthrough(self, value: bool) -> None:
|
|
71
|
+
self._passthrough = bool(value)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class QYOLOFilter(QVideoFilter):
|
|
75
|
+
|
|
76
|
+
'''QVideoFilter wrapper for YOLOFilter.'''
|
|
77
|
+
|
|
78
|
+
def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
|
|
79
|
+
super().__init__(parent, 'YOLO Filter', YOLOFilter())
|
|
80
|
+
|
|
81
|
+
def _setupUi(self) -> None:
|
|
82
|
+
super()._setupUi()
|
|
83
|
+
self._checkbox = QtWidgets.QCheckBox('Passthrough')
|
|
84
|
+
self._layout.addWidget(self._checkbox)
|
|
85
|
+
self._checkbox.stateChanged.connect(self._setPassthrough)
|
|
86
|
+
|
|
87
|
+
@QtCore.pyqtSlot(int)
|
|
88
|
+
def _setPassthrough(self, state: int) -> None:
|
|
89
|
+
self.filter.passthrough = state
|
|
@@ -5,6 +5,7 @@ from .QBlurFilter import BlurFilter, QBlurFilter
|
|
|
5
5
|
from .QEdgeFilter import EdgeFilter, QEdgeFilter
|
|
6
6
|
from .QRGBFilter import RGBFilter, QRGBFilter
|
|
7
7
|
from .QSampleHold import SampleHold, QSampleHold
|
|
8
|
+
from .QYOLOFilter import YOLOFilter, QYOLOFilter
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
__all__ = '''
|
|
@@ -14,4 +15,5 @@ BlurFilter QBlurFilter
|
|
|
14
15
|
EdgeFilter QEdgeFilter
|
|
15
16
|
RGBFilter QRGBFilter
|
|
16
17
|
SampleHold QSampleHold
|
|
18
|
+
YOLOFilter QYOLOFilter
|
|
17
19
|
'''.split()
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
'''Composable pipeline of VideoFilter stages between a source and a display.'''
|
|
2
|
+
import logging
|
|
2
3
|
from typing import Iterator
|
|
3
4
|
|
|
4
5
|
from pyqtgraph.Qt import QtWidgets
|
|
5
|
-
from QVideo.lib.
|
|
6
|
+
from QVideo.lib.QVideoFilter import QVideoFilter
|
|
6
7
|
from QVideo.lib.types import Image
|
|
7
8
|
import QVideo.filters as videofilters
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
__all__ = ['QFilterBank']
|
|
11
12
|
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
class QFilterBank(QtWidgets.QGroupBox):
|
|
14
17
|
|
|
@@ -72,7 +75,8 @@ class QFilterBank(QtWidgets.QGroupBox):
|
|
|
72
75
|
If *video_filter* is not a :class:`~QVideo.lib.VideoFilter.QVideoFilter`.
|
|
73
76
|
'''
|
|
74
77
|
if not isinstance(video_filter, QVideoFilter):
|
|
75
|
-
raise TypeError(
|
|
78
|
+
raise TypeError('expected QVideoFilter, '
|
|
79
|
+
f'got {type(video_filter).__name__}')
|
|
76
80
|
self._filters.append(video_filter)
|
|
77
81
|
self._layout.addWidget(video_filter)
|
|
78
82
|
|
|
@@ -116,4 +120,7 @@ class QFilterBank(QtWidgets.QGroupBox):
|
|
|
116
120
|
if cls is None or not (isinstance(cls, type) and
|
|
117
121
|
issubclass(cls, QVideoFilter)):
|
|
118
122
|
raise ValueError(f'{name!r} is not a known filter')
|
|
119
|
-
|
|
123
|
+
try:
|
|
124
|
+
self.register(cls())
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.warning(f'Failed to instantiate filter {name!r}: {e}')
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'''Base classes for image-processing filters in the QVideo filter pipeline.'''
|
|
2
|
-
from pyqtgraph.Qt import QtWidgets
|
|
2
|
+
from pyqtgraph.Qt import QtCore, QtWidgets
|
|
3
3
|
from QVideo.lib.types import Image
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
__all__ = ['VideoFilter', 'QVideoFilter']
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class VideoFilter:
|
|
9
|
+
class VideoFilter(QtCore.QObject):
|
|
10
10
|
|
|
11
11
|
'''Base class for video filters.
|
|
12
12
|
|
|
@@ -20,6 +20,7 @@ class VideoFilter:
|
|
|
20
20
|
'''
|
|
21
21
|
|
|
22
22
|
def __init__(self) -> None:
|
|
23
|
+
super().__init__()
|
|
23
24
|
self.data: Image | None = None
|
|
24
25
|
|
|
25
26
|
def __call__(self, data: Image) -> Image:
|
|
@@ -80,21 +81,20 @@ class QVideoFilter(QtWidgets.QGroupBox):
|
|
|
80
81
|
|
|
81
82
|
Parameters
|
|
82
83
|
----------
|
|
84
|
+
parent : QtWidgets.QWidget
|
|
85
|
+
Parent widget.
|
|
83
86
|
title : str
|
|
84
87
|
Label displayed in the group box border.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
video_filter : VideoFilter or None
|
|
88
|
-
The filter to apply when enabled. Defaults to a passthrough
|
|
89
|
-
:class:`VideoFilter` if not provided.
|
|
88
|
+
videoFilter : VideoFilter
|
|
89
|
+
The filter to apply when enabled.
|
|
90
90
|
'''
|
|
91
91
|
|
|
92
92
|
def __init__(self,
|
|
93
|
+
parent: QtWidgets.QWidget,
|
|
93
94
|
title: str,
|
|
94
|
-
|
|
95
|
-
video_filter: VideoFilter | None = None) -> None:
|
|
95
|
+
videoFilter: VideoFilter) -> None:
|
|
96
96
|
super().__init__(title, parent)
|
|
97
|
-
self._filter =
|
|
97
|
+
self._filter = videoFilter
|
|
98
98
|
self._setupUi()
|
|
99
99
|
|
|
100
100
|
@property
|
|
@@ -103,10 +103,10 @@ class QVideoFilter(QtWidgets.QGroupBox):
|
|
|
103
103
|
return self._filter
|
|
104
104
|
|
|
105
105
|
@filter.setter
|
|
106
|
-
def filter(self,
|
|
107
|
-
if not isinstance(
|
|
108
|
-
raise TypeError(f'expected VideoFilter, got {type(
|
|
109
|
-
self._filter =
|
|
106
|
+
def filter(self, videoFilter: VideoFilter) -> None:
|
|
107
|
+
if not isinstance(videoFilter, VideoFilter):
|
|
108
|
+
raise TypeError(f'expected VideoFilter, got {type(videoFilter).__name__}')
|
|
109
|
+
self._filter = videoFilter
|
|
110
110
|
|
|
111
111
|
def __call__(self, image: Image) -> Image:
|
|
112
112
|
'''Apply the filter if enabled, otherwise return *image* unchanged.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'''Live video display widget with mouse-aware graphical overlay support.'''
|
|
2
|
-
from QVideo.lib import QVideoSource
|
|
2
|
+
from QVideo.lib.QVideoSource import QVideoSource
|
|
3
|
+
from QVideo.lib.QFilterBank import QFilterBank
|
|
3
4
|
from QVideo.lib.types import Image
|
|
4
5
|
from pyqtgraph.Qt import QtCore
|
|
5
6
|
import numpy as np
|
|
@@ -71,13 +72,17 @@ class QVideoScreen(GraphicsLayoutWidget):
|
|
|
71
72
|
|
|
72
73
|
@property
|
|
73
74
|
def framerate(self) -> int | None:
|
|
74
|
-
'''Maximum display frame rate [fps]
|
|
75
|
+
'''Maximum display frame rate [fps].
|
|
76
|
+
|
|
77
|
+
Accommodate high-speed cameras by throttling the rate at
|
|
78
|
+
which frames are displayed. ``None`` for no throttling.'''
|
|
75
79
|
return self._framerate
|
|
76
80
|
|
|
77
81
|
@framerate.setter
|
|
78
82
|
def framerate(self, framerate: int | None) -> None:
|
|
79
83
|
if framerate is not None and framerate <= 0:
|
|
80
|
-
raise ValueError(
|
|
84
|
+
raise ValueError('framerate must be positive, '
|
|
85
|
+
f'got {framerate}')
|
|
81
86
|
self._framerate = framerate
|
|
82
87
|
self._interval = 0 if framerate is None else int(1000 / framerate)
|
|
83
88
|
|
|
@@ -124,7 +129,7 @@ class QVideoScreen(GraphicsLayoutWidget):
|
|
|
124
129
|
self._pending = image
|
|
125
130
|
|
|
126
131
|
def sizeHint(self) -> QtCore.QSize:
|
|
127
|
-
'''Return the source
|
|
132
|
+
'''Return the source frame size as the preferred widget size.'''
|
|
128
133
|
if self.source is not None:
|
|
129
134
|
return self.source.shape
|
|
130
135
|
return super().sizeHint()
|
|
@@ -164,11 +169,12 @@ class QVideoScreen(GraphicsLayoutWidget):
|
|
|
164
169
|
|
|
165
170
|
@QtCore.pyqtSlot()
|
|
166
171
|
def _fitToVideo(self) -> None:
|
|
167
|
-
'''Resize the containing window
|
|
172
|
+
'''Resize the containing window to match the video aspect ratio.
|
|
168
173
|
|
|
169
|
-
Computes the height the widget should have given its current
|
|
170
|
-
the source aspect ratio, then adjusts the top-level
|
|
171
|
-
the difference.
|
|
174
|
+
Computes the height the widget should have given its current
|
|
175
|
+
width and the source aspect ratio, then adjusts the top-level
|
|
176
|
+
window height by the difference. Only has effect when a source
|
|
177
|
+
is connected.
|
|
172
178
|
'''
|
|
173
179
|
if not self.hasHeightForWidth():
|
|
174
180
|
return
|
|
@@ -3,18 +3,18 @@ from .clickable import clickable
|
|
|
3
3
|
from .QCamera import QCamera
|
|
4
4
|
from .QVideoSource import QVideoSource
|
|
5
5
|
from .QCameraTree import QCameraTree
|
|
6
|
+
from .QVideoScreen import QVideoScreen
|
|
6
7
|
from .QFilterBank import QFilterBank
|
|
8
|
+
from .QVideoFilter import VideoFilter, QVideoFilter
|
|
7
9
|
from .QVideoReader import QVideoReader
|
|
8
10
|
from .QVideoWriter import QVideoWriter
|
|
9
|
-
from .QVideoScreen import QVideoScreen
|
|
10
11
|
from .chooser import choose_camera
|
|
11
12
|
from .QListCameras import QListCameras
|
|
12
13
|
from .QFPSMeter import QFPSMeter
|
|
13
|
-
from .VideoFilter import VideoFilter
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
__all__ = '''Image
|
|
17
17
|
clickable choose_camera QListCameras
|
|
18
18
|
QCamera QVideoSource QCameraTree QFilterBank
|
|
19
19
|
QVideoReader QVideoWriter QVideoScreen
|
|
20
|
-
QFPSMeter VideoFilter'''.split()
|
|
20
|
+
QFPSMeter VideoFilter QVideoFilter'''.split()
|