moved quick start to front
This commit is contained in:
108
.gitignore
vendored
Normal file
108
.gitignore
vendored
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Python template
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
.static_storage/
|
||||||
|
.media/
|
||||||
|
local_settings.py
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
|
||||||
|
.idea/
|
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Josh Miklos
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
1
README.md
Normal file
1
README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
A threaded PubSub OpenCV interfaceREADME.md. Webcam and video feeds to multiple windows is supported.
|
1
README.txt
Normal file
1
README.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
A threaded PubSub OpenCV interfaceREADME.md. Webcam and video feeds to multiple windows is supported.
|
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .cv_pubsubs import *
|
1
cv_pubsubs/__init__.py
Normal file
1
cv_pubsubs/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import window_sub, webcam_pub
|
5
cv_pubsubs/webcam_pub/__init__.py
Normal file
5
cv_pubsubs/webcam_pub/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from .listen_default import listen_default
|
||||||
|
from .get_cam_ids import get_cam_ids
|
||||||
|
from .pub_cam import pub_cam_thread
|
||||||
|
from .frame_handler import frame_handler_thread
|
||||||
|
from .camctrl import CamCtrl
|
11
cv_pubsubs/webcam_pub/camctrl.py
Normal file
11
cv_pubsubs/webcam_pub/camctrl.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import pubsub
|
||||||
|
if False:
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
class CamCtrl:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def stop_cam(cam_id # type: Union[int, str]
|
||||||
|
):
|
||||||
|
pubsub.publish("cvcamhandlers." + str(cam_id) + ".cmd", 'q')
|
39
cv_pubsubs/webcam_pub/frame_handler.py
Normal file
39
cv_pubsubs/webcam_pub/frame_handler.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import pubsub
|
||||||
|
import numpy as np
|
||||||
|
import threading
|
||||||
|
from .listen_default import listen_default
|
||||||
|
from .pub_cam import pub_cam_thread
|
||||||
|
|
||||||
|
if False:
|
||||||
|
from typing import Union, Tuple, Any, Callable
|
||||||
|
|
||||||
|
|
||||||
|
def frame_handler_loop(cam_id, # type: Union[int, str]
|
||||||
|
frame_handler, # type: Callable[[np.ndarray, int], Any]
|
||||||
|
request_size=(1280, 720), # type: Tuple[int, int]
|
||||||
|
high_speed=False, # type: bool
|
||||||
|
fps_limit=240 # type: float
|
||||||
|
):
|
||||||
|
t = pub_cam_thread(cam_id, request_size, high_speed, fps_limit)
|
||||||
|
sub_cam = pubsub.subscribe("cvcams." + str(cam_id) + ".vid")
|
||||||
|
sub_owner = pubsub.subscribe("cvcamhandlers." + str(cam_id) + ".cmd")
|
||||||
|
msg_owner = ''
|
||||||
|
while msg_owner != 'q':
|
||||||
|
frame = listen_default(sub_cam, timeout=.1) # type: np.ndarray
|
||||||
|
if frame is not None:
|
||||||
|
frame = frame[0]
|
||||||
|
frame_handler(frame, cam_id)
|
||||||
|
msg_owner = listen_default(sub_owner, block=False, empty='')
|
||||||
|
pubsub.publish("cvcams." + str(cam_id) + ".cmd", 'q')
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
|
||||||
|
def frame_handler_thread(cam_id, # type: Union[int, str]
|
||||||
|
frame_handler, # type: Callable[[int, np.ndarray], Any]
|
||||||
|
request_size=(1280, 720), # type: Tuple[int, int]
|
||||||
|
high_speed=False, # type: bool
|
||||||
|
fps_limit=240 # type: float
|
||||||
|
): # type: (...) -> threading.Thread
|
||||||
|
t = threading.Thread(target=frame_handler_loop, args=(cam_id, frame_handler, request_size, high_speed, fps_limit))
|
||||||
|
t.start()
|
||||||
|
return t
|
16
cv_pubsubs/webcam_pub/get_cam_ids.py
Normal file
16
cv_pubsubs/webcam_pub/get_cam_ids.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import cv2
|
||||||
|
|
||||||
|
if False:
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def get_cam_ids(): # type: () -> List[int]
|
||||||
|
cam_list = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
cam = cv2.VideoCapture(len(cam_list))
|
||||||
|
if not cam.isOpened():
|
||||||
|
break
|
||||||
|
cam_list.append(len(cam_list))
|
||||||
|
|
||||||
|
return cam_list
|
18
cv_pubsubs/webcam_pub/listen_default.py
Normal file
18
cv_pubsubs/webcam_pub/listen_default.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
if False:
|
||||||
|
from typing import Any, Optional, queue
|
||||||
|
|
||||||
|
|
||||||
|
def listen_default(sub, # type: queue
|
||||||
|
block=True, # type: bool
|
||||||
|
timeout=None, # type: Optional[float]
|
||||||
|
empty=None # type: Any
|
||||||
|
): # type: (...)->Any
|
||||||
|
try:
|
||||||
|
msg = (sub.listen(block=block, timeout=timeout))
|
||||||
|
try:
|
||||||
|
msg = next(msg)['data']
|
||||||
|
except StopIteration:
|
||||||
|
msg = empty
|
||||||
|
except queue.Empty:
|
||||||
|
msg = empty
|
||||||
|
return msg
|
66
cv_pubsubs/webcam_pub/pub_cam.py
Normal file
66
cv_pubsubs/webcam_pub/pub_cam.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import pubsub
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from .listen_default import listen_default
|
||||||
|
|
||||||
|
if False:
|
||||||
|
from typing import Union, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
def pub_cam_loop(cam_id, # type: Union[int, str]
|
||||||
|
request_size=(1280, 720), # type: Tuple[int, int]
|
||||||
|
high_speed=False, # type: bool
|
||||||
|
fps_limit=240 # type: float
|
||||||
|
): # type: (...)->bool
|
||||||
|
"""Publishes whichever camera you select to cvcams.<cam_id>.vid
|
||||||
|
You can send a quit command 'q' to cvcams.<cam_id>.cmd
|
||||||
|
Status information, such as failure to open, will be posted to cvcams.<cam_id>.status
|
||||||
|
|
||||||
|
|
||||||
|
:param high_speed: Selects mjpeg transferring, which most cameras seem to support, so speed isn't limited
|
||||||
|
:param fps_limit: Limits the frames per second.
|
||||||
|
:param cam_id: An integer representing which webcam to use, or a string representing a video file.
|
||||||
|
:param request_size: A tuple with width, then height, to request the video size.
|
||||||
|
:return: True if loop ended normally, False if it failed somehow.
|
||||||
|
"""
|
||||||
|
sub = pubsub.subscribe("cvcams." + str(cam_id) + ".cmd")
|
||||||
|
msg = ''
|
||||||
|
cam = cv2.VideoCapture(cam_id)
|
||||||
|
# cam.set(cv2.CAP_PROP_CONVERT_RGB, 0)
|
||||||
|
|
||||||
|
if high_speed:
|
||||||
|
cam.set(cv2.CAP_PROP_FOURCC, cv2.CAP_OPENCV_MJPEG)
|
||||||
|
|
||||||
|
cam.set(cv2.CAP_PROP_FRAME_WIDTH, request_size[0])
|
||||||
|
cam.set(cv2.CAP_PROP_FRAME_HEIGHT, request_size[1])
|
||||||
|
|
||||||
|
if not cam.isOpened():
|
||||||
|
pubsub.publish("cvcams." + str(cam_id) + ".status", "failed")
|
||||||
|
return False
|
||||||
|
now = time.time()
|
||||||
|
while msg != 'q':
|
||||||
|
time.sleep(1. / (fps_limit - (time.time() - now)))
|
||||||
|
now = time.time()
|
||||||
|
(ret, frame) = cam.read() # type: Tuple[bool, np.ndarray ]
|
||||||
|
if ret is False or not isinstance(frame, np.ndarray):
|
||||||
|
cam.release()
|
||||||
|
pubsub.publish("cvcams." + str(cam_id) + ".status", "failed")
|
||||||
|
return False
|
||||||
|
pubsub.publish("cvcams." + str(cam_id) + ".vid", (frame,))
|
||||||
|
msg = listen_default(sub, block=False, empty='')
|
||||||
|
|
||||||
|
cam.release()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def pub_cam_thread(cam_id, # type: Union[int, str]
|
||||||
|
request_ize=(1280, 720), # type: Tuple[int, int]
|
||||||
|
high_speed=False, # type: bool
|
||||||
|
fps_limit=240 # type: float
|
||||||
|
):
|
||||||
|
# type: (...) -> threading.Thread
|
||||||
|
t = threading.Thread(target=pub_cam_loop, args=(cam_id, request_ize, high_speed, fps_limit))
|
||||||
|
t.start()
|
||||||
|
return t
|
1
cv_pubsubs/window_sub/__init__.py
Normal file
1
cv_pubsubs/window_sub/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .cv_window_sub import sub_win_loop, frame_dict
|
42
cv_pubsubs/window_sub/cv_window_sub.py
Normal file
42
cv_pubsubs/window_sub/cv_window_sub.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import cv2
|
||||||
|
from ..webcam_pub.camctrl import CamCtrl
|
||||||
|
|
||||||
|
if False:
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
frame_dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def destruct_windows(window_names, input_cams):
|
||||||
|
for name in window_names:
|
||||||
|
cv2.destroyWindow(name)
|
||||||
|
for c in input_cams:
|
||||||
|
CamCtrl.stop_cam(c)
|
||||||
|
|
||||||
|
def set_frames_from_callbacks(input_vid_global_names, callbacks, frame):
|
||||||
|
global frame_dict
|
||||||
|
|
||||||
|
if callbacks[frame % len(callbacks)] is not None:
|
||||||
|
frames = callbacks[frame % len(callbacks)](frame_dict[input_vid_global_names[frame]])
|
||||||
|
else:
|
||||||
|
frames = frame_dict[input_vid_global_names[frame]]
|
||||||
|
return frames
|
||||||
|
|
||||||
|
# todo: figure out how to get the red x button to work. Try: https://stackoverflow.com/a/37881722/782170
|
||||||
|
def sub_win_loop(
|
||||||
|
names, # type: List[str]
|
||||||
|
input_vid_global_names, # type: List[str]
|
||||||
|
callbacks=(None,),
|
||||||
|
input_cams=(0,)
|
||||||
|
):
|
||||||
|
global frame_dict
|
||||||
|
|
||||||
|
while True:
|
||||||
|
for i in range(len(input_vid_global_names)):
|
||||||
|
if input_vid_global_names[i] in frame_dict and frame_dict[input_vid_global_names[i]] is not None:
|
||||||
|
frames = set_frames_from_callbacks(input_vid_global_names, callbacks, i)
|
||||||
|
for f in range(len(frames)):
|
||||||
|
cv2.imshow(names[f % len(names)], frames[f])
|
||||||
|
if cv2.waitKey(1) & 0xFF == ord('q'):
|
||||||
|
destruct_windows(names, input_cams)
|
||||||
|
return
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
opencv-python
|
||||||
|
pubsub
|
||||||
|
numpy
|
19
setup.py
Normal file
19
setup.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from distutils.core import setup
|
||||||
|
setup(
|
||||||
|
name= 'cv_pubsubs',
|
||||||
|
packages = ['cv_pubsubs', 'cv_pubsubs.webcam_pub', 'cv_pubsubs.window_sub'],
|
||||||
|
version='0.1',
|
||||||
|
description='Pubsub interface for Python OpenCV',
|
||||||
|
author='Josh Miklos',
|
||||||
|
author_email='simulatorleek@gmail.com',
|
||||||
|
url='https://github.com/SimLeek/cv_pubsubs',
|
||||||
|
download_url='https://github.com/SimLeek/cv_pubsubs/archive/0.1.tar.gz',
|
||||||
|
keywords=['OpenCV', 'PubSub'],
|
||||||
|
license='MIT',
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 3 - Alpha',
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.6',
|
||||||
|
]
|
||||||
|
)
|
0
tests_interactive/__init__.py
Normal file
0
tests_interactive/__init__.py
Normal file
20
tests_interactive/test_pub_cam.py
Normal file
20
tests_interactive/test_pub_cam.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from cv_pubsubs import webcam_pub as w
|
||||||
|
import unittest as ut
|
||||||
|
|
||||||
|
|
||||||
|
class TestFrameHandler(ut.TestCase):
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
def test_handler(self):
|
||||||
|
|
||||||
|
def test_frame_handler(frame, cam_id):
|
||||||
|
if self.i == 200:
|
||||||
|
w.CamCtrl.stop_cam(cam_id)
|
||||||
|
if self.i % 100 == 0:
|
||||||
|
print(frame.shape)
|
||||||
|
self.i += 1
|
||||||
|
|
||||||
|
w.frame_handler_thread(0, test_frame_handler,
|
||||||
|
request_size=(1280, 720),
|
||||||
|
high_speed=True,
|
||||||
|
fps_limit=240)
|
22
tests_interactive/test_sub_win.py
Normal file
22
tests_interactive/test_sub_win.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import unittest as ut
|
||||||
|
from cv_pubsubs import window_sub as win
|
||||||
|
from cv_pubsubs import webcam_pub as cam
|
||||||
|
|
||||||
|
class TestSubWin(ut.TestCase):
|
||||||
|
|
||||||
|
def test_sub(self):
|
||||||
|
def cam_handler(frame, cam_id):
|
||||||
|
win.frame_dict[str(cam_id) + "Frame"] = (frame, frame)
|
||||||
|
|
||||||
|
t = cam.frame_handler_thread(0, cam_handler,
|
||||||
|
request_size=(1280, 720),
|
||||||
|
high_speed=True,
|
||||||
|
fps_limit=240
|
||||||
|
)
|
||||||
|
|
||||||
|
win.sub_win_loop(names=['cammy', 'cammy2'],
|
||||||
|
input_vid_global_names=[str(0) + "Frame"])
|
||||||
|
|
||||||
|
cam.CamCtrl.stop_cam(0)
|
||||||
|
|
||||||
|
t.join()
|
Reference in New Issue
Block a user