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/displayarray/frame/frame_publishing.py b/displayarray/frame/frame_publishing.py index 5e4ad68..5dae1a2 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,73 @@ 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( # type: ignore + f"/dev/video{cam_id}", *request_size + ) + else: + cam = pyv4lcamera(cam_id, *request_size) # type: ignore + 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 +169,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 +197,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 +245,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/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' 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()