Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Returning frames as PIL images #37

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
24 changes: 7 additions & 17 deletions sample_scripts/multicamera_demo.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,24 @@
#!/usr/bin/env python3
"""Reads framegrab configuration from a yaml file, creates FrameGrabber objects, and grabs images from each camera.
Remember to adjust sample_config.yaml according to your needs.
"""

from framegrab import FrameGrabber
import yaml
import cv2

# load the configurations from yaml
config_path = 'sample_config.yaml'
with open(config_path, 'r') as f:
configs = yaml.safe_load(f)['image_sources']

print('Loaded the following configurations from yaml:')
print(configs)

# Create the grabbers
grabbers = FrameGrabber.create_grabbers(configs)

while True:
# Get a frame from each camera
for camera_name, grabber in grabbers.items():
frame = grabber.grab()

cv2.imshow(camera_name, frame)

key = cv2.waitKey(30)

if key == ord('q'):
break

cv2.destroyAllWindows()
for camera_name, grabber in grabbers.items():
frame = grabber.grabimg()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updating to use grabimg as that is our preferred method

frame.show()

for grabber in grabbers.values():
grabber.release()


15 changes: 3 additions & 12 deletions sample_scripts/single_camera_demo.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
#!/usr/bin/env python3
"""Finds a single USB camera (or built-in webcam) and displays its feed in a window.
Press 'q' to quit.
"""Finds a single USB camera (or built-in webcam), grabs an image and displays the image in a window.
"""

import cv2
from framegrab import FrameGrabber

config = {
Expand All @@ -13,15 +11,8 @@

grabber = FrameGrabber.create_grabber(config)

while True:
frame = grabber.grab()
frame = grabber.grabimg()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changing to grabimg again


cv2.imshow('FrameGrab Single-Camera Demo', frame)

key = cv2.waitKey(30)
if key == ord('q'):
break

cv2.destroyAllWindows()
frame.show()

grabber.release()
19 changes: 16 additions & 3 deletions src/framegrab/grabber.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import cv2
import numpy as np
import yaml
from PIL import Image

from .unavailable_module import UnavailableModule

Expand Down Expand Up @@ -317,12 +318,24 @@ def autodiscover(warmup_delay: float = 1.0) -> dict:

@abstractmethod
def grab(self) -> np.ndarray:
"""Read a frame from the camera, zoom and crop if called for, and then perform any camera-specific
postprocessing operations.
Returns a frame.
"""Grabs a single frame from the configured camera device,
then performs post-processing operations such as cropping and zooming based
on the grabber's configuration.

Returns a numpy array.
"""
pass

def grabimg(self) -> Image:
"""Grabs a single frame from the configured camera device,
then performs post-processing operations such as cropping and zooming based
on the grabber's configuration.

Returns a PIL image.
"""
frame = self.grab()[:, :, ::-1] # convert from BGR to RGB, which PIL expects
return Image.fromarray(frame)

def _autogenerate_name(self) -> None:
"""For generating and assigning unique names for unnamed FrameGrabber objects.

Expand Down
7 changes: 6 additions & 1 deletion src/framegrab/motion.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
from typing import Union

import numpy as np
from PIL import Image

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -36,7 +38,10 @@ def pixel_threshold(self, img: np.ndarray, threshold_val: float = None) -> bool:
logger.debug(f"No motion detected: {pct_hi:.3f}% < {self.pixel_pct_threshold}%")
return False

def motion_detected(self, new_img: np.ndarray) -> bool:
def motion_detected(self, new_img: Union[np.ndarray, Image.Image]) -> bool:
if isinstance(new_img, Image.Image):
new_img = np.array(new_img)

if self.unused:
self.base_img = new_img
self.base2 = self.base_img
Expand Down
40 changes: 37 additions & 3 deletions test/test_framegrab_with_mock_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import os
import unittest
from framegrab.grabber import FrameGrabber, RTSPFrameGrabber
import numpy as np
from PIL import Image

class TestFrameGrabWithMockCamera(unittest.TestCase):
def test_crop_pixels(self):
Expand Down Expand Up @@ -86,7 +88,7 @@ def test_zoom(self):
frame = grabber.grab()

grabber.release()

assert frame.shape == (240, 320, 3)

def test_attempt_create_grabber_with_invalid_input_type(self):
Expand Down Expand Up @@ -157,11 +159,10 @@ def test_attempt_create_more_grabbers_than_exist(self):
# Try to connect to another grabber, this should raise an exception because there are only 3 mock cameras available
try:
FrameGrabber.create_grabber({'input_type': 'mock'})
self.fail()
self.fail() # we shouldn't get here
except ValueError:
pass
finally:
# release all the grabbers
for grabber in grabbers.values():
grabber.release()

Expand Down Expand Up @@ -213,3 +214,36 @@ def test_substitute_rtsp_url_without_placeholder(self):
new_config = RTSPFrameGrabber._substitute_rtsp_password(config)

assert new_config == config

def test_grab_returns_np_array(self):
"""Make sure that the grab method returns a numpy array.
"""
config = {
'input_type': 'mock',
}

grabber = FrameGrabber.create_grabber(config)

frame = grabber.grab()

assert isinstance(frame, np.ndarray)

grabber.release()


def test_grabimg_returns_pil_image(self):
"""Make sure that the grabimg method returns a PIL Image
and that the mode is 'RGB'.
"""
config = {
'input_type': 'mock',
}

grabber = FrameGrabber.create_grabber(config)

frame = grabber.grabimg()

assert isinstance(frame, Image.Image)
assert frame.mode == 'RGB'

grabber.release()
10 changes: 10 additions & 0 deletions test/test_motdet.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import unittest
import numpy as np
from PIL import Image
from framegrab.motion import MotionDetector

class TestMotionDetector(unittest.TestCase):
Expand Down Expand Up @@ -67,3 +68,12 @@ def test_detect_motion_with_configured_threshold(self):
self.motion_detector.motion_detected(img1) # Initialize base image
self.motion_detector.motion_detected(img1) # again to really reset
self.assertTrue(self.motion_detector.motion_detected(img3))

def test_that_motet_can_take_pil_image_or_numpy_image(self):
pil_img = Image.new('RGB', (100, 100), color='red')
for _ in range(10):
self.motion_detector.motion_detected(pil_img)

numpy_img = np.full((100, 100, 3), 255, dtype=np.uint8)
for _ in range(10):
self.motion_detector.motion_detected(numpy_img)
Loading