Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ jobs:
with:
python-version: 3.7
- uses: dschep/[email protected]
- name: Install dependencies
run: poetry install
- name: Install and build
run: |
python -m ensurepip --user
python -m pip install --upgrade pip --user
python -m pip install .[dev]
- name: Build with Poetry
run: poetry build
run: python setup.py sdist bdist_wheel
- name: Publish distribution 📦 to Test PyPI
uses: pypa/gh-action-pypi-publish@master
with:
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ jobs:
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- uses: dschep/[email protected]
- name: Install dependencies
run: poetry install
run: |
python -m ensurepip --user
python -m pip install --upgrade pip --user
python -m pip install .[dev]
- name: Test with tox
run: poetry run tox -p auto -o
run: tox -p auto -o
2 changes: 1 addition & 1 deletion displayarray/effects/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Effects to run on numpy arrays to make data clearer."""

from . import crop, lens, select_channels
from . import crop, lens, select_channels, overlay, transform
54 changes: 54 additions & 0 deletions displayarray/effects/overlay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Overlay functions."""


def overlay_transparent(background, overlay, x=None, y=None):
"""
Overlay a transparent image on top of a background image.

:param background: background rgb image to overlay on top of
:param overlay: rgba image to overlay on top of the background
:param x: leftmost part to overlay at on the background
:param y: topmost part to overlay at on the background
"""
# https://stackoverflow.com/a/54058766/782170
assert overlay.shape[2] == 4, "Overlay must be BGRA"

background_width = background.shape[1]
background_height = background.shape[0]

if (x is not None and x >= background_width) or (
y is not None and y >= background_height
):
return background

h, w = overlay.shape[0], overlay.shape[1]

if x is None:
x = int(background_width / 2 - w / 2)
if y is None:
y = int(background_height / 2 - h / 2)

if x < 0:
w += x
overlay = overlay[:, -x:]

if y < 0:
w += y
overlay = overlay[:, -y:]

if x + w > background_width:
w = background_width - x
overlay = overlay[:, :w]

if y + h > background_height:
h = background_height - y
overlay = overlay[:h]

overlay_image = overlay[..., :3]
mask = overlay[..., 3:] / 255.0

background[y : y + h, x : x + w] = (1.0 - mask) * background[
y : y + h, x : x + w
] + mask * overlay_image

return background
45 changes: 45 additions & 0 deletions displayarray/effects/transform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Transform functions."""

import numpy as np
import cv2


def transform_about_center(
arr, scale_multiplier=(1, 1), rotation_degrees=0, translation=(0, 0), skew=(0, 0)
):
"""
Transform an image about its center.

