Loading openflexure_microscope/camera/base.py +13 −7 Original line number Diff line number Diff line Loading @@ -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: Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 openflexure_microscope/camera/pi.py +95 −63 Original line number Diff line number Diff line Loading @@ -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({ Loading @@ -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 Loading @@ -96,7 +102,16 @@ 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) Loading Loading @@ -140,9 +155,16 @@ class StreamingCamera(BaseCamera): 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() else: 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: Loading @@ -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 Loading @@ -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 Loading Loading @@ -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: Loading Loading @@ -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: Loading Loading @@ -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'] Loading Loading @@ -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) self.wait_for_camera() # Settings config self.update_settings() # Set stream resolution self.camera.resolution = self.settings['video_resolution'] # streaming # Create stream self.stream = io.BytesIO() # start recording on video 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: # reset stream for next frame self.stream.seek(0) Loading @@ -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") tests/test_camera.py +32 −44 Original line number Diff line number Diff line Loading @@ -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): Loading @@ -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( Loading Loading @@ -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( Loading Loading @@ -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( Loading Loading @@ -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( Loading Loading @@ -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( Loading Loading @@ -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() Loading @@ -270,32 +260,30 @@ 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), Loading Loading
openflexure_microscope/camera/base.py +13 −7 Original line number Diff line number Diff line Loading @@ -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: Loading @@ -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 Loading Loading @@ -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 Loading @@ -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
openflexure_microscope/camera/pi.py +95 −63 Original line number Diff line number Diff line Loading @@ -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({ Loading @@ -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 Loading @@ -96,7 +102,16 @@ 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) Loading Loading @@ -140,9 +155,16 @@ class StreamingCamera(BaseCamera): 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() else: 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: Loading @@ -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 Loading @@ -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 Loading Loading @@ -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: Loading Loading @@ -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: Loading Loading @@ -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'] Loading Loading @@ -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) self.wait_for_camera() # Settings config self.update_settings() # Set stream resolution self.camera.resolution = self.settings['video_resolution'] # streaming # Create stream self.stream = io.BytesIO() # start recording on video 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: # reset stream for next frame self.stream.seek(0) Loading @@ -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")
tests/test_camera.py +32 −44 Original line number Diff line number Diff line Loading @@ -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): Loading @@ -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( Loading Loading @@ -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( Loading Loading @@ -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( Loading Loading @@ -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( Loading Loading @@ -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( Loading Loading @@ -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() Loading @@ -270,32 +260,30 @@ 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), Loading