diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index 459bb888..93158ea7 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -106,6 +106,15 @@ class BaseCamera(object): # Stop worker thread 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 def start_worker(self, timeout: int=5) -> bool: @@ -131,6 +140,7 @@ class BaseCamera(object): def stop_worker(self, timeout: int=5) -> bool: """Flags worker thread for stop. Waits for thread to close, or times out.""" + logging.debug("Stopping worker thread") timeout_time = time.time() + timeout self.stop = True @@ -200,9 +210,7 @@ class BaseCamera(object): def _thread(self): """Camera background thread.""" self.frames_iterator = self.frames() - - # Update state - self.state['stream_active'] = True + logging.debug("Entering worker thread.") for frame in self.frames_iterator: self.frame = frame @@ -217,15 +225,13 @@ class BaseCamera(object): try: if self.stop is True: + logging.debug("Worker thread flagged for stop.") self.frames_iterator.close() break except AttributeError: pass + logging.debug("BaseCamera worker thread exiting...") # Reset thread self.thread = None - # Destroy any stored camera object (used especially for handling Pi cameras) - self.camera = None - # Update state - self.state['stream_active'] = False \ No newline at end of file diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index 9af51457..b29b9368 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -59,6 +59,8 @@ class StreamingCamera(BaseCamera): """Raspberry Pi camera implementation of StreamingCamera.""" # Run BaseCamera init BaseCamera.__init__(self) + # Attach to Pi camera + self.camera = picamera.PiCamera() # Camera settings self.settings.update({ @@ -83,7 +85,11 @@ class StreamingCamera(BaseCamera): def close(self): """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 @@ -96,53 +102,69 @@ class StreamingCamera(BaseCamera): """Open config_picamera.yaml file and write to camera.""" 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: - 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']) + raise Exception("Cannot update camera settings while recording is active.") def change_zoom(self, zoom_value: int=1) -> None: """Change the camera zoom, handling recentering and scaling.""" - self.start_worker() + #self.start_worker() zoom_value = float(zoom_value) if zoom_value < 1: @@ -165,7 +187,7 @@ class StreamingCamera(BaseCamera): def start_preview(self) -> bool: """Start the onboard GPU camera preview.""" - self.start_worker() + #self.start_worker() self.camera.start_preview() self.state['preview_active'] = True @@ -173,7 +195,7 @@ class StreamingCamera(BaseCamera): def stop_preview(self) -> bool: """Stop the onboard GPU camera preview.""" - self.start_worker() + #self.start_worker() self.camera.stop_preview() self.state['preview_active'] = False @@ -202,7 +224,7 @@ class StreamingCamera(BaseCamera): # Start recording method only if a current recording is not running if not self.state['record_active']: - self.start_worker() + #self.start_worker() # If no target is specified, store to StreamingCamera if not target: @@ -319,7 +341,7 @@ class StreamingCamera(BaseCamera): (default 'h264') resize ((int, int)): Resize the captured image. """ - self.start_worker() + #self.start_worker() # If no target is specified, store to StreamingCamera 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) resize ((int, int)): Resize the captured image. """ - self.start_worker() + #self.start_worker() if use_video_port: resolution = self.settings['video_resolution'] @@ -415,24 +437,28 @@ class StreamingCamera(BaseCamera): # Run this initialisation method self.initialisation() - with picamera.PiCamera() as self.camera: - # Let camera warm up - time.sleep(0.1) - # Settings config - self.update_settings() - # Set stream resolution - self.camera.resolution = self.settings['video_resolution'] + self.wait_for_camera() - # streaming - self.stream = io.BytesIO() + # Settings config + self.update_settings() + # Set stream resolution + self.camera.resolution = self.settings['video_resolution'] - # start recording on video splitter port 1 - self.camera.start_recording( - self.stream, - format='mjpeg', - quality=self.settings['jpeg_quality'], - splitter_port=1) + # Create stream + self.stream = io.BytesIO() + # 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: # reset stream for next frame self.stream.seek(0) @@ -441,8 +467,14 @@ class StreamingCamera(BaseCamera): time.sleep(1/self.settings['framerate']*0.1) # yield the result to be read frame = self.stream.getvalue() + # ensure the size of package is right if len(frame) == 0: pass else: 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") diff --git a/tests/test_camera.py b/tests/test_camera.py index ae8a03c5..c03f98c7 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -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): def test_still_capture(self): @@ -46,7 +36,7 @@ class TestCaptureMethods(unittest.TestCase): for resize in [None, (640, 480)]: # Wait for camera - wait_for_cam(camera) + camera.wait_for_camera() # Capture to a context (auto-deletes files when done) with camera.capture( @@ -114,7 +104,7 @@ class TestCaptureMethods(unittest.TestCase): for use_video_port in [True, False]: for resize in [None, (640, 480)]: # Wait for camera - wait_for_cam(camera) + camera.wait_for_camera() # Capture stream = camera.capture( @@ -151,7 +141,7 @@ class TestUnencodedMethods(unittest.TestCase): print("{}, {}, {}".format(id, resize, use_video_port)) # Wait for camera - wait_for_cam(camera) + camera.wait_for_camera() # Capture RGB array yuv = camera.array( @@ -183,7 +173,7 @@ class TestUnencodedMethods(unittest.TestCase): print("{}, {}, {}".format(id, resize, use_video_port)) # Wait for camera - wait_for_cam(camera) + camera.wait_for_camera() # Capture RGB array rgb = camera.array( @@ -216,7 +206,7 @@ class TestRecordMethods(unittest.TestCase): for write_to_file in [True, False, None]: # Wait for camera - wait_for_cam(camera) + camera.wait_for_camera() # Start recording with camera.start_recording( @@ -249,7 +239,7 @@ class TestRecordMethods(unittest.TestCase): global camera # Wait for camera - wait_for_cam(camera) + camera.wait_for_camera() # Start recording stream = camera.start_recording() @@ -270,52 +260,50 @@ class TestRecordMethods(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.""" global camera # Wait for camera - wait_for_cam(camera) + camera.wait_for_camera() # Force-stop the camera worker thread (should return True) self.assertTrue(camera.stop_worker()) # 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 - with camera.capture( - use_video_port=True) as stream: + # Restart worker thread + self.assertTrue(camera.start_worker()) - # Ensure StreamObject 'stream' has - # a valid BytesIO object and byte string - self.assertTrue(isinstance(stream.data, io.IOBase)) - self.assertTrue(isinstance(stream.binary, (bytes, bytearray))) + self.assertTrue(camera.camera) + self.assertTrue(camera.thread) + self.assertIsInstance(camera.stream, io.IOBase) if __name__ == '__main__': - camera = None - with StreamingCamera() as camera: + camera = StreamingCamera() - suites = [ - unittest.TestLoader().loadTestsFromTestCase(TestCaptureMethods), - unittest.TestLoader().loadTestsFromTestCase(TestUnencodedMethods), - unittest.TestLoader().loadTestsFromTestCase(TestThreadStarting), - unittest.TestLoader().loadTestsFromTestCase(TestRecordMethods), - ] + suites = [ + unittest.TestLoader().loadTestsFromTestCase(TestCaptureMethods), + unittest.TestLoader().loadTestsFromTestCase(TestUnencodedMethods), + unittest.TestLoader().loadTestsFromTestCase(TestThreadStarting), + 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:") - # Show us what was captured - for im in camera.images: - pprint(im.metadata) - for im in camera.videos: - pprint(im.metadata) + print("Objects created during tests:") + # Show us what was captured + for im in camera.images: + pprint(im.metadata) + for im in camera.videos: + pprint(im.metadata) - if result.wasSuccessful(): - print(success_string) + if result.wasSuccessful(): + print(success_string) camera.close()