Initial commit
This commit is contained in:
commit
2f4f57a59b
16 changed files with 1509 additions and 0 deletions
63
.gitignore
vendored
Normal file
63
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
#Big-o files
|
||||
capture/
|
||||
record/
|
||||
*.data
|
||||
|
||||
#IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
5
README.md
Normal file
5
README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
OpenFlexure Microscope Software
|
||||
=====================
|
||||
|
||||
## Video streaming
|
||||
Based on supporting code for the article [video streaming with Flask](http://blog.miguelgrinberg.com/post/video-streaming-with-flask) and its follow-up [Flask Video Streaming Revisited](http://blog.miguelgrinberg.com/post/flask-video-streaming-revisited).
|
||||
1
activate
Normal file
1
activate
Normal file
|
|
@ -0,0 +1 @@
|
|||
source env/bin/activate
|
||||
1
openflexure_microscope/__init__.py
Normal file
1
openflexure_microscope/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
__version__ = "0.1.0"
|
||||
175
openflexure_microscope/api/app.py
Normal file
175
openflexure_microscope/api/app.py
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
#!/usr/bin/env python
|
||||
#TODO: Completely rewrite for new version of StreamingCamera
|
||||
from pprint import pprint
|
||||
from importlib import import_module
|
||||
import time, datetime
|
||||
|
||||
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
|
||||
state = request.get_json()
|
||||
return state
|
||||
|
||||
def gen(camera):
|
||||
"""Video streaming generator function."""
|
||||
while True:
|
||||
# the obtained frame is a jpeg
|
||||
frame = camera.get_frame()
|
||||
|
||||
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.
|
||||
'''
|
||||
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')
|
||||
|
||||
|
||||
#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
|
||||
'''
|
||||
if request.method == 'POST' or request.method == 'PUT':
|
||||
state = parse_payload(request)
|
||||
print(state)
|
||||
|
||||
# Handle filename argument
|
||||
if 'filename' in state:
|
||||
filename = state['filename']
|
||||
else:
|
||||
filename = None
|
||||
|
||||
if 'write_to_file' in state:
|
||||
write_to_file = bool(state['write_to_file'])
|
||||
else:
|
||||
write_to_file = False
|
||||
|
||||
if 'use_video_port' in state:
|
||||
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))
|
||||
resize = (resize_w, resize_h)
|
||||
else:
|
||||
resize = None
|
||||
|
||||
response = cam.capture(
|
||||
write_to_file=write_to_file,
|
||||
use_video_port=use_video_port,
|
||||
filename=filename,
|
||||
resize=resize)
|
||||
|
||||
return str(response) + "\n"
|
||||
|
||||
else: # If GET request
|
||||
|
||||
download = request.args.get('download')
|
||||
|
||||
state = cam.state
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
# Handle filename argument
|
||||
if 'filename' in state:
|
||||
filename = state['filename']
|
||||
else:
|
||||
filename = None
|
||||
|
||||
# Handle status argument
|
||||
if 'status' in state:
|
||||
status = bool(state['status'])
|
||||
|
||||
# synchronise the arduino_time
|
||||
if status == True:
|
||||
response = cam.start_recording(filename=filename)
|
||||
return str(response)
|
||||
|
||||
elif status == 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'],
|
||||
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
|
||||
GET: Return preview status
|
||||
'''
|
||||
if request.method == 'POST' or request.method == 'PUT':
|
||||
state = request.get_json()
|
||||
print(state)
|
||||
|
||||
if 'status' in state:
|
||||
status = bool(state['status'])
|
||||
|
||||
if status == 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
|
||||
return jsonify(state)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', threaded=True, debug=True, use_reloader=False)
|
||||
9
openflexure_microscope/api/templates/index.html
Normal file
9
openflexure_microscope/api/templates/index.html
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Video Streaming Demonstration</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Video Streaming Demonstration</h1>
|
||||
<img src="{{ url_for('stream') }}">
|
||||
</body>
|
||||
</html>
|
||||
1
openflexure_microscope/camera/__init__.py
Normal file
1
openflexure_microscope/camera/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import pi, base
|
||||
275
openflexure_microscope/camera/base.py
Normal file
275
openflexure_microscope/camera/base.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import time
|
||||
import os
|
||||
import io
|
||||
import threading
|
||||
import copy
|
||||
import datetime
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
from greenlet import getcurrent as get_ident
|
||||
except ImportError:
|
||||
try:
|
||||
from thread import get_ident
|
||||
except ImportError:
|
||||
from _thread import get_ident
|
||||
|
||||
# Slightly crappy debugger logging
|
||||
DEBUG = True
|
||||
|
||||
def log(s):
|
||||
if DEBUG:
|
||||
print('DEBUG: {}'.format(s))
|
||||
|
||||
|
||||
class StreamObject(object):
|
||||
def __init__(self, write_to_file: bool=None, filename: str=None, folder: str=None, fmt: str='file') -> None:
|
||||
# Store a nice ID
|
||||
self.id = filename
|
||||
log("Created {}".format(self.id))
|
||||
|
||||
# Create file name
|
||||
self.file = self.build_file_path(filename, folder, fmt)
|
||||
|
||||
# Byte stream properties
|
||||
self.stream = io.BytesIO() # Byte stream that data will be written to
|
||||
|
||||
# Set default write target
|
||||
self.write_to_file = write_to_file
|
||||
if self.write_to_file == False:
|
||||
log("Default target for {} set to 'stream'".format(self.id))
|
||||
self.target = self.stream
|
||||
else:
|
||||
log("Default target for {} set to 'file'".format(self.id))
|
||||
self.target = self.file
|
||||
|
||||
# Object lock
|
||||
self.locked = False # Is the StreamObject locked for writing? (Handled by StreamingCamera)
|
||||
|
||||
def __enter__(self):
|
||||
log("Entering context for {}. Stored files will be cleaned up automatically.".format(self.id))
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
log("Cleaning up {}".format(self.id))
|
||||
self.delete()
|
||||
|
||||
def build_file_path(self, filename: str, folder: str, fmt: str) -> str:
|
||||
"""
|
||||
Construct a full file path, based on filename, folder, and file format. Defaults to datestamp
|
||||
"""
|
||||
if filename:
|
||||
file_name = "{}.{}".format(filename, fmt)
|
||||
else:
|
||||
file_name = "{}.{}".format(datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S"), fmt)
|
||||
|
||||
# Create folder and file
|
||||
if folder:
|
||||
if not os.path.exists(folder):
|
||||
os.mkdir(folder)
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def data(self) -> io.BytesIO:
|
||||
"""Return a byte string of the capture data"""
|
||||
self.stream.seek(0) # Rewind the data bytes for reading
|
||||
|
||||
if not self.stream.getvalue() and os.path.isfile(self.file): # If stream is empty but file is not
|
||||
log("No stream data. Opening from file {}".format(self.file))
|
||||
with open(self.file, 'rb') as f:
|
||||
self.stream = io.BytesIO(f.read()) # Load bytes from file in disk
|
||||
self.stream.seek(0) # Rewind data bytes again
|
||||
|
||||
data = self.stream.getbuffer() # Create a copy of the stream bytes
|
||||
|
||||
return io.BytesIO(data) # Read and return bytes data
|
||||
|
||||
@property
|
||||
def binary(self) -> bytes:
|
||||
"""Return a byte string of the capture data"""
|
||||
return self.data.getvalue()
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Creates/opens a file, and writes this StreamObjects bytestring to the file.
|
||||
"""
|
||||
with open(self.file, 'ab') as f: # Load file as bytes
|
||||
log("Writing stream to file {}".format(self.file))
|
||||
f.seek(0, 0) # Seek to the start of the file
|
||||
f.write(self.binary) # Write data bytes to file
|
||||
|
||||
def delete(self) -> bool:
|
||||
"""
|
||||
If the StreamObject has been saved to disk, deletes the file and returns True.
|
||||
"""
|
||||
if os.path.isfile(self.file):
|
||||
log("Deleting file {}".format(self.file))
|
||||
os.remove(self.file)
|
||||
return True
|
||||
else:
|
||||
log("File not found {}".format(self.file))
|
||||
return False
|
||||
|
||||
|
||||
class CameraEvent(object):
|
||||
"""An Event-like class that signals all active clients when a new frame is
|
||||
available.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.events = {}
|
||||
|
||||
def wait(self, timeout: int=5):
|
||||
"""Invoked from each client's thread to wait for the next frame."""
|
||||
ident = get_ident()
|
||||
if ident not in self.events:
|
||||
# this is a new client
|
||||
# add an entry for it in the self.events dict
|
||||
# each entry has two elements, a threading.Event() and a timestamp
|
||||
self.events[ident] = [threading.Event(), time.time()]
|
||||
return self.events[ident][0].wait(timeout)
|
||||
|
||||
def set(self):
|
||||
"""Invoked by the camera thread when a new frame is available."""
|
||||
now = time.time()
|
||||
remove = None
|
||||
for ident, event in self.events.items():
|
||||
if not event[0].isSet():
|
||||
# if this client's event is not set, then set it
|
||||
# also update the last set timestamp to now
|
||||
event[0].set()
|
||||
event[1] = now
|
||||
else:
|
||||
# if the client's event is already set, it means the client
|
||||
# did not process a previous frame
|
||||
# if the event stays set for more than 5 seconds, then assume
|
||||
# the client is gone and remove it
|
||||
if now - event[1] > 5:
|
||||
remove = ident
|
||||
if remove:
|
||||
del self.events[remove]
|
||||
|
||||
def clear(self):
|
||||
"""Invoked from each client's thread after a frame was processed."""
|
||||
self.events[get_ident()][0].clear()
|
||||
|
||||
|
||||
class BaseCamera(object):
|
||||
thread = None # Background thread that reads frames from camera
|
||||
camera = None # Camera object, for direct access to camera
|
||||
|
||||
frame = None # Current frame is stored here by background thread
|
||||
last_access = 0 # Time of last client access to the camera
|
||||
event = CameraEvent()
|
||||
|
||||
def __init__(self):
|
||||
self.start_worker()
|
||||
|
||||
# Stores state of StreamingCamera
|
||||
self.state = {
|
||||
}
|
||||
|
||||
def start_worker(self, timeout: int=5) -> bool:
|
||||
"""Start the background camera thread if it isn't running yet."""
|
||||
timeout_time = time.time() + timeout
|
||||
|
||||
self.last_access = time.time()
|
||||
self.stop = False
|
||||
|
||||
if self.thread is None:
|
||||
# start background frame thread
|
||||
self.thread = threading.Thread(target=self._thread)
|
||||
self.thread.start()
|
||||
|
||||
# wait until frames are available
|
||||
log("Waiting for frames")
|
||||
while self.get_frame() is None:
|
||||
if time.time() > timeout_time:
|
||||
raise TimeoutError("Timeout waiting for frames.")
|
||||
break
|
||||
else:
|
||||
time.sleep(0)
|
||||
return True
|
||||
|
||||
def stop_worker(self, timeout: int=5) -> bool:
|
||||
"""Flags worker thread for stop. Waits for thread to close, or times out."""
|
||||
timeout_time = time.time() + timeout
|
||||
|
||||
self.stop = True
|
||||
while self.thread:
|
||||
if time.time() > timeout_time:
|
||||
raise TimeoutError("Timeout waiting for worker thread to close.")
|
||||
break
|
||||
else:
|
||||
time.sleep(0)
|
||||
return True
|
||||
|
||||
def get_frame(self):
|
||||
"""Return the current camera frame."""
|
||||
self.last_access = time.time()
|
||||
|
||||
# wait for a signal from the camera thread
|
||||
self.event.wait()
|
||||
self.event.clear()
|
||||
|
||||
return self.frame
|
||||
|
||||
def frames(self):
|
||||
"""Generator that returns frames from the camera."""
|
||||
raise RuntimeError('Must be implemented by subclasses.')
|
||||
|
||||
def close(self):
|
||||
"""Handles closing the StreamingCamera"""
|
||||
self.stop_worker()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
"""Close camera stream on context exit."""
|
||||
self.close()
|
||||
|
||||
def _thread(self):
|
||||
"""Camera background thread."""
|
||||
self.frames_iterator = self.frames()
|
||||
|
||||
# Update state
|
||||
self.state['stream_active'] = True
|
||||
|
||||
for frame in self.frames_iterator:
|
||||
self.frame = frame
|
||||
self.event.set() # send signal to clients
|
||||
time.sleep(0)
|
||||
|
||||
# if there hasn't been any clients asking for frames in
|
||||
# the last 10 seconds then stop the thread
|
||||
if time.time() - self.last_access > 20:
|
||||
self.frames_iterator.close()
|
||||
break
|
||||
|
||||
try:
|
||||
if self.stop is True:
|
||||
self.frames_iterator.close()
|
||||
break
|
||||
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# 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
|
||||
37
openflexure_microscope/camera/config.py
Normal file
37
openflexure_microscope/camera/config.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import yaml
|
||||
|
||||
TYPES = {
|
||||
'stream_resolution': tuple,
|
||||
'video_resolution': tuple,
|
||||
'image_resolution': tuple,
|
||||
'numpy_resolution': tuple,
|
||||
'jpeg_quality': int,
|
||||
'framerate': int,
|
||||
'awb_mode': str,
|
||||
'red_gain': float,
|
||||
'blue_gain': float,
|
||||
'shutter_speed': int,
|
||||
'saturation': int,
|
||||
'analog_gain': float,
|
||||
'digital_gain': float,
|
||||
}
|
||||
|
||||
def convert_config(config):
|
||||
"""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):
|
||||
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)
|
||||
35
openflexure_microscope/camera/config_picamera.yaml
Normal file
35
openflexure_microscope/camera/config_picamera.yaml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Exposure mode
|
||||
exposure_mode: 'off'
|
||||
|
||||
# The analog gain offers higher sensitivity and less noise than using digital gain only
|
||||
analog_gain: 1.
|
||||
digital_gain: 1.
|
||||
|
||||
# When queried, the shutter_speed property returns the shutter speed of the camera in microseconds,
|
||||
# or 0 which indicates that the speed will be automatically determined.
|
||||
# If high value does not work, need to decrease the fps for streaming
|
||||
shutter_speed: 30000
|
||||
|
||||
# Auto white balance: The red and blue values are returned Fraction instances.
|
||||
# The values will be between 0.0 and 8.0.
|
||||
awb_mode: 'off'
|
||||
|
||||
# If not using auto, then you can change the awb_gains
|
||||
red_gain: 1.3
|
||||
blue_gain: 1.2
|
||||
|
||||
# Color saturation of the camera as an integer between -100 and 100.
|
||||
saturation: 0
|
||||
|
||||
# ISO Valid values are between 0 (auto) and 800 (1600 is not available?).
|
||||
# ISO value is not used anyway as we are fixing the gains
|
||||
iso: 500
|
||||
|
||||
# Resolutions for streaming and capture
|
||||
video_resolution: [832, 624]
|
||||
image_resolution: [2592, 1944]
|
||||
numpy_resolution: [1312, 976]
|
||||
|
||||
# Capture quality
|
||||
jpeg_quality: 75
|
||||
framerate: 24
|
||||
433
openflexure_microscope/camera/pi.py
Normal file
433
openflexure_microscope/camera/pi.py
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Raspberry Pi camera implementation of the StreamingCamera class.
|
||||
|
||||
NOTES:
|
||||
Still port used for image capture
|
||||
Preview port reserved for onboard GPU preview
|
||||
Video port:
|
||||
Splitter port 0: Image capture (if use_video_port == True)
|
||||
Splitter port 1: Streaming frames
|
||||
Splitter port 2: Video capture
|
||||
Splitter port 3: [Currently unused]
|
||||
|
||||
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.
|
||||
|
||||
Still capture (if use_video_port == False) uses pause_stream_for_capture
|
||||
to temporarily increase the capture resolution.
|
||||
"""
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import io
|
||||
import time
|
||||
import datetime
|
||||
import sys
|
||||
import os
|
||||
import yaml
|
||||
import copy
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
# Pi camera
|
||||
import picamera
|
||||
import picamera.array
|
||||
|
||||
# Type hinting
|
||||
from typing import Tuple
|
||||
|
||||
# Threading
|
||||
import threading
|
||||
|
||||
from .base import BaseCamera, StreamObject, log
|
||||
# Richard's fix gain
|
||||
from .set_picamera_gain import set_analog_gain, set_digital_gain
|
||||
# Manage config
|
||||
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):
|
||||
|
||||
def __init__(self):
|
||||
# Capture data
|
||||
self.image = None
|
||||
self.video = None
|
||||
|
||||
# Camera settings
|
||||
self.settings = {
|
||||
'video_resolution': (832, 624),
|
||||
'image_resolution': (2592, 1944),
|
||||
'numpy_resolution': (1312, 976),
|
||||
'jpeg_quality': 75,
|
||||
}
|
||||
|
||||
# Store state of StreamingCamera
|
||||
self.state = {
|
||||
'video_recent': None,
|
||||
'image_recent': None,
|
||||
'stream_active': False,
|
||||
'record_active': False,
|
||||
'preview_active': False,
|
||||
}
|
||||
|
||||
# Finally, run BaseCamera init to start thread
|
||||
BaseCamera.__init__(self)
|
||||
|
||||
def initialisation(self):
|
||||
pass
|
||||
|
||||
def start_preview(self) -> bool:
|
||||
"""Function to start the onboard GPU camera preview."""
|
||||
self.start_worker()
|
||||
|
||||
self.camera.start_preview()
|
||||
self.state['preview_active'] = True
|
||||
return True
|
||||
|
||||
def stop_preview(self) -> bool:
|
||||
"""Function to start the onboard GPU camera preview."""
|
||||
self.start_worker()
|
||||
|
||||
self.camera.stop_preview()
|
||||
self.state['preview_active'] = False
|
||||
return True
|
||||
|
||||
# TODO: Handle exceptions
|
||||
def update_settings(self, config_path: str=None) -> None:
|
||||
"""
|
||||
Opens config_picamera.yaml file and writes valid settings
|
||||
to the camera hardware object
|
||||
"""
|
||||
global DEFAULT_CONFIG
|
||||
|
||||
self.start_worker()
|
||||
|
||||
if not config_path:
|
||||
config = load_config(DEFAULT_CONFIG)
|
||||
else:
|
||||
config = load_config(config_path)
|
||||
|
||||
log("Applying config:")
|
||||
log(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'])
|
||||
|
||||
# TODO: Rewrite this bit?
|
||||
# https://picamera.readthedocs.io/en/release-1.13/api_camera.html
|
||||
def change_zoom(self, zoom_value: int=1) -> None:
|
||||
"""Change the camera zoom, handling recentering and scaling."""
|
||||
zoom_value = float(zoom_value)
|
||||
if zoom_value < 1:
|
||||
zoom_value = 1
|
||||
# Richard's code for zooming !
|
||||
fov = self.camera.zoom
|
||||
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
|
||||
# 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:
|
||||
centre[i] = 0.5 + (1.0 - size)/2 * np.sign(centre[i]-0.5)
|
||||
print("setting zoom, centre {}, size {}".format(centre, size))
|
||||
new_fov = (centre[0] - size/2, centre[1] - size/2, size, size)
|
||||
self.camera.zoom = new_fov
|
||||
|
||||
def stop_recording(self, target=None) -> bool:
|
||||
"""Stop the last started video recording on splitter port 2.
|
||||
|
||||
target (str/BytesIO): Target object to write data bytes to.
|
||||
"""
|
||||
if not target: # If no target is defined
|
||||
target_obj = self.video # Assume StreamingCamera as target
|
||||
target_obj.unlock() # Unlock the StreamObject
|
||||
else:
|
||||
target_obj = target
|
||||
|
||||
# Stop the camera video recording on port 2
|
||||
log("Stopping recording")
|
||||
self.camera.stop_recording(splitter_port=2)
|
||||
log("Recording stopped")
|
||||
|
||||
# Update state dictionary
|
||||
self.state['record_active'] = False
|
||||
self.state['video_recent'] = str(target_obj)
|
||||
|
||||
return True
|
||||
|
||||
def start_recording(
|
||||
self,
|
||||
target=None,
|
||||
write_to_file: bool=True,
|
||||
filename: str=None,
|
||||
folder: str='record',
|
||||
fmt: str='h264',
|
||||
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)
|
||||
folder (str): Relative directory to store data file in.
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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.")
|
||||
return None
|
||||
else:
|
||||
target_obj = target
|
||||
|
||||
# Start the camera video recording on port 2
|
||||
log("Starting record at {}".format(self.settings['video_resolution']))
|
||||
self.camera.start_recording(
|
||||
target,
|
||||
format=fmt,
|
||||
splitter_port=2,
|
||||
resize=self.settings['video_resolution'],
|
||||
quality=quality)
|
||||
|
||||
# Update state dictionary
|
||||
self.state['record_active'] = True
|
||||
|
||||
return target_obj
|
||||
|
||||
def pause_stream_for_capture(self, splitter_port: int=1, resolution: Tuple[int, int]=None) -> None:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
log("Pausing stream")
|
||||
# If no resolution is specified, default to image_resolution
|
||||
if not resolution:
|
||||
resolution = self.settings['image_resolution']
|
||||
|
||||
# Stop the camera video recording on port 1
|
||||
self.camera.stop_recording(splitter_port=splitter_port)
|
||||
|
||||
# Increase the resolution for taking an image
|
||||
self.camera.resolution = resolution
|
||||
|
||||
def resume_stream_for_capture(self, splitter_port: int=1, resolution: Tuple[int, int]=None) -> None:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
log("Unpausing stream")
|
||||
if not resolution:
|
||||
resolution = self.settings['video_resolution']
|
||||
|
||||
# Reduce the resolution for video streaming
|
||||
self.camera.resolution = resolution
|
||||
|
||||
# Resume the video channel
|
||||
self.camera.start_recording(
|
||||
self.stream,
|
||||
format='mjpeg',
|
||||
quality=self.settings['jpeg_quality'],
|
||||
splitter_port=splitter_port)
|
||||
|
||||
def capture(
|
||||
self,
|
||||
target=None,
|
||||
write_to_file: bool=False,
|
||||
use_video_port: bool=False,
|
||||
filename: str=None,
|
||||
folder: str='capture',
|
||||
fmt: str='jpeg',
|
||||
resize: Tuple[int, int]=None):
|
||||
"""
|
||||
Captures 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)
|
||||
folder (str): Relative directory to store data file in.
|
||||
fmt (str): Format of the capture (default 'h264')
|
||||
resize ((int, int)): Resize the captured image.
|
||||
"""
|
||||
self.start_worker()
|
||||
|
||||
# If no target is specified, store to StreamingCamera
|
||||
if not target:
|
||||
self.image = StreamObject(write_to_file=write_to_file, filename=filename, folder=folder, fmt=fmt) # Reset StreamObject
|
||||
|
||||
target = self.image.target # Store to the StreamObject BytesIO
|
||||
target_obj = self.image # Note the target StreamObject, used for function return
|
||||
|
||||
else:
|
||||
target_obj = target
|
||||
|
||||
log("Capturing to {}".format(target))
|
||||
|
||||
if not use_video_port:
|
||||
|
||||
# Pause video splitter port 1
|
||||
self.pause_stream_for_capture()
|
||||
|
||||
self.camera.capture(
|
||||
target,
|
||||
format=fmt,
|
||||
quality=100,
|
||||
resize=resize,
|
||||
bayer=True)
|
||||
|
||||
# Resume video splitter port 1
|
||||
self.resume_stream_for_capture()
|
||||
|
||||
else:
|
||||
self.camera.capture(
|
||||
target,
|
||||
format=fmt,
|
||||
quality=100,
|
||||
resize=resize,
|
||||
bayer=False,
|
||||
use_video_port=True)
|
||||
|
||||
# Update state dictionary
|
||||
self.state['image_recent'] = str(target_obj)
|
||||
|
||||
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.
|
||||
|
||||
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()
|
||||
|
||||
if use_video_port:
|
||||
resolution = self.settings['video_resolution']
|
||||
else:
|
||||
resolution = self.settings['numpy_resolution']
|
||||
|
||||
if resize:
|
||||
size = resize
|
||||
else:
|
||||
size = resolution
|
||||
|
||||
if not use_video_port:
|
||||
self.pause_stream_for_capture(resolution=resolution)
|
||||
|
||||
log("Creating PiYUVArray")
|
||||
with picamera.array.PiYUVArray(self.camera, size=size) as output:
|
||||
log("Capturing to PiYUVArray from {}".format(self.camera))
|
||||
|
||||
self.camera.capture(
|
||||
output,
|
||||
resize=size,
|
||||
format='yuv',
|
||||
use_video_port=use_video_port)
|
||||
|
||||
log("Capturing complete")
|
||||
|
||||
if not use_video_port:
|
||||
self.resume_stream_for_capture()
|
||||
|
||||
if rgb:
|
||||
log("Converting to RGB")
|
||||
return output.rgb_array
|
||||
else:
|
||||
return output.array
|
||||
|
||||
def frames(self):
|
||||
"""
|
||||
Create iterator used by worker thread to generate stream frames.
|
||||
|
||||
Holds a Pi camera in context, records video from port 1 to a
|
||||
byte stream, and iterates sequential frames.
|
||||
"""
|
||||
# Run this initialisation method
|
||||
self.initialisation()
|
||||
self.stream_method = 'PiCamera'
|
||||
|
||||
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']
|
||||
|
||||
# streaming
|
||||
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)
|
||||
|
||||
while True:
|
||||
# reset stream for next frame
|
||||
self.stream.seek(0)
|
||||
self.stream.truncate()
|
||||
# to stream, read the new frame
|
||||
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
|
||||
70
openflexure_microscope/camera/set_picamera_gain.py
Normal file
70
openflexure_microscope/camera/set_picamera_gain.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
from __future__ import print_function
|
||||
|
||||
import picamera
|
||||
from picamera import mmal, mmalobj, exc
|
||||
from picamera.mmalobj import to_rational
|
||||
import time
|
||||
|
||||
|
||||
MMAL_PARAMETER_ANALOG_GAIN = mmal.MMAL_PARAMETER_GROUP_CAMERA + 0x59
|
||||
MMAL_PARAMETER_DIGITAL_GAIN = mmal.MMAL_PARAMETER_GROUP_CAMERA + 0x5A
|
||||
|
||||
def set_gain(camera, gain, value):
|
||||
"""Set the analog gain of a PiCamera.
|
||||
|
||||
camera: the picamera.PiCamera() instance you are configuring
|
||||
gain: either MMAL_PARAMETER_ANALOG_GAIN or MMAL_PARAMETER_DIGITAL_GAIN
|
||||
value: a numeric value that can be converted to a rational number.
|
||||
"""
|
||||
if gain not in [MMAL_PARAMETER_ANALOG_GAIN, MMAL_PARAMETER_DIGITAL_GAIN]:
|
||||
raise ValueError("The gain parameter was not valid")
|
||||
ret = mmal.mmal_port_parameter_set_rational(camera._camera.control._port,
|
||||
gain,
|
||||
to_rational(value))
|
||||
if ret == 4:
|
||||
raise exc.PiCameraMMALError(ret, "Are you running the latest version of the userland libraries? Gain setting was introduced in late 2017.")
|
||||
elif ret != 0:
|
||||
raise exc.PiCameraMMALError(ret)
|
||||
|
||||
def set_analog_gain(camera, value):
|
||||
"""Set the gain of a PiCamera object to a given value."""
|
||||
set_gain(camera, MMAL_PARAMETER_ANALOG_GAIN, value)
|
||||
|
||||
def set_digital_gain(camera, value):
|
||||
"""Set the digital gain of a PiCamera object to a given value."""
|
||||
set_gain(camera, MMAL_PARAMETER_DIGITAL_GAIN, value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with picamera.PiCamera() as cam:
|
||||
cam.start_preview(fullscreen=False, window=(0,50,640,480))
|
||||
time.sleep(2)
|
||||
|
||||
# fix the auto white balance gains at their current values
|
||||
g = cam.awb_gains
|
||||
cam.awb_mode = "off"
|
||||
cam.awb_gains = g
|
||||
|
||||
# fix the shutter speed
|
||||
cam.shutter_speed = cam.exposure_speed
|
||||
|
||||
print("Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain))
|
||||
|
||||
print("Attempting to set analogue gain to 1")
|
||||
set_analog_gain(cam, 1)
|
||||
print("Attempting to set digital gain to 1")
|
||||
set_digital_gain(cam, 1)
|
||||
# The old code is left in here in case it is a useful example...
|
||||
#ret = mmal.mmal_port_parameter_set_rational(cam._camera.control._port,
|
||||
# MMAL_PARAMETER_DIGITAL_GAIN,
|
||||
# to_rational(1))
|
||||
#print("Return code: {}".format(ret))
|
||||
|
||||
try:
|
||||
while True:
|
||||
print("Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain))
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("Stopping...")
|
||||
|
||||
|
||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
click==6.7
|
||||
flask >= 0.12.3
|
||||
itsdangerous==0.24
|
||||
Jinja2==2.9.6
|
||||
MarkupSafe==1.0
|
||||
picamera==1.13
|
||||
pyyaml==3.13
|
||||
numpy==1.10.1
|
||||
Pillow
|
||||
RPi.GPIO
|
||||
56
setup.py
Normal file
56
setup.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from setuptools import setup, find_packages
|
||||
from codecs import open
|
||||
from os import path
|
||||
import re
|
||||
|
||||
__author__ = 'Joel Collins'
|
||||
|
||||
HERE = path.abspath(path.dirname(__file__))
|
||||
|
||||
# Get the long description from the README file
|
||||
with open(path.join(HERE, 'README.md'), encoding='utf-8') as f:
|
||||
LONG_DESCRIPTION = f.read()
|
||||
|
||||
|
||||
def find_version():
|
||||
"""Determine the version based on __init__.py."""
|
||||
with open(path.join(HERE, "openflexure_microscope", "__init__.py"), 'r') as f:
|
||||
init_py = f.read()
|
||||
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", init_py, re.M)
|
||||
if version_match:
|
||||
return version_match.group(1)
|
||||
raise RuntimeError("Couldn't parse version string from __init__.py")
|
||||
|
||||
VERSION = find_version()
|
||||
|
||||
setup(
|
||||
name='openflexure_microscope',
|
||||
version=VERSION,
|
||||
description='Control scripts for the OpenFlexure Microscope',
|
||||
long_description=LONG_DESCRIPTION,
|
||||
author='Joel Collins',
|
||||
author_email='joel@jtcollins.net',
|
||||
packages=find_packages(),
|
||||
keywords=['arduino', 'python', 'serial', 'microscope'],
|
||||
zip_safe=True,
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||
'Programming Language :: Python :: 3.5'
|
||||
],
|
||||
install_requires=[
|
||||
'pyserial',
|
||||
'future',
|
||||
'openflexure_stage',
|
||||
'flask',
|
||||
'itsdangerous',
|
||||
'Jinja2',
|
||||
'MarkupSafe',
|
||||
'picamera',
|
||||
'Werkzeug',
|
||||
'pyyaml',
|
||||
'numpy',
|
||||
'Pillow',
|
||||
'RPi.GPIO',
|
||||
],
|
||||
)
|
||||
1
start_interface
Normal file
1
start_interface
Normal file
|
|
@ -0,0 +1 @@
|
|||
gunicorn --threads 5 --workers 1 --bind 0.0.0.0:5000 openflexure_microscope.api.app:app
|
||||
337
tests/test_camera.py
Normal file
337
tests/test_camera.py
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
#!/usr/bin/env python
|
||||
from openflexure_microscope.camera.pi import StreamingCamera, StreamObject, log
|
||||
import os
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
import numpy as np
|
||||
import uuid
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import unittest
|
||||
|
||||
success_string = """
|
||||
/O
|
||||
| |
|
||||
_____) \\
|
||||
(__0) \\______
|
||||
(____0)
|
||||
(____0) EVERYTHING IS OK!
|
||||
(__o)___________
|
||||
"""
|
||||
|
||||
|
||||
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):
|
||||
"""Tests capturing still images to a BytesIO stream."""
|
||||
global camera
|
||||
|
||||
for use_video_port in [True, False]:
|
||||
for resize in [None, (640, 480)]:
|
||||
# Create unique ID
|
||||
id = uuid.uuid4().hex
|
||||
|
||||
log("Resize: {}, V_Port: {}, ID: {}".format(
|
||||
resize,
|
||||
use_video_port,
|
||||
id))
|
||||
|
||||
# Wait for camera
|
||||
wait_for_cam(camera)
|
||||
|
||||
# Capture to a context (auto-deletes files when done)
|
||||
with camera.capture(
|
||||
write_to_file=False,
|
||||
use_video_port=use_video_port,
|
||||
filename=id,
|
||||
resize=resize) as stream:
|
||||
|
||||
# Ensure file deletion fails and returns False
|
||||
self.assertFalse(stream.delete())
|
||||
# Ensure capture not stored to file
|
||||
self.assertFalse(os.path.isfile(stream.file))
|
||||
|
||||
# BEFORE DELETE: 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)
|
||||
))
|
||||
|
||||
# Save capture to file
|
||||
stream.save()
|
||||
# Check file got saved
|
||||
self.assertTrue(os.path.isfile(stream.file))
|
||||
|
||||
# Delete file
|
||||
stream.delete()
|
||||
# Check file got deleted
|
||||
self.assertFalse(os.path.isfile(stream.file))
|
||||
|
||||
# AFTER DELETE: 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)
|
||||
))
|
||||
|
||||
# Create a PIL image from stream
|
||||
image = Image.open(stream.data)
|
||||
|
||||
# Ensure a valid PIL image was created
|
||||
self.assertTrue(isinstance(image, Image.Image))
|
||||
|
||||
# Calculate expected dimensions
|
||||
if resize:
|
||||
dims = resize
|
||||
else:
|
||||
if use_video_port:
|
||||
dims = camera.settings['video_resolution']
|
||||
else:
|
||||
dims = camera.settings['image_resolution']
|
||||
|
||||
# Ensure PIL image size matches expected size
|
||||
print(image.size, dims)
|
||||
self.assertTrue(image.size == dims)
|
||||
|
||||
def test_still_store(self):
|
||||
"""Tests capturing still images to a file on disk."""
|
||||
global camera
|
||||
|
||||
for use_video_port in [True, False]:
|
||||
for resize in [None, (640, 480)]:
|
||||
# Create unique ID
|
||||
id = uuid.uuid4().hex
|
||||
|
||||
# Wait for camera
|
||||
wait_for_cam(camera)
|
||||
|
||||
# Capture
|
||||
stream = camera.capture(
|
||||
write_to_file=True,
|
||||
use_video_port=use_video_port,
|
||||
filename=id,
|
||||
resize=resize)
|
||||
|
||||
# Check file got saved
|
||||
self.assertTrue(os.path.isfile(stream.file))
|
||||
statinfo = os.stat(stream.file)
|
||||
self.assertTrue(statinfo.st_size > 0)
|
||||
|
||||
# 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)))
|
||||
|
||||
# Ensure file deletion completes and returns True
|
||||
self.assertTrue(stream.delete())
|
||||
|
||||
# Check file got deleted
|
||||
self.assertFalse(os.path.isfile(stream.file))
|
||||
|
||||
|
||||
class TestUnencodedMethods(unittest.TestCase):
|
||||
|
||||
def test_yuv_array(self):
|
||||
"""Tests capturing unencoded YUV data to a Numpy array."""
|
||||
global camera
|
||||
|
||||
for use_video_port in [True, False]:
|
||||
for resize in [None, (640, 480)]:
|
||||
# Create unique ID
|
||||
id = uuid.uuid4().hex
|
||||
|
||||
print("{}, {}, {}".format(id, resize, use_video_port))
|
||||
|
||||
# Wait for camera
|
||||
wait_for_cam(camera)
|
||||
|
||||
# Capture RGB array
|
||||
yuv = camera.array(
|
||||
use_video_port=use_video_port,
|
||||
resize=resize)
|
||||
|
||||
# Ensure capture output is a valid numpy array
|
||||
self.assertTrue(isinstance(yuv, np.ndarray))
|
||||
|
||||
# Calculate expected dimensions
|
||||
if resize:
|
||||
dims = resize
|
||||
else:
|
||||
if use_video_port:
|
||||
dims = camera.settings['video_resolution']
|
||||
else:
|
||||
dims = camera.settings['numpy_resolution']
|
||||
|
||||
# Ensure array shape matches expected dimensions
|
||||
self.assertTrue(yuv.shape == (dims[1], dims[0], 3))
|
||||
|
||||
def test_rgb_array(self):
|
||||
"""Tests capturing unencoded YUV/RGB data to a Numpy array."""
|
||||
global camera
|
||||
|
||||
for use_video_port in [True, False]:
|
||||
for resize in [None, (640, 480)]:
|
||||
# Create unique ID
|
||||
id = uuid.uuid4().hex
|
||||
|
||||
print("{}, {}, {}".format(id, resize, use_video_port))
|
||||
|
||||
# Wait for camera
|
||||
wait_for_cam(camera)
|
||||
|
||||
# Capture RGB array
|
||||
rgb = camera.array(
|
||||
rgb=True,
|
||||
use_video_port=use_video_port,
|
||||
resize=resize)
|
||||
|
||||
# Ensure capture output is a valid numpy array
|
||||
self.assertTrue(isinstance(rgb, np.ndarray))
|
||||
|
||||
# Calculate expected dimensions
|
||||
if resize:
|
||||
dims = resize
|
||||
else:
|
||||
if use_video_port:
|
||||
dims = camera.settings['video_resolution']
|
||||
else:
|
||||
dims = camera.settings['numpy_resolution']
|
||||
|
||||
# Ensure array shape matches expected dimensions
|
||||
self.assertTrue(rgb.shape == (dims[1], dims[0], 3))
|
||||
|
||||
|
||||
class TestRecordMethods(unittest.TestCase):
|
||||
|
||||
def test_video_record(self):
|
||||
"""Tests recording videos to BytesIO stream, and to file on disk."""
|
||||
global camera
|
||||
|
||||
for write_to_file in [True, False, None]:
|
||||
|
||||
# Create unique ID
|
||||
id = uuid.uuid4().hex
|
||||
|
||||
# Wait for camera
|
||||
wait_for_cam(camera)
|
||||
|
||||
# Start recording
|
||||
with camera.start_recording(
|
||||
write_to_file=write_to_file,
|
||||
filename=id) as stream:
|
||||
|
||||
# Record for 2 seconds
|
||||
time.sleep(2)
|
||||
# Stop recording
|
||||
camera.stop_recording()
|
||||
|
||||
# Check stream
|
||||
self.assertTrue(isinstance(stream.data, io.IOBase))
|
||||
self.assertTrue(isinstance(stream.binary, (bytes, bytearray)))
|
||||
|
||||
# Check file
|
||||
if write_to_file:
|
||||
statinfo = os.stat(stream.file)
|
||||
self.assertTrue(statinfo.st_size > 0)
|
||||
|
||||
# Log path
|
||||
temp_path = stream.file
|
||||
|
||||
# Check file got deleted on __exit__
|
||||
self.assertFalse(os.path.isfile(temp_path))
|
||||
|
||||
time.sleep(0.25)
|
||||
|
||||
def test_video_store(self):
|
||||
"""Tests recording videos to file on disk, without context manager."""
|
||||
global camera
|
||||
|
||||
# Create unique ID
|
||||
id = uuid.uuid4().hex
|
||||
|
||||
# Wait for camera
|
||||
wait_for_cam(camera)
|
||||
|
||||
# Start recording
|
||||
stream = camera.start_recording(filename=id)
|
||||
# Record for 2 seconds
|
||||
time.sleep(2)
|
||||
# Stop recording
|
||||
camera.stop_recording()
|
||||
|
||||
# Check file
|
||||
statinfo = os.stat(stream.file)
|
||||
self.assertTrue(statinfo.st_size > 0)
|
||||
|
||||
# Ensure file deletion completes and returns True
|
||||
self.assertTrue(stream.delete())
|
||||
|
||||
# Check file got deleted
|
||||
self.assertFalse(os.path.isfile(stream.file))
|
||||
|
||||
|
||||
class TestThreadStarting(unittest.TestCase):
|
||||
def test_capture_restarting_stream(self):
|
||||
"""Tests that a capture call restarts the camera worker thread."""
|
||||
global camera
|
||||
|
||||
# Create unique ID
|
||||
id = uuid.uuid4().hex
|
||||
|
||||
# Wait for camera
|
||||
wait_for_cam(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)
|
||||
|
||||
# Restart worker thread by initialising a capture
|
||||
with camera.capture(
|
||||
use_video_port=True,
|
||||
filename=id) as stream:
|
||||
|
||||
# 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)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
camera = None
|
||||
with StreamingCamera() as camera:
|
||||
|
||||
suites = [
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestCaptureMethods),
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestUnencodedMethods),
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestThreadStarting),
|
||||
unittest.TestLoader().loadTestsFromTestCase(TestRecordMethods),
|
||||
]
|
||||
|
||||
alltests = unittest.TestSuite(suites)
|
||||
|
||||
result = unittest.TextTestRunner(verbosity=2).run(alltests)
|
||||
|
||||
if result.wasSuccessful():
|
||||
print(success_string)
|
||||
|
||||
camera.close()
|
||||
Loading…
Add table
Add a link
Reference in a new issue