:param arr: numpy image to be transformed
:param scale_multiplier: grow/shrink in x and y
:param rotation_degrees: degrees to rotate, from 0 to 360
:param translation: pixels to translate the image
:param skew: mimics rotation along screen axes. In degrees. 90 degrees should give a line.
:return: transformed numpy image
"""
center_scale_xform = np.eye(3)
center_scale_xform[0, 0] = scale_multiplier[1]
center_scale_xform[1, 1] = scale_multiplier[0]
center_scale_xform[0:2, -1] = [arr.shape[1] // 2, arr.shape[0] // 2]

rotation_xform = np.eye(3)

theta = np.radians(rotation_degrees)
c, s = np.cos(theta), np.sin(theta)
R = np.array(((c, -s), (s, c)))
rotation_xform[0:2, 0:2] = R
skew = np.radians(skew)
skew = np.tan(skew)
rotation_xform[-1, 0:2] = [skew[1] / arr.shape[1], skew[0] / arr.shape[0]]

translation_skew_xform = np.eye(3)
translation_skew_xform[0:2, -1] = [
(-arr.shape[1] - translation[1]) // 2,
(-arr.shape[0] - translation[0]) // 2,
]

full_xform = center_scale_xform @ rotation_xform @ translation_skew_xform
xformd_arr = cv2.warpPerspective(
arr, full_xform, tuple(reversed(arr.shape[:2])), flags=0
)
return xformd_arr
57 changes: 26 additions & 31 deletions displayarray/frame/frame_publishing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
using_pyv4l2cam = False
try:
if sys.platform == "linux":
from PyV4L2Cam.camera import Camera as pyv4lcamera
from PyV4L2Cam.controls import ControlIDs as pyv4lcontrolids
from PyV4L2Cam.camera import Camera as pyv4lcamera # type: ignore
from PyV4L2Cam.controls import ControlIDs as pyv4lcontrolids # type: ignore
from PyV4L2Cam import convert_mjpeg, convert_rgb24 # type: ignore
from PyV4L2Cam.get_camera import get_camera_by_bus_info, get_camera_by_string # type: ignore

using_pyv4l2cam = True
except ImportError:
Expand All @@ -32,25 +34,6 @@
FrameCallable = Callable[[np.ndarray], Optional[np.ndarray]]


def _v4l2_convert_mjpeg(mjpeg: bytes) -> Optional[np.ndarray]:
# Thanks: https://stackoverflow.com/a/21844162
a = mjpeg.find(b"\xff\xd8")
b = mjpeg.find(b"\xff\xd9")

if a == -1 or b == -1:
return None
else:
jpg = mjpeg[a : b + 2]
frame = cv2.imdecode(np.frombuffer(jpg, dtype=np.uint8), cv2.IMREAD_COLOR)
return frame


def _v4l2_convert_rgb24(rgb24: bytes, width: int, height: int) -> Optional[np.ndarray]:
nparr = np.frombuffer(rgb24, np.uint8)
np_frame = nparr.reshape((height, width, 3))
return np_frame


def pub_cam_loop_pyv4l2(
cam_id: Union[int, str, np.ndarray],
request_size: Tuple[int, int] = (-1, -1),
Expand All @@ -77,13 +60,16 @@ def pub_cam_loop_pyv4l2(
f"/dev/video{cam_id}", *request_size
)
else:
cam = pyv4lcamera(cam_id, *request_size) # type: ignore
if "usb" in cam_id:
cam = get_camera_by_bus_info(cam_id, *request_size) # type: ignore
else:
cam = get_camera_by_string(cam_id, *request_size) # type: ignore
else:
raise TypeError(
"Only strings or ints representing cameras are supported with v4l2."
)

subscriber_dictionary.register_cam(name)
subscriber_dictionary.register_cam(name, cam)

sub = subscriber_dictionary.cam_cmd_sub(name)
sub.return_on_no_data = ""
Expand All @@ -99,9 +85,9 @@ def pub_cam_loop_pyv4l2(
frame_bytes = cam.get_frame() # type: bytes

if cam.pixel_format == "MJPEG":
nd_frame = _v4l2_convert_mjpeg(frame_bytes)
nd_frame = convert_mjpeg(frame_bytes) # type: ignore
elif cam.pixel_format == "RGB24":
nd_frame = _v4l2_convert_rgb24(frame_bytes, cam.width, cam.height)
nd_frame = convert_rgb24(frame_bytes, cam.width, cam.height) # type: ignore
else:
raise NotImplementedError(f"{cam.pixel_format} format not supported.")

Expand Down Expand Up @@ -149,7 +135,7 @@ def pub_cam_loop_opencv(
"Only strings or ints representing cameras, or numpy arrays representing pictures supported."
)

subscriber_dictionary.register_cam(name)
subscriber_dictionary.register_cam(name, cam)

frame_counter = 0

Expand All @@ -158,7 +144,10 @@ def pub_cam_loop_opencv(
msg = ""

if high_speed:
cam.set(cv2.CAP_PROP_FOURCC, cv2.CAP_OPENCV_MJPEG)
try:
cam.set(cv2.CAP_PROP_FOURCC, cv2.CAP_OPENCV_MJPEG)
except AttributeError:
warnings.warn("Please update OpenCV")

cam.set(cv2.CAP_PROP_FRAME_WIDTH, request_size[0])
cam.set(cv2.CAP_PROP_FRAME_HEIGHT, request_size[1])
Expand Down Expand Up @@ -196,22 +185,28 @@ def pub_cam_thread(
request_ize: Tuple[int, int] = (-1, -1),
high_speed: bool = True,
fps_limit: float = float("inf"),
force_backend="",
) -> threading.Thread:
"""Run pub_cam_loop in a new thread. Starts on creation."""

name = uid_for_source(cam_id)
if name in uid_dict.keys():
t = uid_dict[name]
else:
if (
if "cv" in force_backend.lower():
pub_cam_loop = pub_cam_loop_opencv
elif (
sys.platform == "linux"
and using_pyv4l2cam
and (
isinstance(cam_id, int)
or (isinstance(cam_id, str) and "/dev/video" in cam_id)
or (
isinstance(cam_id, str)
and any(["/dev/video" in cam_id, "usb" in cam_id])
)
)
):
pub_cam_loop = pub_cam_loop_pyv4l2
) or "v4l2" in force_backend.lower():
pub_cam_loop = pub_cam_loop_pyv4l2 # type: ignore
else:
pub_cam_loop = pub_cam_loop_opencv

Expand Down
8 changes: 7 additions & 1 deletion displayarray/frame/frame_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(
request_size: Tuple[int, int] = (-1, -1),
high_speed: bool = True,
fps_limit: float = float("inf"),
force_backend="",
):
"""Create the frame updater thread."""
super(FrameUpdater, self).__init__(target=self.loop, args=())
Expand All @@ -42,6 +43,7 @@ def __init__(
self.high_speed = high_speed
self.fps_limit = fps_limit
self.exception_raised = None
self.force_backend = force_backend

def __wait_for_cam_id(self):
while str(self.cam_id) not in subscriber_dictionary.CV_CAMS_DICT:
Expand Down Expand Up @@ -84,7 +86,11 @@ def __apply_callbacks_to_frame(self, frame):
def loop(self):
"""Continually get frames from the video publisher, run callbacks on them, and listen to commands."""
t = pub_cam_thread(
self.video_source, self.request_size, self.high_speed, self.fps_limit
self.video_source,
self.request_size,
self.high_speed,
self.fps_limit,
self.force_backend,
)
self.__wait_for_cam_id()

Expand Down
7 changes: 4 additions & 3 deletions displayarray/frame/subscriber_dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,23 @@ def __init__(self, name, sub):
class Cam(object):
"""A camera publisher instance that will send frames, status, and commands out."""

def __init__(self, name):
def __init__(self, name, cam_instance=None):
"""Create the cam."""
self.name = name
self.cmd = None
self.frame_pub = VariablePub()
self.cmd_pub = VariablePub()
self.status_pub = VariablePub()
self.cam_instance = cam_instance


CV_CAM_HANDLERS_DICT: Dict[str, CamHandler] = {}
CV_CAMS_DICT: Dict[str, Cam] = {}


def register_cam(cam_id):
def register_cam(cam_id, cam_instance=None):
"""Register camera "cam_id" to a global list so it can be picked up."""
cam = Cam(str(cam_id))
cam = Cam(str(cam_id), cam_instance)
CV_CAMS_DICT[str(cam_id)] = cam
CV_CAM_HANDLERS_DICT[str(cam_id)] = CamHandler(
str(cam_id), cam.frame_pub.make_sub()
Expand Down
Loading