Minor style updates
This commit is contained in:
parent
79974a71e5
commit
8031327a5b
5 changed files with 124 additions and 86 deletions
|
|
@ -1,25 +1,29 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
#TODO: Completely rewrite for new version of StreamingCamera
|
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
from importlib import import_module
|
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
|
import numpy as np
|
||||||
|
|
||||||
# Raspberry Pi camera module (requires picamera package)
|
|
||||||
#from camera.pi import Camera
|
|
||||||
from openflexure_microscope.camera.pi import StreamingCamera
|
from openflexure_microscope.camera.pi import StreamingCamera
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
cam = StreamingCamera()
|
cam = StreamingCamera()
|
||||||
|
|
||||||
|
|
||||||
def parse_payload(request):
|
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()
|
state = request.get_json()
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
||||||
def gen(camera):
|
def gen(camera):
|
||||||
"""Video streaming generator function."""
|
"""Video streaming generator function."""
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -29,31 +33,29 @@ def gen(camera):
|
||||||
yield (b'--frame\r\n'
|
yield (b'--frame\r\n'
|
||||||
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
|
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
'''
|
"""Video streaming home page."""
|
||||||
Video streaming home page.
|
|
||||||
'''
|
|
||||||
cam.start_worker() # Start the stream
|
cam.start_worker() # Start the stream
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/stream')
|
@app.route('/stream')
|
||||||
def stream():
|
def stream():
|
||||||
'''
|
"""Video streaming route. Put this in the src attribute of an img tag."""
|
||||||
Video streaming route. Put this in the src attribute of an img tag.
|
return Response(
|
||||||
'''
|
gen(cam),
|
||||||
return Response(gen(cam), mimetype='multipart/x-mixed-replace; boundary=frame')
|
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'])
|
@app.route('/capture/', methods=['GET', 'POST', 'PUT'])
|
||||||
def capture():
|
def capture():
|
||||||
'''
|
"""
|
||||||
POST/PUT: Capture a single image
|
POST/PUT: Capture a single image.
|
||||||
GET: Return capture status, or download latest capture
|
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
|
|
||||||
'''
|
|
||||||
if request.method == 'POST' or request.method == 'PUT':
|
if request.method == 'POST' or request.method == 'PUT':
|
||||||
state = parse_payload(request)
|
state = parse_payload(request)
|
||||||
print(state)
|
print(state)
|
||||||
|
|
@ -95,21 +97,23 @@ def capture():
|
||||||
|
|
||||||
state = cam.state
|
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')
|
return send_file(cam.image.data, mimetype='image/jpeg')
|
||||||
else:
|
else:
|
||||||
return jsonify(state)
|
return jsonify(state)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/record/', methods=['GET', 'POST', 'PUT'])
|
@app.route('/record/', methods=['GET', 'POST', 'PUT'])
|
||||||
def record():
|
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
|
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':
|
if request.method == 'POST' or request.method == 'PUT':
|
||||||
state = request.get_json()
|
state = request.get_json()
|
||||||
print(state)
|
print(state)
|
||||||
|
|
@ -125,11 +129,11 @@ def record():
|
||||||
status = bool(state['status'])
|
status = bool(state['status'])
|
||||||
|
|
||||||
# synchronise the arduino_time
|
# synchronise the arduino_time
|
||||||
if status == True:
|
if status is True:
|
||||||
response = cam.start_recording(filename=filename)
|
response = cam.start_recording(filename=filename)
|
||||||
return str(response)
|
return str(response)
|
||||||
|
|
||||||
elif status == False:
|
elif status is False:
|
||||||
response = cam.stop_recording()
|
response = cam.stop_recording()
|
||||||
return str(response)
|
return str(response)
|
||||||
|
|
||||||
|
|
@ -138,19 +142,27 @@ def record():
|
||||||
|
|
||||||
state = cam.state
|
state = cam.state
|
||||||
|
|
||||||
if (download == 'true' or download == 'True' or download =='1') and cam.state_record['recent'] is not None:
|
if ((
|
||||||
return send_file(cam.state_record['recent'],
|
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',
|
mimetype='video/H264',
|
||||||
as_attachment=True)
|
as_attachment=True)
|
||||||
else:
|
else:
|
||||||
return jsonify(state)
|
return jsonify(state)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/preview/', methods=['GET', 'POST', 'PUT'])
|
@app.route('/preview/', methods=['GET', 'POST', 'PUT'])
|
||||||
def preview():
|
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
|
GET: Return preview status
|
||||||
'''
|
"""
|
||||||
if request.method == 'POST' or request.method == 'PUT':
|
if request.method == 'POST' or request.method == 'PUT':
|
||||||
state = request.get_json()
|
state = request.get_json()
|
||||||
print(state)
|
print(state)
|
||||||
|
|
@ -158,7 +170,7 @@ def preview():
|
||||||
if 'status' in state:
|
if 'status' in state:
|
||||||
status = bool(state['status'])
|
status = bool(state['status'])
|
||||||
|
|
||||||
if status == False:
|
if status is False:
|
||||||
response = cam.stop_preview()
|
response = cam.stop_preview()
|
||||||
else:
|
else:
|
||||||
response = cam.start_preview()
|
response = cam.start_preview()
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ except ImportError:
|
||||||
# Slightly crappy debugger logging
|
# Slightly crappy debugger logging
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
|
|
||||||
def log(s):
|
def log(s):
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
print('DEBUG: {}'.format(s))
|
print('DEBUG: {}'.format(s))
|
||||||
|
|
@ -111,6 +112,11 @@ class StreamObject(object):
|
||||||
f.seek(0, 0) # Seek to the start of the file
|
f.seek(0, 0) # Seek to the start of the file
|
||||||
f.write(self.binary) # Write data bytes to 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:
|
def delete(self) -> bool:
|
||||||
"""
|
"""
|
||||||
If the StreamObject has been saved to disk, deletes the file and returns True.
|
If the StreamObject has been saved to disk, deletes the file and returns True.
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,9 @@ TYPES = {
|
||||||
'digital_gain': float,
|
'digital_gain': float,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def convert_config(config):
|
def convert_config(config):
|
||||||
"""Convert datatype of config based on type dictionary"""
|
"""Convert datatype of config based on type dictionary."""
|
||||||
global TYPES
|
global TYPES
|
||||||
|
|
||||||
for key in config:
|
for key in config:
|
||||||
|
|
@ -26,12 +27,8 @@ def convert_config(config):
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def load_config(yaml_path):
|
def load_config(yaml_path):
|
||||||
|
"""Load YAML file, pass through dictionary conversion, and return."""
|
||||||
with open(yaml_path) as config_file:
|
with open(yaml_path) as config_file:
|
||||||
return convert_config(yaml.load(config_file))
|
return convert_config(yaml.load(config_file))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
from pprint import pprint
|
|
||||||
|
|
||||||
config = load_config('config_picamera.yaml')
|
|
||||||
pprint(config)
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Raspberry Pi camera implementation of the StreamingCamera class.
|
Raspberry Pi camera implementation of the StreamingCamera class.
|
||||||
|
|
||||||
|
|
@ -51,8 +52,9 @@ from .config import load_config
|
||||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||||
DEFAULT_CONFIG = os.path.join(HERE, 'config_picamera.yaml')
|
DEFAULT_CONFIG = os.path.join(HERE, 'config_picamera.yaml')
|
||||||
|
|
||||||
class StreamingCamera(BaseCamera):
|
|
||||||
|
|
||||||
|
class StreamingCamera(BaseCamera):
|
||||||
|
"""Raspberry Pi camera implementation of StreamingCamera."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Capture data
|
# Capture data
|
||||||
self.image = None
|
self.image = None
|
||||||
|
|
@ -79,10 +81,11 @@ class StreamingCamera(BaseCamera):
|
||||||
BaseCamera.__init__(self)
|
BaseCamera.__init__(self)
|
||||||
|
|
||||||
def initialisation(self):
|
def initialisation(self):
|
||||||
|
"""Run any initialisation code when the frame iterator starts."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def start_preview(self) -> bool:
|
def start_preview(self) -> bool:
|
||||||
"""Function to 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()
|
||||||
|
|
@ -90,7 +93,7 @@ class StreamingCamera(BaseCamera):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def stop_preview(self) -> bool:
|
def stop_preview(self) -> bool:
|
||||||
"""Function to start 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()
|
||||||
|
|
@ -98,11 +101,12 @@ class StreamingCamera(BaseCamera):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# TODO: Handle exceptions
|
# 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:
|
def update_settings(self, config_path: str=None) -> None:
|
||||||
"""
|
"""Open config_picamera.yaml file and write to camera."""
|
||||||
Opens config_picamera.yaml file and writes valid settings
|
|
||||||
to the camera hardware object
|
|
||||||
"""
|
|
||||||
global DEFAULT_CONFIG
|
global DEFAULT_CONFIG
|
||||||
|
|
||||||
self.start_worker()
|
self.start_worker()
|
||||||
|
|
@ -203,27 +207,40 @@ class StreamingCamera(BaseCamera):
|
||||||
quality: int=15):
|
quality: int=15):
|
||||||
"""Start a new video recording, writing to a target object.
|
"""Start a new video recording, writing to a target object.
|
||||||
|
|
||||||
target (str/BytesIO): Target object to write bytes to (default StreamObject)
|
target (str/BytesIO): Target object to write bytes to.
|
||||||
write_to_file (bool/NoneType): Should the StreamObject write to a file? (default True for video capture)
|
(default StreamObject)
|
||||||
filename (str): Name of the stored file (defaults to timestamp)
|
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.
|
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()
|
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:
|
||||||
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
|
# Store to the StreamObject BytesIO
|
||||||
target_obj = self.video # Note the target StreamObject, used for function return
|
target = self.video.target
|
||||||
|
# Note the target StreamObject, used for function return
|
||||||
|
target_obj = self.video
|
||||||
|
|
||||||
target_obj.lock() # Lock the StreamObject while recording
|
target_obj.lock() # Lock the StreamObject while recording
|
||||||
|
|
||||||
else:
|
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
|
return None
|
||||||
else:
|
else:
|
||||||
target_obj = target
|
target_obj = target
|
||||||
|
|
@ -247,7 +264,8 @@ class StreamingCamera(BaseCamera):
|
||||||
Pause capture on a splitter port.
|
Pause capture on a splitter port.
|
||||||
|
|
||||||
splitter_port (int): Splitter port to stop recording on
|
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")
|
log("Pausing stream")
|
||||||
# If no resolution is specified, default to image_resolution
|
# 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:
|
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
|
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")
|
log("Unpausing stream")
|
||||||
if not resolution:
|
if not resolution:
|
||||||
|
|
@ -291,17 +310,21 @@ class StreamingCamera(BaseCamera):
|
||||||
fmt: str='jpeg',
|
fmt: str='jpeg',
|
||||||
resize: Tuple[int, int]=None):
|
resize: Tuple[int, int]=None):
|
||||||
"""
|
"""
|
||||||
Captures a still image to a StreamObject.
|
Capture a still image to a StreamObject.
|
||||||
|
|
||||||
Defaults to JPEG format.
|
Defaults to JPEG format.
|
||||||
Target object can be overridden for development purposes.
|
Target object can be overridden for development purposes.
|
||||||
|
|
||||||
target (str/BytesIO): Target object to write data bytes to
|
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?
|
write_to_file (bool): Should the StreamObject write to a file,
|
||||||
use_video_port (bool): Capture from the video port used for streaming (lower resolution, faster)
|
instead of BytesIO stream?
|
||||||
filename (str): Name of the stored file (defaults to timestamp)
|
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.
|
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.
|
resize ((int, int)): Resize the captured image.
|
||||||
"""
|
"""
|
||||||
self.start_worker()
|
self.start_worker()
|
||||||
|
|
@ -348,7 +371,7 @@ class StreamingCamera(BaseCamera):
|
||||||
return target_obj
|
return target_obj
|
||||||
|
|
||||||
def array(self, rgb=False, use_video_port: bool=True, resize: Tuple[int, int]=None) -> np.ndarray:
|
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)
|
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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue