Pi camera is now persistent in StreamingCamera
This commit is contained in:
parent
61beb211ae
commit
a7598c5e5b
3 changed files with 140 additions and 114 deletions
|
|
@ -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
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue