Minor style updates

This commit is contained in:
Joel Collins 2018-11-06 15:57:33 +00:00
parent 79974a71e5
commit 8031327a5b
5 changed files with 124 additions and 86 deletions

View file

@ -1 +1 @@
__version__ = "0.1.0"
__version__ = "0.1.0"

View file

@ -1,25 +1,29 @@
#!/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:
@ -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
# 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)
@ -63,7 +65,7 @@ def capture():
filename = state['filename']
else:
filename = None
if 'write_to_file' in state:
write_to_file = bool(state['write_to_file'])
else:
@ -73,7 +75,7 @@ def capture():
use_video_port = bool(state['use_video_port'])
else:
use_video_port = False
if 'resize' in state:
resize_h = int(state['resize'])
resize_w = int(resize_h*(4/3))
@ -82,7 +84,7 @@ def capture():
resize = None
response = cam.capture(
write_to_file=write_to_file,
write_to_file=write_to_file,
use_video_port=use_video_port,
filename=filename,
resize=resize)
@ -94,22 +96,24 @@ def capture():
download = request.args.get('download')
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)
@ -125,32 +129,40 @@ 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)
else:
download = request.args.get('download')
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)
@ -158,13 +170,13 @@ 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()
return str(response)
else:
# Get args passed via URL (not useful here. Just for reference.)
state = cam.state
@ -172,4 +184,4 @@ def preview():
if __name__ == '__main__':
app.run(host='0.0.0.0', threaded=True, debug=True, use_reloader=False)
app.run(host='0.0.0.0', threaded=True, debug=True, use_reloader=False)

View file

@ -17,6 +17,7 @@ except ImportError:
# Slightly crappy debugger logging
DEBUG = True
def log(s):
if DEBUG:
print('DEBUG: {}'.format(s))
@ -71,13 +72,13 @@ class StreamObject(object):
file_path = os.path.join(folder, file_name)
else:
file_path = file_name
return file_path
def lock(self):
"""Set locked flag to True"""
self.locked = True
def unlock(self):
"""Set locked flag to False"""
self.locked = False
@ -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.

View file

@ -16,22 +16,19 @@ 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:
if key in TYPES:
config[key] = TYPES[key](config[key])
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)

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
"""
Raspberry Pi camera implementation of the StreamingCamera class.
@ -14,7 +15,7 @@ Video port:
StreamingCamera streams at video_resolution
Camera capture resolution set to video_resolution in frames()
Video port uses that resolution for everything. If a different resolution
is specified for video capture, this is handled by the resizer.
is specified for video capture, this is handled by the resizer.
Still capture (if use_video_port == False) uses pause_stream_for_capture
to temporarily increase the capture resolution.
@ -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
@ -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()
@ -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()
@ -98,13 +101,14 @@ 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()
if not config_path:
@ -162,7 +166,7 @@ class StreamingCamera(BaseCamera):
centre = np.array([fov[0] + fov[2]/2.0, fov[1] + fov[3]/2.0])
size = 1.0/zoom_value
# If the new zoom value would be invalid, move the centre to
# keep it within the camera's sensor (this is only relevant
# keep it within the camera's sensor (this is only relevant
# when zooming out, if the FoV is not centred on (0.5, 0.5)
for i in range(2):
if np.abs(centre[i] - 0.5) + size/2 > 0.5:
@ -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
@ -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
@ -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:
@ -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()
@ -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.