Loading openflexure_microscope/api/app.py +57 −45 Original line number Diff line number Diff line #!/usr/bin/env python #TODO: Completely rewrite for new version of StreamingCamera from pprint import pprint from importlib import import_module import time, datetime import time import datetime from flask import ( Flask, render_template, Response, redirect, request, jsonify, send_file) from flask import Flask, render_template, Response, redirect, request, jsonify, send_file #import yaml import numpy as np # Raspberry Pi camera module (requires picamera package) #from camera.pi import Camera from openflexure_microscope.camera.pi import StreamingCamera app = Flask(__name__) cam = StreamingCamera() def parse_payload(request): # TODO: Try-except for invalid JSON payloads """Convert request to JSON. Will eventually handle error-checking.""" # TODO: Handle invalid JSON payloads state = request.get_json() return state def gen(camera): """Video streaming generator function.""" while True: Loading @@ -29,31 +33,29 @@ def gen(camera): yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') @app.route('/') def index(): ''' Video streaming home page. ''' """Video streaming home page.""" cam.start_worker() # Start the stream return render_template('index.html') @app.route('/stream') def stream(): ''' Video streaming route. Put this in the src attribute of an img tag. ''' return Response(gen(cam), mimetype='multipart/x-mixed-replace; boundary=frame') """Video streaming route. Put this in the src attribute of an img tag.""" return Response( gen(cam), mimetype='multipart/x-mixed-replace; boundary=frame') # TODO: Be able to change the image resolution with an option @app.route('/capture/', methods=['GET', 'POST', 'PUT']) def capture(): ''' POST/PUT: Capture a single image GET: Return capture status, or download latest capture Eg. curl http://192.168.1.140:5000/capture/ -H "Content-Type: application/json" -d '{"store":true}' -X POST ''' """ POST/PUT: Capture a single image. GET: Return capture status, or download latest capture. """ if request.method == 'POST' or request.method == 'PUT': state = parse_payload(request) print(state) Loading Loading @@ -95,21 +97,23 @@ def capture(): state = cam.state if (download == 'true' or download == 'True' or download =='1') and cam.image is not None: if (( download == 'true' or download == 'True' or download == '1') and cam.image is not None): return send_file(cam.image.data, mimetype='image/jpeg') else: return jsonify(state) @app.route('/record/', methods=['GET', 'POST', 'PUT']) def record(): ''' POST/PUT: Start or stop a video recording """ POST/PUT: Start or stop a video recording. GET: Return recording status, or download latest recording Eg. curl http://192.168.1.140:5000/record/ -H "Content-Type: application/json" -d '{"status":true}' -X POST curl http://192.168.1.140:5000/record/ -H "Content-Type: application/json" -d '{"status":false}' -X POST ''' """ if request.method == 'POST' or request.method == 'PUT': state = request.get_json() print(state) Loading @@ -125,11 +129,11 @@ def record(): status = bool(state['status']) # synchronise the arduino_time if status == True: if status is True: response = cam.start_recording(filename=filename) return str(response) elif status == False: elif status is False: response = cam.stop_recording() return str(response) Loading @@ -138,19 +142,27 @@ def record(): state = cam.state if (download == 'true' or download == 'True' or download =='1') and cam.state_record['recent'] is not None: return send_file(cam.state_record['recent'], if (( download == 'true' or download == 'True' or download == '1') and cam.state['recent_video'] is not None): return send_file( cam.state['recent_video'], mimetype='video/H264', as_attachment=True) else: return jsonify(state) @app.route('/preview/', methods=['GET', 'POST', 'PUT']) def preview(): ''' POST/PUT: Start or stop the direct-to-GPU camera preview on the Pi """ POST/PUT: Start or stop the direct-to-GPU camera preview on the Pi. GET: Return preview status ''' """ if request.method == 'POST' or request.method == 'PUT': state = request.get_json() print(state) Loading @@ -158,7 +170,7 @@ def preview(): if 'status' in state: status = bool(state['status']) if status == False: if status is False: response = cam.stop_preview() else: response = cam.start_preview() Loading openflexure_microscope/camera/base.py +9 −3 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ except ImportError: # Slightly crappy debugger logging DEBUG = True def log(s): if DEBUG: print('DEBUG: {}'.format(s)) Loading Loading @@ -111,6 +112,11 @@ class StreamObject(object): f.seek(0, 0) # Seek to the start of the file f.write(self.binary) # Write data bytes to file def clear_stream(self): """Clears the BytesIO stream of the StreamObject.""" self.stream = io.BytesIO() return True def delete(self) -> bool: """ If the StreamObject has been saved to disk, deletes the file and returns True. Loading openflexure_microscope/camera/config.py +5 −8 Original line number Diff line number Diff line Loading @@ -16,8 +16,9 @@ TYPES = { 'digital_gain': float, } def convert_config(config): """Convert datatype of config based on type dictionary""" """Convert datatype of config based on type dictionary.""" global TYPES for key in config: Loading @@ -26,12 +27,8 @@ def convert_config(config): return config def load_config(yaml_path): """Load YAML file, pass through dictionary conversion, and return.""" with open(yaml_path) as config_file: return convert_config(yaml.load(config_file)) if __name__ == "__main__": from pprint import pprint config = load_config('config_picamera.yaml') pprint(config) No newline at end of file openflexure_microscope/camera/pi.py +52 −29 Original line number Diff line number Diff line # -*- coding: utf-8 -*- """ Raspberry Pi camera implementation of the StreamingCamera class. Loading Loading @@ -51,8 +52,9 @@ from .config import load_config HERE = os.path.abspath(os.path.dirname(__file__)) DEFAULT_CONFIG = os.path.join(HERE, 'config_picamera.yaml') class StreamingCamera(BaseCamera): class StreamingCamera(BaseCamera): """Raspberry Pi camera implementation of StreamingCamera.""" def __init__(self): # Capture data self.image = None Loading @@ -79,10 +81,11 @@ class StreamingCamera(BaseCamera): BaseCamera.__init__(self) def initialisation(self): """Run any initialisation code when the frame iterator starts.""" pass def start_preview(self) -> bool: """Function to start the onboard GPU camera preview.""" """Start the onboard GPU camera preview.""" self.start_worker() self.camera.start_preview() Loading @@ -90,7 +93,7 @@ class StreamingCamera(BaseCamera): return True def stop_preview(self) -> bool: """Function to start the onboard GPU camera preview.""" """Stop the onboard GPU camera preview.""" self.start_worker() self.camera.stop_preview() Loading @@ -98,11 +101,12 @@ class StreamingCamera(BaseCamera): return True # TODO: Handle exceptions # TODO: Have this take a dictionary (not a config file) # TODO: Web API entry point to send settings as JSON to this method # TODO: Separate method to store current settings back to YAML file # TODO: API entry point to get settings to JSON def update_settings(self, config_path: str=None) -> None: """ Opens config_picamera.yaml file and writes valid settings to the camera hardware object """ """Open config_picamera.yaml file and write to camera.""" global DEFAULT_CONFIG self.start_worker() Loading Loading @@ -203,27 +207,40 @@ class StreamingCamera(BaseCamera): quality: int=15): """Start a new video recording, writing to a target object. target (str/BytesIO): Target object to write bytes to (default StreamObject) write_to_file (bool/NoneType): Should the StreamObject write to a file? (default True for video capture) filename (str): Name of the stored file (defaults to timestamp) target (str/BytesIO): Target object to write bytes to. (default StreamObject) write_to_file (bool/NoneType): Should the StreamObject write to a file? (default True for video capture) filename (str): Name of the stored file. (defaults to timestamp) folder (str): Relative directory to store data file in. fmt (str): Format of the capture (default 'h264') fmt (str): Format of the capture. (default 'h264') """ self.start_worker() # If no target is specified, store to StreamingCamera if not target: if (self.video is None) or (not self.video.locked): # If target is not locked # If target is not locked if (self.video is None) or (not self.video.locked): self.video = StreamObject(write_to_file=write_to_file, filename=filename, folder=folder, fmt=fmt) # Reset/create StreamObject self.video = StreamObject( write_to_file=write_to_file, filename=filename, folder=folder, fmt=fmt) # Reset/create StreamObject target = self.video.target # Store to the StreamObject BytesIO target_obj = self.video # Note the target StreamObject, used for function return # Store to the StreamObject BytesIO target = self.video.target # Note the target StreamObject, used for function return target_obj = self.video target_obj.lock() # Lock the StreamObject while recording else: print("Cannot start recording a new video until the current recording has been stopped. Returning None.") print("Cannot start recording a new video \ until the current recording has been stopped. \ Returning None.") return None else: target_obj = target Loading @@ -247,7 +264,8 @@ class StreamingCamera(BaseCamera): Pause capture on a splitter port. splitter_port (int): Splitter port to stop recording on resolution ((int, int)): Resolution to set the camera to, after stopping recording. resolution ((int, int)): Resolution to set the camera to, after stopping recording. """ log("Pausing stream") # If no resolution is specified, default to image_resolution Loading @@ -262,10 +280,11 @@ class StreamingCamera(BaseCamera): def resume_stream_for_capture(self, splitter_port: int=1, resolution: Tuple[int, int]=None) -> None: """ Resume capture on a splitter port Resume capture on a splitter port. splitter_port (int): Splitter port to start recording on resolution ((int, int)): Resolution to set the camera to, before starting recording. resolution ((int, int)): Resolution to set the camera to, before starting recording. """ log("Unpausing stream") if not resolution: Loading @@ -291,17 +310,21 @@ class StreamingCamera(BaseCamera): fmt: str='jpeg', resize: Tuple[int, int]=None): """ Captures a still image to a StreamObject. Capture a still image to a StreamObject. Defaults to JPEG format. Target object can be overridden for development purposes. target (str/BytesIO): Target object to write data bytes to write_to_file (bool): Should the StreamObject write to a file, instead of BytesIO stream? use_video_port (bool): Capture from the video port used for streaming (lower resolution, faster) filename (str): Name of the stored file (defaults to timestamp) target (str/BytesIO): Target object to write data bytes to. write_to_file (bool): Should the StreamObject write to a file, instead of BytesIO stream? use_video_port (bool): Capture from the video port used for streaming. (lower resolution, faster) filename (str): Name of the stored file. (defaults to timestamp) folder (str): Relative directory to store data file in. fmt (str): Format of the capture (default 'h264') fmt (str): Format of the capture. (default 'h264') resize ((int, int)): Resize the captured image. """ self.start_worker() Loading Loading @@ -348,7 +371,7 @@ class StreamingCamera(BaseCamera): return target_obj def array(self, rgb=False, use_video_port: bool=True, resize: Tuple[int, int]=None) -> np.ndarray: """Captures an uncompressed still YUV image to a Numpy array. """Capture an uncompressed still YUV image to a Numpy array. use_video_port (bool): Capture from the video port used for streaming (lower resolution, faster) resize ((int, int)): Resize the captured image. Loading openflexure_microscope/__init__.py +1 −1 File changed.Contains only whitespace changes. Show changes Loading
openflexure_microscope/api/app.py +57 −45 Original line number Diff line number Diff line #!/usr/bin/env python #TODO: Completely rewrite for new version of StreamingCamera from pprint import pprint from importlib import import_module import time, datetime import time import datetime from flask import ( Flask, render_template, Response, redirect, request, jsonify, send_file) from flask import Flask, render_template, Response, redirect, request, jsonify, send_file #import yaml import numpy as np # Raspberry Pi camera module (requires picamera package) #from camera.pi import Camera from openflexure_microscope.camera.pi import StreamingCamera app = Flask(__name__) cam = StreamingCamera() def parse_payload(request): # TODO: Try-except for invalid JSON payloads """Convert request to JSON. Will eventually handle error-checking.""" # TODO: Handle invalid JSON payloads state = request.get_json() return state def gen(camera): """Video streaming generator function.""" while True: Loading @@ -29,31 +33,29 @@ def gen(camera): yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') @app.route('/') def index(): ''' Video streaming home page. ''' """Video streaming home page.""" cam.start_worker() # Start the stream return render_template('index.html') @app.route('/stream') def stream(): ''' Video streaming route. Put this in the src attribute of an img tag. ''' return Response(gen(cam), mimetype='multipart/x-mixed-replace; boundary=frame') """Video streaming route. Put this in the src attribute of an img tag.""" return Response( gen(cam), mimetype='multipart/x-mixed-replace; boundary=frame') # TODO: Be able to change the image resolution with an option @app.route('/capture/', methods=['GET', 'POST', 'PUT']) def capture(): ''' POST/PUT: Capture a single image GET: Return capture status, or download latest capture Eg. curl http://192.168.1.140:5000/capture/ -H "Content-Type: application/json" -d '{"store":true}' -X POST ''' """ POST/PUT: Capture a single image. GET: Return capture status, or download latest capture. """ if request.method == 'POST' or request.method == 'PUT': state = parse_payload(request) print(state) Loading Loading @@ -95,21 +97,23 @@ def capture(): state = cam.state if (download == 'true' or download == 'True' or download =='1') and cam.image is not None: if (( download == 'true' or download == 'True' or download == '1') and cam.image is not None): return send_file(cam.image.data, mimetype='image/jpeg') else: return jsonify(state) @app.route('/record/', methods=['GET', 'POST', 'PUT']) def record(): ''' POST/PUT: Start or stop a video recording """ POST/PUT: Start or stop a video recording. GET: Return recording status, or download latest recording Eg. curl http://192.168.1.140:5000/record/ -H "Content-Type: application/json" -d '{"status":true}' -X POST curl http://192.168.1.140:5000/record/ -H "Content-Type: application/json" -d '{"status":false}' -X POST ''' """ if request.method == 'POST' or request.method == 'PUT': state = request.get_json() print(state) Loading @@ -125,11 +129,11 @@ def record(): status = bool(state['status']) # synchronise the arduino_time if status == True: if status is True: response = cam.start_recording(filename=filename) return str(response) elif status == False: elif status is False: response = cam.stop_recording() return str(response) Loading @@ -138,19 +142,27 @@ def record(): state = cam.state if (download == 'true' or download == 'True' or download =='1') and cam.state_record['recent'] is not None: return send_file(cam.state_record['recent'], if (( download == 'true' or download == 'True' or download == '1') and cam.state['recent_video'] is not None): return send_file( cam.state['recent_video'], mimetype='video/H264', as_attachment=True) else: return jsonify(state) @app.route('/preview/', methods=['GET', 'POST', 'PUT']) def preview(): ''' POST/PUT: Start or stop the direct-to-GPU camera preview on the Pi """ POST/PUT: Start or stop the direct-to-GPU camera preview on the Pi. GET: Return preview status ''' """ if request.method == 'POST' or request.method == 'PUT': state = request.get_json() print(state) Loading @@ -158,7 +170,7 @@ def preview(): if 'status' in state: status = bool(state['status']) if status == False: if status is False: response = cam.stop_preview() else: response = cam.start_preview() Loading
openflexure_microscope/camera/base.py +9 −3 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ except ImportError: # Slightly crappy debugger logging DEBUG = True def log(s): if DEBUG: print('DEBUG: {}'.format(s)) Loading Loading @@ -111,6 +112,11 @@ class StreamObject(object): f.seek(0, 0) # Seek to the start of the file f.write(self.binary) # Write data bytes to file def clear_stream(self): """Clears the BytesIO stream of the StreamObject.""" self.stream = io.BytesIO() return True def delete(self) -> bool: """ If the StreamObject has been saved to disk, deletes the file and returns True. Loading
openflexure_microscope/camera/config.py +5 −8 Original line number Diff line number Diff line Loading @@ -16,8 +16,9 @@ TYPES = { 'digital_gain': float, } def convert_config(config): """Convert datatype of config based on type dictionary""" """Convert datatype of config based on type dictionary.""" global TYPES for key in config: Loading @@ -26,12 +27,8 @@ def convert_config(config): return config def load_config(yaml_path): """Load YAML file, pass through dictionary conversion, and return.""" with open(yaml_path) as config_file: return convert_config(yaml.load(config_file)) if __name__ == "__main__": from pprint import pprint config = load_config('config_picamera.yaml') pprint(config) No newline at end of file
openflexure_microscope/camera/pi.py +52 −29 Original line number Diff line number Diff line # -*- coding: utf-8 -*- """ Raspberry Pi camera implementation of the StreamingCamera class. Loading Loading @@ -51,8 +52,9 @@ from .config import load_config HERE = os.path.abspath(os.path.dirname(__file__)) DEFAULT_CONFIG = os.path.join(HERE, 'config_picamera.yaml') class StreamingCamera(BaseCamera): class StreamingCamera(BaseCamera): """Raspberry Pi camera implementation of StreamingCamera.""" def __init__(self): # Capture data self.image = None Loading @@ -79,10 +81,11 @@ class StreamingCamera(BaseCamera): BaseCamera.__init__(self) def initialisation(self): """Run any initialisation code when the frame iterator starts.""" pass def start_preview(self) -> bool: """Function to start the onboard GPU camera preview.""" """Start the onboard GPU camera preview.""" self.start_worker() self.camera.start_preview() Loading @@ -90,7 +93,7 @@ class StreamingCamera(BaseCamera): return True def stop_preview(self) -> bool: """Function to start the onboard GPU camera preview.""" """Stop the onboard GPU camera preview.""" self.start_worker() self.camera.stop_preview() Loading @@ -98,11 +101,12 @@ class StreamingCamera(BaseCamera): return True # TODO: Handle exceptions # TODO: Have this take a dictionary (not a config file) # TODO: Web API entry point to send settings as JSON to this method # TODO: Separate method to store current settings back to YAML file # TODO: API entry point to get settings to JSON def update_settings(self, config_path: str=None) -> None: """ Opens config_picamera.yaml file and writes valid settings to the camera hardware object """ """Open config_picamera.yaml file and write to camera.""" global DEFAULT_CONFIG self.start_worker() Loading Loading @@ -203,27 +207,40 @@ class StreamingCamera(BaseCamera): quality: int=15): """Start a new video recording, writing to a target object. target (str/BytesIO): Target object to write bytes to (default StreamObject) write_to_file (bool/NoneType): Should the StreamObject write to a file? (default True for video capture) filename (str): Name of the stored file (defaults to timestamp) target (str/BytesIO): Target object to write bytes to. (default StreamObject) write_to_file (bool/NoneType): Should the StreamObject write to a file? (default True for video capture) filename (str): Name of the stored file. (defaults to timestamp) folder (str): Relative directory to store data file in. fmt (str): Format of the capture (default 'h264') fmt (str): Format of the capture. (default 'h264') """ self.start_worker() # If no target is specified, store to StreamingCamera if not target: if (self.video is None) or (not self.video.locked): # If target is not locked # If target is not locked if (self.video is None) or (not self.video.locked): self.video = StreamObject(write_to_file=write_to_file, filename=filename, folder=folder, fmt=fmt) # Reset/create StreamObject self.video = StreamObject( write_to_file=write_to_file, filename=filename, folder=folder, fmt=fmt) # Reset/create StreamObject target = self.video.target # Store to the StreamObject BytesIO target_obj = self.video # Note the target StreamObject, used for function return # Store to the StreamObject BytesIO target = self.video.target # Note the target StreamObject, used for function return target_obj = self.video target_obj.lock() # Lock the StreamObject while recording else: print("Cannot start recording a new video until the current recording has been stopped. Returning None.") print("Cannot start recording a new video \ until the current recording has been stopped. \ Returning None.") return None else: target_obj = target Loading @@ -247,7 +264,8 @@ class StreamingCamera(BaseCamera): Pause capture on a splitter port. splitter_port (int): Splitter port to stop recording on resolution ((int, int)): Resolution to set the camera to, after stopping recording. resolution ((int, int)): Resolution to set the camera to, after stopping recording. """ log("Pausing stream") # If no resolution is specified, default to image_resolution Loading @@ -262,10 +280,11 @@ class StreamingCamera(BaseCamera): def resume_stream_for_capture(self, splitter_port: int=1, resolution: Tuple[int, int]=None) -> None: """ Resume capture on a splitter port Resume capture on a splitter port. splitter_port (int): Splitter port to start recording on resolution ((int, int)): Resolution to set the camera to, before starting recording. resolution ((int, int)): Resolution to set the camera to, before starting recording. """ log("Unpausing stream") if not resolution: Loading @@ -291,17 +310,21 @@ class StreamingCamera(BaseCamera): fmt: str='jpeg', resize: Tuple[int, int]=None): """ Captures a still image to a StreamObject. Capture a still image to a StreamObject. Defaults to JPEG format. Target object can be overridden for development purposes. target (str/BytesIO): Target object to write data bytes to write_to_file (bool): Should the StreamObject write to a file, instead of BytesIO stream? use_video_port (bool): Capture from the video port used for streaming (lower resolution, faster) filename (str): Name of the stored file (defaults to timestamp) target (str/BytesIO): Target object to write data bytes to. write_to_file (bool): Should the StreamObject write to a file, instead of BytesIO stream? use_video_port (bool): Capture from the video port used for streaming. (lower resolution, faster) filename (str): Name of the stored file. (defaults to timestamp) folder (str): Relative directory to store data file in. fmt (str): Format of the capture (default 'h264') fmt (str): Format of the capture. (default 'h264') resize ((int, int)): Resize the captured image. """ self.start_worker() Loading Loading @@ -348,7 +371,7 @@ class StreamingCamera(BaseCamera): return target_obj def array(self, rgb=False, use_video_port: bool=True, resize: Tuple[int, int]=None) -> np.ndarray: """Captures an uncompressed still YUV image to a Numpy array. """Capture an uncompressed still YUV image to a Numpy array. use_video_port (bool): Capture from the video port used for streaming (lower resolution, faster) resize ((int, int)): Resize the captured image. Loading