Pi camera is now persistent in StreamingCamera

This commit is contained in:
Joel Collins 2018-11-07 12:03:09 +00:00
parent 61beb211ae
commit a7598c5e5b
3 changed files with 140 additions and 114 deletions

View file

@ -106,6 +106,15 @@ class BaseCamera(object):
# Stop worker thread # Stop worker thread
self.stop_worker() self.stop_worker()
def wait_for_camera(self, timeout=5):
"""Wait for camera object, with 5 second timeout."""
timeout_time = time.time() + timeout
while not self.camera:
if time.time() > timeout_time:
raise TimeoutError("Timeout waiting for camera")
else:
pass
# START AND STOP WORKER THREAD # START AND STOP WORKER THREAD
def start_worker(self, timeout: int=5) -> bool: def start_worker(self, timeout: int=5) -> bool:
@ -131,6 +140,7 @@ class BaseCamera(object):
def stop_worker(self, timeout: int=5) -> bool: def stop_worker(self, timeout: int=5) -> bool:
"""Flags worker thread for stop. Waits for thread to close, or times out.""" """Flags worker thread for stop. Waits for thread to close, or times out."""
logging.debug("Stopping worker thread")
timeout_time = time.time() + timeout timeout_time = time.time() + timeout
self.stop = True self.stop = True
@ -200,9 +210,7 @@ class BaseCamera(object):
def _thread(self): def _thread(self):
"""Camera background thread.""" """Camera background thread."""
self.frames_iterator = self.frames() self.frames_iterator = self.frames()
logging.debug("Entering worker thread.")
# Update state
self.state['stream_active'] = True
for frame in self.frames_iterator: for frame in self.frames_iterator:
self.frame = frame self.frame = frame
@ -217,15 +225,13 @@ class BaseCamera(object):
try: try:
if self.stop is True: if self.stop is True:
logging.debug("Worker thread flagged for stop.")
self.frames_iterator.close() self.frames_iterator.close()
break break
except AttributeError: except AttributeError:
pass pass
logging.debug("BaseCamera worker thread exiting...")
# Reset thread # Reset thread
self.thread = None self.thread = None
# Destroy any stored camera object (used especially for handling Pi cameras)
self.camera = None
# Update state
self.state['stream_active'] = False

View file

@ -59,6 +59,8 @@ class StreamingCamera(BaseCamera):
"""Raspberry Pi camera implementation of StreamingCamera.""" """Raspberry Pi camera implementation of StreamingCamera."""
# Run BaseCamera init # Run BaseCamera init
BaseCamera.__init__(self) BaseCamera.__init__(self)
# Attach to Pi camera
self.camera = picamera.PiCamera()
# Camera settings # Camera settings
self.settings.update({ self.settings.update({
@ -83,7 +85,11 @@ class StreamingCamera(BaseCamera):
def close(self): def close(self):
"""Close the Raspberry Pi StreamingCamera""" """Close the Raspberry Pi StreamingCamera"""
BaseCamera.close(self) # Run BaseCamera close method # Run BaseCamera close method
BaseCamera.close(self)
# Detatch Pi camera
if self.camera:
self.camera.close()
# HANDLE SETTINGS # HANDLE SETTINGS
@ -96,53 +102,69 @@ class StreamingCamera(BaseCamera):
"""Open config_picamera.yaml file and write to camera.""" """Open config_picamera.yaml file and write to camera."""
global DEFAULT_CONFIG global DEFAULT_CONFIG
self.start_worker() paused_stream = False
if not self.state['record_active']: # If not recording a video
#self.start_worker()
if self.state['stream_active']: # If stream is active
logging.info("Pausing stream to update settings.")
self.pause_stream_for_capture() # Pause stream
paused_stream = True # Remember to unpause stream when done
if not config_path:
config = load_config(DEFAULT_CONFIG)
else:
config = load_config(config_path)
logging.debug(config)
# StreamingCamera settings
if 'video_resolution' in config:
self.settings['video_resolution'] = config['video_resolution']
if 'image_resolution' in config:
self.settings['image_resolution'] = config['image_resolution']
if 'numpy_resolution' in config:
self.settings['numpy_resolution'] = config['numpy_resolution']
if 'jpeg_quality' in config:
self.settings['jpeg_quality'] = config['jpeg_quality']
if 'framerate' in config:
self.settings['framerate'] = config['framerate']
# Camera AWB
if 'awb_mode' in config:
self.camera.awb_mode = config['awb_mode']
if 'red_gain' in config and 'blue_gain' in config:
self.camera.awb_gains = (config['red_gain'], config['blue_gain'])
# Camera framerate
if 'framerate' in config:
self.camera.framerate = config['framerate']
# Camera exposure
if 'shutter_speed' in config:
self.camera.shutter_speed = config['shutter_speed']
if 'saturation' in config:
self.camera.saturation = config['saturation']
# Camera misc.
self.camera.led = False
# Richard's library to set analog and digital gains
if 'analog_gain' in config:
set_analog_gain(self.camera, config['analog_gain'])
if 'digital_gain' in config:
set_digital_gain(self.camera, config['digital_gain'])
if paused_stream: # If stream was paused to update settings
logging.info("Resuming stream.")
self.resume_stream_for_capture()
if not config_path:
config = load_config(DEFAULT_CONFIG)
else: else:
config = load_config(config_path) raise Exception("Cannot update camera settings while recording is active.")
logging.debug(config)
# StreamingCamera settings
if 'video_resolution' in config:
self.settings['video_resolution'] = config['video_resolution']
if 'image_resolution' in config:
self.settings['image_resolution'] = config['image_resolution']
if 'numpy_resolution' in config:
self.settings['numpy_resolution'] = config['numpy_resolution']
if 'jpeg_quality' in config:
self.settings['jpeg_quality'] = config['jpeg_quality']
if 'framerate' in config:
self.settings['framerate'] = config['framerate']
# Camera AWB
if 'awb_mode' in config:
self.camera.awb_mode = config['awb_mode']
if 'red_gain' in config and 'blue_gain' in config:
self.camera.awb_gains = (config['red_gain'], config['blue_gain'])
# Camera framerate
if 'framerate' in config:
self.camera.framerate = config['framerate']
# Camera exposure
if 'shutter_speed' in config:
self.camera.shutter_speed = config['shutter_speed']
if 'saturation' in config:
self.camera.saturation = config['saturation']
# Camera misc.
self.camera.led = False
# Richard's library to set analog and digital gains
if 'analog_gain' in config:
set_analog_gain(self.camera, config['analog_gain'])
if 'digital_gain' in config:
set_digital_gain(self.camera, config['digital_gain'])
def change_zoom(self, zoom_value: int=1) -> None: def change_zoom(self, zoom_value: int=1) -> None:
"""Change the camera zoom, handling recentering and scaling.""" """Change the camera zoom, handling recentering and scaling."""
self.start_worker() #self.start_worker()
zoom_value = float(zoom_value) zoom_value = float(zoom_value)
if zoom_value < 1: if zoom_value < 1:
@ -165,7 +187,7 @@ class StreamingCamera(BaseCamera):
def start_preview(self) -> bool: def start_preview(self) -> bool:
"""Start the onboard GPU camera preview.""" """Start the onboard GPU camera preview."""
self.start_worker() #self.start_worker()
self.camera.start_preview() self.camera.start_preview()
self.state['preview_active'] = True self.state['preview_active'] = True
@ -173,7 +195,7 @@ class StreamingCamera(BaseCamera):
def stop_preview(self) -> bool: def stop_preview(self) -> bool:
"""Stop the onboard GPU camera preview.""" """Stop the onboard GPU camera preview."""
self.start_worker() #self.start_worker()
self.camera.stop_preview() self.camera.stop_preview()
self.state['preview_active'] = False self.state['preview_active'] = False
@ -202,7 +224,7 @@ class StreamingCamera(BaseCamera):
# Start recording method only if a current recording is not running # Start recording method only if a current recording is not running
if not self.state['record_active']: if not self.state['record_active']:
self.start_worker() #self.start_worker()
# If no target is specified, store to StreamingCamera # If no target is specified, store to StreamingCamera
if not target: if not target:
@ -319,7 +341,7 @@ class StreamingCamera(BaseCamera):
(default 'h264') (default 'h264')
resize ((int, int)): Resize the captured image. resize ((int, int)): Resize the captured image.
""" """
self.start_worker() #self.start_worker()
# If no target is specified, store to StreamingCamera # If no target is specified, store to StreamingCamera
if not target: if not target:
@ -367,7 +389,7 @@ class StreamingCamera(BaseCamera):
use_video_port (bool): Capture from the video port used for streaming (lower resolution, faster) use_video_port (bool): Capture from the video port used for streaming (lower resolution, faster)
resize ((int, int)): Resize the captured image. resize ((int, int)): Resize the captured image.
""" """
self.start_worker() #self.start_worker()
if use_video_port: if use_video_port:
resolution = self.settings['video_resolution'] resolution = self.settings['video_resolution']
@ -415,24 +437,28 @@ class StreamingCamera(BaseCamera):
# Run this initialisation method # Run this initialisation method
self.initialisation() self.initialisation()
with picamera.PiCamera() as self.camera: self.wait_for_camera()
# Let camera warm up
time.sleep(0.1)
# Settings config
self.update_settings()
# Set stream resolution
self.camera.resolution = self.settings['video_resolution']
# streaming # Settings config
self.stream = io.BytesIO() self.update_settings()
# Set stream resolution
self.camera.resolution = self.settings['video_resolution']
# start recording on video splitter port 1 # Create stream
self.camera.start_recording( self.stream = io.BytesIO()
self.stream,
format='mjpeg',
quality=self.settings['jpeg_quality'],
splitter_port=1)
# Start recording on video splitter port 1
self.camera.start_recording(
self.stream,
format='mjpeg',
quality=self.settings['jpeg_quality'],
splitter_port=1)
# Update state
logging.debug("STREAM ACTIVE")
self.state['stream_active'] = True
try: # While the iterator is not closed
while True: while True:
# reset stream for next frame # reset stream for next frame
self.stream.seek(0) self.stream.seek(0)
@ -441,8 +467,14 @@ class StreamingCamera(BaseCamera):
time.sleep(1/self.settings['framerate']*0.1) time.sleep(1/self.settings['framerate']*0.1)
# yield the result to be read # yield the result to be read
frame = self.stream.getvalue() frame = self.stream.getvalue()
# ensure the size of package is right # ensure the size of package is right
if len(frame) == 0: if len(frame) == 0:
pass pass
else: else:
yield frame yield frame
finally: # When GeneratorExit or StopIteration raised, run cleanup code
logging.debug("Stopping stream recording on port 1")
self.camera.stop_recording(splitter_port=1)
self.state['stream_active'] = False
logging.debug("FRAME ITERATOR END")

View file

@ -26,16 +26,6 @@ success_string = """
""" """
def wait_for_cam(camera, timeout=5):
"""Wait for camera object in global namespace, with 5 second timeout."""
timeout_time = time.time() + timeout
while not camera:
if time.time() > timeout_time:
raise TimeoutError("Timeout waiting for camera")
else:
pass
class TestCaptureMethods(unittest.TestCase): class TestCaptureMethods(unittest.TestCase):
def test_still_capture(self): def test_still_capture(self):
@ -46,7 +36,7 @@ class TestCaptureMethods(unittest.TestCase):
for resize in [None, (640, 480)]: for resize in [None, (640, 480)]:
# Wait for camera # Wait for camera
wait_for_cam(camera) camera.wait_for_camera()
# Capture to a context (auto-deletes files when done) # Capture to a context (auto-deletes files when done)
with camera.capture( with camera.capture(
@ -114,7 +104,7 @@ class TestCaptureMethods(unittest.TestCase):
for use_video_port in [True, False]: for use_video_port in [True, False]:
for resize in [None, (640, 480)]: for resize in [None, (640, 480)]:
# Wait for camera # Wait for camera
wait_for_cam(camera) camera.wait_for_camera()
# Capture # Capture
stream = camera.capture( stream = camera.capture(
@ -151,7 +141,7 @@ class TestUnencodedMethods(unittest.TestCase):
print("{}, {}, {}".format(id, resize, use_video_port)) print("{}, {}, {}".format(id, resize, use_video_port))
# Wait for camera # Wait for camera
wait_for_cam(camera) camera.wait_for_camera()
# Capture RGB array # Capture RGB array
yuv = camera.array( yuv = camera.array(
@ -183,7 +173,7 @@ class TestUnencodedMethods(unittest.TestCase):
print("{}, {}, {}".format(id, resize, use_video_port)) print("{}, {}, {}".format(id, resize, use_video_port))
# Wait for camera # Wait for camera
wait_for_cam(camera) camera.wait_for_camera()
# Capture RGB array # Capture RGB array
rgb = camera.array( rgb = camera.array(
@ -216,7 +206,7 @@ class TestRecordMethods(unittest.TestCase):
for write_to_file in [True, False, None]: for write_to_file in [True, False, None]:
# Wait for camera # Wait for camera
wait_for_cam(camera) camera.wait_for_camera()
# Start recording # Start recording
with camera.start_recording( with camera.start_recording(
@ -249,7 +239,7 @@ class TestRecordMethods(unittest.TestCase):
global camera global camera
# Wait for camera # Wait for camera
wait_for_cam(camera) camera.wait_for_camera()
# Start recording # Start recording
stream = camera.start_recording() stream = camera.start_recording()
@ -270,52 +260,50 @@ class TestRecordMethods(unittest.TestCase):
class TestThreadStarting(unittest.TestCase): class TestThreadStarting(unittest.TestCase):
def test_capture_restarting_stream(self): def test_restarting_stream(self):
"""Tests that a capture call restarts the camera worker thread.""" """Tests that a capture call restarts the camera worker thread."""
global camera global camera
# Wait for camera # Wait for camera
wait_for_cam(camera) camera.wait_for_camera()
# Force-stop the camera worker thread (should return True) # Force-stop the camera worker thread (should return True)
self.assertTrue(camera.stop_worker()) self.assertTrue(camera.stop_worker())
# Check Pi camera has been disconnected # Check Pi camera has been disconnected
self.assertFalse(camera.camera) self.assertTrue(camera.camera)
self.assertFalse(camera.thread)
# Restart worker thread by initialising a capture # Restart worker thread
with camera.capture( self.assertTrue(camera.start_worker())
use_video_port=True) as stream:
# Ensure StreamObject 'stream' has self.assertTrue(camera.camera)
# a valid BytesIO object and byte string self.assertTrue(camera.thread)
self.assertTrue(isinstance(stream.data, io.IOBase)) self.assertIsInstance(camera.stream, io.IOBase)
self.assertTrue(isinstance(stream.binary, (bytes, bytearray)))
if __name__ == '__main__': if __name__ == '__main__':
camera = None camera = StreamingCamera()
with StreamingCamera() as camera:
suites = [ suites = [
unittest.TestLoader().loadTestsFromTestCase(TestCaptureMethods), unittest.TestLoader().loadTestsFromTestCase(TestCaptureMethods),
unittest.TestLoader().loadTestsFromTestCase(TestUnencodedMethods), unittest.TestLoader().loadTestsFromTestCase(TestUnencodedMethods),
unittest.TestLoader().loadTestsFromTestCase(TestThreadStarting), unittest.TestLoader().loadTestsFromTestCase(TestThreadStarting),
unittest.TestLoader().loadTestsFromTestCase(TestRecordMethods), unittest.TestLoader().loadTestsFromTestCase(TestRecordMethods),
] ]
alltests = unittest.TestSuite(suites) alltests = unittest.TestSuite(suites)
result = unittest.TextTestRunner(verbosity=2).run(alltests) result = unittest.TextTestRunner(verbosity=2).run(alltests)
print("Objects created during tests:") print("Objects created during tests:")
# Show us what was captured # Show us what was captured
for im in camera.images: for im in camera.images:
pprint(im.metadata) pprint(im.metadata)
for im in camera.videos: for im in camera.videos:
pprint(im.metadata) pprint(im.metadata)
if result.wasSuccessful(): if result.wasSuccessful():
print(success_string) print(success_string)
camera.close() camera.close()