From 7a7c415e83180cab1b11b00c829e171ea1b4ac4d Mon Sep 17 00:00:00 2001 From: Josh Miklos Date: Mon, 13 Apr 2020 19:57:31 -0700 Subject: [PATCH 1/5] Added high speed v4l2 webcams for linux. --- displayarray/frame/frame_publishing.py | 92 ++++++++++++++++++++++++-- examples/looping/no_display.py | 6 +- tests/frame/test_frame_publishing.py | 14 ++-- 3 files changed, 98 insertions(+), 14 deletions(-) diff --git a/displayarray/frame/frame_publishing.py b/displayarray/frame/frame_publishing.py index 5e4ad68..ce371ea 100644 --- a/displayarray/frame/frame_publishing.py +++ b/displayarray/frame/frame_publishing.py @@ -3,8 +3,20 @@ import threading import time import asyncio - import cv2 +import warnings +import sys + +try: + if sys.platform == "linux": + from PyV4L2Cam.camera import Camera as pyv4lcamera + from PyV4L2Cam.controls import ControlIDs as pyv4lcontrolids +except ImportError: + warnings.warn("Could not import PyV4L2Cam on linux. Camera capture will be slow.") + warnings.warn( + "To install, run: pip install git+https://github.com/simleek/PyV4L2Cam.git" + ) + import numpy as np from displayarray.frame import subscriber_dictionary @@ -16,7 +28,71 @@ from typing import Union, Tuple, Optional, Dict, Any, List, Callable FrameCallable = Callable[[np.ndarray], Optional[np.ndarray]] -def pub_cam_loop( +def pub_cam_loop_pyv4l2( + cam_id: Union[int, str, np.ndarray], + request_size: Tuple[int, int] = (-1, -1), + high_speed: bool = True, + fps_limit: float = float("inf"), +): + """ + Publish whichever camera you select to CVCams..Vid, using v4l2 instead of opencv. + + You can send a quit command 'quit' to CVCams..Cmd + Status information, such as failure to open, will be posted to CVCams..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. + """ + name = uid_for_source(cam_id) + + if isinstance(cam_id, (int, str)): + if isinstance(cam_id, int): + cam: pyv4lcamera = pyv4lcamera(f"/dev/video{cam_id}", *request_size) + else: + cam: pyv4lcamera = pyv4lcamera(cam_id, *request_size) + else: + raise TypeError( + "Only strings or ints representing cameras are supported with v4l2." + ) + + subscriber_dictionary.register_cam(name) + + sub = subscriber_dictionary.cam_cmd_sub(name) + sub.return_on_no_data = "" + msg = "" + + if high_speed and cam.pixel_format != "MJPEG": + warnings.warn("Camera does not support high speed.") + + now = time.time() + while msg != "quit": + time.sleep(1.0 / (fps_limit - (time.time() - now))) + now = time.time() + frame_bytes = cam.get_frame() # type: bytes + + # Thanks: https://stackoverflow.com/a/21844162 + a = frame_bytes.find(b"\xff\xd8") + b = frame_bytes.find(b"\xff\xd9") + + if a == -1 or b == -1: + cam.close() + subscriber_dictionary.CV_CAMS_DICT[name].status_pub.publish("failed") + return False + else: + jpg = frame_bytes[a : b + 2] + frame = cv2.imdecode(np.fromstring(jpg, dtype=np.uint8), cv2.IMREAD_COLOR) + subscriber_dictionary.CV_CAMS_DICT[name].frame_pub.publish(frame) + msg = sub.get() + sub.release() + + cam.close() + return True + + +def pub_cam_loop_opencv( cam_id: Union[int, str, np.ndarray], request_size: Tuple[int, int] = (-1, -1), high_speed: bool = True, @@ -91,6 +167,14 @@ def pub_cam_thread( fps_limit: float = float("inf"), ) -> threading.Thread: """Run pub_cam_loop in a new thread. Starts on creation.""" + + if sys.platform == "linux" and ( + isinstance(cam_id, int) or (isinstance(cam_id, str) and "/dev/video" in cam_id) + ): + pub_cam_loop = pub_cam_loop_pyv4l2 + else: + pub_cam_loop = pub_cam_loop_opencv + t = threading.Thread( target=pub_cam_loop, args=(cam_id, request_ize, high_speed, fps_limit) ) @@ -111,7 +195,7 @@ async def publish_updates_zero_mq( prepend_topic="", flags=0, copy=True, - track=False + track=False, ): """Publish frames to ZeroMQ when they're updated.""" import zmq @@ -159,7 +243,7 @@ async def publish_updates_ros( node_name="displayarray", publisher_name="npy", rate_hz=None, - dtype=None + dtype=None, ): """Publish frames to ROS when they're updated.""" import rospy diff --git a/examples/looping/no_display.py b/examples/looping/no_display.py index 5d035b8..7e17d6b 100644 --- a/examples/looping/no_display.py +++ b/examples/looping/no_display.py @@ -1,14 +1,14 @@ -from displayarray import read_updates, end_feeds +from displayarray import read_updates, display import time import cProfile from examples.videos import test_video -def profile_reading(total_seconds=2): +def profile_reading(total_seconds=5): t_init = t01 = time.time() times = [] started = False - for up in read_updates(test_video, size=(1, 1)): + for up in display(0, size=(9999, 9999)): if up: t1 = time.time() if started: diff --git a/tests/frame/test_frame_publishing.py b/tests/frame/test_frame_publishing.py index 1b4c8d0..2f73124 100644 --- a/tests/frame/test_frame_publishing.py +++ b/tests/frame/test_frame_publishing.py @@ -1,4 +1,4 @@ -from displayarray.frame.frame_publishing import pub_cam_loop, pub_cam_thread +from displayarray.frame.frame_publishing import pub_cam_loop_opencv, pub_cam_thread import displayarray import mock import pytest @@ -12,7 +12,7 @@ import displayarray.frame.frame_publishing as fpub def test_pub_cam_loop_exit(): not_a_camera = mock.MagicMock() with pytest.raises(TypeError): - pub_cam_loop(not_a_camera) + pub_cam_loop_opencv(not_a_camera) def test_pub_cam_int(): @@ -38,7 +38,7 @@ def test_pub_cam_int(): cam_0 = subd.CV_CAMS_DICT["0"] = subd.Cam("0") with mock.patch.object(cam_0.frame_pub, "publish") as cam_pub: - pub_cam_loop(0, high_speed=False) + pub_cam_loop_opencv(0, high_speed=False) cam_pub.assert_has_calls([mock.call(img)] * 4) @@ -79,7 +79,7 @@ def test_pub_cam_fail(): with mock.patch.object( subd.CV_CAMS_DICT["0"].status_pub, "publish" ) as mock_fail_pub: - pub_cam_loop(0, high_speed=False) + pub_cam_loop_opencv(0, high_speed=False) mock_fail_pub.assert_called_once_with("failed") @@ -100,7 +100,7 @@ def test_pub_cam_high_speed(): mock_is_open.return_value = False - pub_cam_loop(0, request_size=(640, 480), high_speed=True) + pub_cam_loop_opencv(0, request_size=(640, 480), high_speed=True) mock_cam_set.assert_has_calls( [ @@ -130,7 +130,7 @@ def test_pub_cam_numpy(): mock_uidfs.return_value = "0" cam_0 = subd.CV_CAMS_DICT["0"] = subd.Cam("0") with mock.patch.object(cam_0.frame_pub, "publish") as cam_pub: - pub_cam_loop(img) + pub_cam_loop_opencv(img) cam_pub.assert_has_calls([mock.call(img)] * 3) subd.CV_CAMS_DICT = {} @@ -145,6 +145,6 @@ def test_pub_cam_thread(): pub_cam_thread(5) mock_thread.assert_called_once_with( - target=fpub.pub_cam_loop, args=(5, (-1, -1), True, float("inf")) + target=fpub.pub_cam_loop_opencv, args=(5, (-1, -1), True, float("inf")) ) thread_instance.start.assert_called_once() From 7e2fd0a96a0d08e369f752251c69b1970e567f36 Mon Sep 17 00:00:00 2001 From: Josh Miklos Date: Mon, 13 Apr 2020 20:00:34 -0700 Subject: [PATCH 2/5] updated version --- displayarray/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/displayarray/__init__.py b/displayarray/__init__.py index 277b1e9..4eeb746 100644 --- a/displayarray/__init__.py +++ b/displayarray/__init__.py @@ -4,7 +4,7 @@ Display any array, webcam, or video file. display is a function that displays these in their own windows. """ -__version__ = "0.7.3" +__version__ = "0.7.4" from .window.subscriber_windows import display, breakpoint_display, read_updates from .frame.frame_publishing import publish_updates_zero_mq, publish_updates_ros diff --git a/pyproject.toml b/pyproject.toml index b292ba6..8af3a72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = 'displayarray' -version = '0.7.3' +version = '0.7.4' description = 'Tool for displaying numpy arrays.' authors = ['SimLeek '] license = 'MIT' From fe95a539baa4fead5567506de33e8d021232667d Mon Sep 17 00:00:00 2001 From: Josh Miklos Date: Mon, 13 Apr 2020 20:07:27 -0700 Subject: [PATCH 3/5] fixed pub_cam_loop_pyv4l2 mypy, docstring --- displayarray/frame/frame_publishing.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/displayarray/frame/frame_publishing.py b/displayarray/frame/frame_publishing.py index ce371ea..e44099f 100644 --- a/displayarray/frame/frame_publishing.py +++ b/displayarray/frame/frame_publishing.py @@ -35,24 +35,24 @@ def pub_cam_loop_pyv4l2( fps_limit: float = float("inf"), ): """ - Publish whichever camera you select to CVCams..Vid, using v4l2 instead of opencv. + Publish whichever camera you select to CVCams..Vid, using v4l2 instead of opencv. - You can send a quit command 'quit' to CVCams..Cmd - Status information, such as failure to open, will be posted to CVCams..Status + You can send a quit command 'quit' to CVCams..Cmd + Status information, such as failure to open, will be posted to CVCams..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. - """ + :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. + """ name = uid_for_source(cam_id) if isinstance(cam_id, (int, str)): if isinstance(cam_id, int): - cam: pyv4lcamera = pyv4lcamera(f"/dev/video{cam_id}", *request_size) + cam: pyv4lcamera = pyv4lcamera(f"/dev/video{cam_id}", *request_size) # type: ignore else: - cam: pyv4lcamera = pyv4lcamera(cam_id, *request_size) + cam = pyv4lcamera(cam_id, *request_size) # type: ignore else: raise TypeError( "Only strings or ints representing cameras are supported with v4l2." From ac94d44511dd8b2b05402252d0e3822b917a352a Mon Sep 17 00:00:00 2001 From: Josh Miklos Date: Mon, 13 Apr 2020 20:11:26 -0700 Subject: [PATCH 4/5] black autoformat --- displayarray/frame/frame_publishing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/displayarray/frame/frame_publishing.py b/displayarray/frame/frame_publishing.py index e44099f..3473244 100644 --- a/displayarray/frame/frame_publishing.py +++ b/displayarray/frame/frame_publishing.py @@ -50,7 +50,9 @@ def pub_cam_loop_pyv4l2( if isinstance(cam_id, (int, str)): if isinstance(cam_id, int): - cam: pyv4lcamera = pyv4lcamera(f"/dev/video{cam_id}", *request_size) # type: ignore + cam: pyv4lcamera = pyv4lcamera( + f"/dev/video{cam_id}", *request_size + ) # type: ignore else: cam = pyv4lcamera(cam_id, *request_size) # type: ignore else: From bea9392bcea70c225dc67c5745704a3b150c195a Mon Sep 17 00:00:00 2001 From: Josh Miklos Date: Mon, 13 Apr 2020 20:16:18 -0700 Subject: [PATCH 5/5] fixed mypy --- displayarray/frame/frame_publishing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/displayarray/frame/frame_publishing.py b/displayarray/frame/frame_publishing.py index 3473244..5dae1a2 100644 --- a/displayarray/frame/frame_publishing.py +++ b/displayarray/frame/frame_publishing.py @@ -50,9 +50,9 @@ def pub_cam_loop_pyv4l2( if isinstance(cam_id, (int, str)): if isinstance(cam_id, int): - cam: pyv4lcamera = pyv4lcamera( + cam: pyv4lcamera = pyv4lcamera( # type: ignore f"/dev/video{cam_id}", *request_size - ) # type: ignore + ) else: cam = pyv4lcamera(cam_id, *request_size) # type: ignore else: