Merge pull request #37 from SimLeek/v4l2
Added high speed v4l2 webcams for linux.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.<cam_id>.Vid, using v4l2 instead of opencv.
|
||||
|
||||
You can send a quit command 'quit' 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.
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = 'displayarray'
|
||||
version = '0.7.3'
|
||||
version = '0.7.4'
|
||||
description = 'Tool for displaying numpy arrays.'
|
||||
authors = ['SimLeek <simulator.leek@gmail.com>']
|
||||
license = 'MIT'
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user