Created initial general documentation

This commit is contained in:
Joel Collins 2018-11-16 15:46:19 +00:00
parent dd7a3ff2e5
commit ea10cf1ce9
16 changed files with 607 additions and 73 deletions

View file

@ -81,7 +81,13 @@ class StreamAPI(MicroscopeView):
def get(self):
"""
Video streaming route. Put this in the src attribute of an img tag.
Real-time MJPEG stream from the microscope camera
.. :quickref: State; Camera stream
:>header Accept: image/jpeg
:>header Content-Type: image/jpeg
:status 200: stream active
"""
# Restart stream worker thread
self.microscope.camera.start_worker()
@ -99,7 +105,39 @@ class StateAPI(MicroscopeView):
def get(self):
"""
Return JSONified microscope state.
JSON representation of the microscope object.
.. :quickref: State; Microscope state
**Example request**:
.. sourcecode:: http
GET /state/ HTTP/1.1
Accept: application/json
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"position": {
"x": 0,
"y": 0,
"z": 0
},
"preview_active": false,
"record_active": false,
"stream_active": true
}
:>header Accept: application/json
:>header Content-Type: application/json
:status 200: state available
"""
return jsonify(self.microscope.state)
@ -115,17 +153,42 @@ class PositionAPI(MicroscopeView):
def get(self):
"""
Return current x, y and z positions of the stage.
.. :quickref: Position; Get current position
**Example request**:
.. sourcecode:: http
GET /position/ HTTP/1.1
Accept: application/json
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"x": 0,
"y": 0,
"z": 0
}
:>json int x: x steps
:>json int y: y steps
:>json int z: z steps
"""
return jsonify(self.microscope.state['position'])
def post(self):
"""
Set x, y and z positions of the stage.
.. :quickref: Position; Update current position
:reqheader Accept: application/json
:<json boolean absolute: (true) move to absolute position, (false) move by relative amount
:<json boolean force: allow moving by more than programmed limit
@ -183,8 +246,25 @@ class CaptureListAPI(MicroscopeView):
def get(self):
"""
Get list of image captures.
.. :quickref: Capture collection; Get collection of captures
:>header Accept: application/json
:query include_unavailable: return json representations of captures that have been completely deleted
:>jsonarr boolean available: availability of capture data
:>jsonarr string filename: filename of capture
:>jsonarr string id: unique id of the capture object
:>jsonarr boolean keep_on_disk: keep the capture file on microscope after closing
:>jsonarr boolean locked: file locked for modifications (mostly used for video recording)
:>jsonarr string path: path on pi storage to the capture file, if available
:>jsonarr boolean stream: capture stored in-memory as a BytesIO stream
:>jsonarr json uri: - **download** *(string)*: api uri to the capture file download
- **metadata** *(string)*: api uri to the capture json representation
:>header Content-Type: application/json
:status 200: capture found
:status 404: no capture found with that id
"""
include_unavailable = get_bool(request.args.get('include_unavailable'))
@ -197,8 +277,8 @@ class CaptureListAPI(MicroscopeView):
def delete(self):
"""
Delete all captures.
Delete all captures (not yet implemented)
.. :quickref: Capture collection; Delete all captures
"""
return jsonify({"error": "not yet implemented"})
@ -206,16 +286,47 @@ class CaptureListAPI(MicroscopeView):
def post(self):
"""
Create a new image capture.
.. :quickref: Capture collection; New capture
:reqheader Accept: application/json
**Example request**:
.. sourcecode:: http
POST /position/ HTTP/1.1
Accept: application/json
{
"filename": "myfirstcapture",
"keep_on_disk": true,
"use_video_port": true,
"size": {
"x": 640,
"y": 480
}
}
:>header Accept: application/json
:<json string filename: filename of stored capture
:<json boolean keep_on_disk: keep the capture file on microscope after closing
:<json boolean use_video_port: capture still image from the video port (lower resolution)
:<json boolean use_video_port: capture still image from the video port
:<json json size: - **x** *(int)*: x-axis resize
- **y** *(int)*: y-axis resize
:>json boolean available: availability of capture data
:>json string filename: filename of capture
:>json string id: unique id of the capture object
:>json boolean keep_on_disk: keep the capture file on microscope after closing
:>json boolean locked: file locked for modifications (mostly used for video recording)
:>json string path: path on pi storage to the capture file, if available
:>json boolean stream: capture stored in-memory as a BytesIO stream
:>json json uri: - **download** *(string)*: api uri to the capture file download
- **metadata** *(string)*: api uri to the capture json representation
:<header Content-Type: application/json
:status 200: capture created
:status 422: invalid parameters
"""
state = parse_payload(request)
@ -263,8 +374,48 @@ class CaptureAPI(MicroscopeView):
def get(self, capture_id):
"""
Get JSON representation of a capture
.. :quickref: Capture; Get capture
**Example request**:
.. sourcecode:: http
GET /capture/d0b2067abbb946f19351e075c5e7cd5b/ HTTP/1.1
Accept: application/json
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"available": true,
"filename": "2018-11-16_10-21-53.jpeg",
"id": "d0b2067abbb946f19351e075c5e7cd5b",
"keep_on_disk": false,
"locked": false,
"path": "capture/2018-11-16_10-21-53.jpeg",
"stream": false,
"uri": {
"download": "/api/v1/capture/d0b2067abbb946f19351e075c5e7cd5b/download",
"metadata": "/api/v1/capture/d0b2067abbb946f19351e075c5e7cd5b/"
}
}
:>json boolean available: availability of capture data
:>json string filename: filename of capture
:>json string id: unique id of the capture object
:>json boolean keep_on_disk: keep the capture file on microscope after closing
:>json boolean locked: file locked for modifications (mostly used for video recording)
:>json string path: path on pi storage to the capture file, if available
:>json boolean stream: capture stored in-memory as a BytesIO stream
:>json json uri: - **download** *(string)*: api uri to the capture file download
- **metadata** *(string)*: api uri to the capture json representation
"""
capture_obj = self.microscope.camera.image_from_id(capture_id)
@ -290,7 +441,7 @@ class CaptureAPI(MicroscopeView):
def delete(self, capture_id):
"""
Delete a capture
Delete a capture (not yet implemented)
.. :quickref: Capture; Delete capture
"""
@ -298,7 +449,7 @@ class CaptureAPI(MicroscopeView):
def put(self, capture_id):
"""
Modify the metadata of a capture
Modify the metadata of a capture (not yet implemented)
.. :quickref: Capture; Update capture metadata
"""
@ -315,6 +466,20 @@ class CaptureDownloadAPI(MicroscopeView):
Return image data for a capture.
.. :quickref: Capture; Download capture file
**Example request**:
.. sourcecode:: http
GET /capture/d0b2067abbb946f19351e075c5e7cd5b/download?thumbnail=true HTTP/1.1
Accept: image/jpeg
:>header Accept: image/jpeg
:query thumbnail: return an image thumbnail e.g. ?thumbnail=true
:>header Content-Type: image/jpeg
:status 200: capture data found
:status 404: no capture found with that id
"""
print(capture_id)
capture_obj = self.microscope.camera.image_from_id(capture_id)

View file

@ -34,13 +34,12 @@ def entry_by_id(id: str, object_list: list):
class CameraEvent(object):
def __init__(self):
"""
Create a frame-signaller object for StreamingCamera.
"""
A frame-signaller object used by any instances or subclasses of BaseCamera.
An event-like class that signals all active clients
when a new frame is available.
"""
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):
@ -79,21 +78,23 @@ class CameraEvent(object):
class BaseCamera(object):
"""
Base implementation of StreamingCamera.
"""
def __init__(self):
"""Base implementation of StreamingCamera."""
self.thread = None # Background thread that reads frames from camera
self.camera = None # Camera object, for direct access to camera
self.thread = None #: Background thread reading frames from camera
self.camera = None #: Camera object
self.frame = None # Current frame is stored here by background thread
self.last_access = 0 # Time of last client access to the camera
self.frame = None #: bytes: Current frame is stored here by background thread
self.last_access = 0 #: time: Time of last client access to the camera
self.event = CameraEvent()
self.state = {} # Create dict for capture state
self.settings = {} # Create dict to store settings
self.state = {} #: dict: Dictionary for capture state
self.settings = {} #: dict: Dictionary of camera settings
# Capture data
self.images = []
self.videos = []
self.images = [] #: list: List of image capture objects
self.videos = [] #: list: List of video recording objects
def __enter__(self):
"""Create camera on context enter."""

View file

@ -11,6 +11,9 @@ thumbnail_size = (60, 60)
class StreamObject(object):
"""
StreamObject used to store and process capture data, and metadata.
"""
def __init__(
self,
write_to_file: bool=None,

View file

@ -55,12 +55,12 @@ DEFAULT_CONFIG = os.path.join(HERE, 'config_picamera.yaml')
class StreamingCamera(BaseCamera):
"""Raspberry Pi camera implementation of StreamingCamera."""
def __init__(self):
"""Raspberry Pi camera implementation of StreamingCamera."""
# Run BaseCamera init
BaseCamera.__init__(self)
# Attach to Pi camera
self.camera = picamera.PiCamera()
self.camera = picamera.PiCamera() #: :py:class:`picamera.PiCamera`: Picamera object
# Camera settings
self.settings.update({
@ -93,13 +93,21 @@ class StreamingCamera(BaseCamera):
# HANDLE SETTINGS
# 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:
"""Open config_picamera.yaml file and write to camera."""
"""
Open config_picamera.yaml file and write to camera.
Todo:
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
"""
global DEFAULT_CONFIG
paused_stream = False
@ -164,7 +172,12 @@ class StreamingCamera(BaseCamera):
"Cannot update camera settings while recording is active.")
def change_zoom(self, zoom_value: int=1) -> None:
"""Change the camera zoom, handling recentering and scaling."""
"""Change the camera zoom, handling recentering and scaling.
Todo:
TODO: Needs to be re-implemented
"""
zoom_value = float(zoom_value)
if zoom_value < 1:
zoom_value = 1
@ -204,17 +217,20 @@ class StreamingCamera(BaseCamera):
folder: str='record',
fmt: str='h264',
quality: int=15):
"""Start a new video recording, writing to a target object.
"""Start recording.
Start a new video recording, writing to a target object.
Args:
target (str/BytesIO): Target object to write bytes to.
write_to_file (bool/NoneType): Should the StreamObject write to a file?
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.
Returns:
target_object (str/BytesIO): 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')
"""
# Start recording method only if a current recording is not running
if not self.state['record_active']:
@ -274,9 +290,9 @@ 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.
Args:
splitter_port (int): Splitter port to stop recording on
resolution ((int, int)): Resolution to set the camera to, after stopping recording.
"""
logging.debug("Pausing stream")
# If no resolution is specified, default to image_resolution
@ -296,9 +312,9 @@ class StreamingCamera(BaseCamera):
"""
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.
Args:
splitter_port (int): Splitter port to start recording on
resolution ((int, int)): Resolution to set the camera to, before starting recording.
"""
logging.debug("Unpausing stream")
if not resolution:
@ -330,17 +346,14 @@ class StreamingCamera(BaseCamera):
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.
Args:
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.
resize ((int, int)): Resize the captured image.
"""
# If no target is specified, store to StreamingCamera
if not target:
@ -391,9 +404,9 @@ class StreamingCamera(BaseCamera):
resize: Tuple[int, int]=None) -> np.ndarray:
"""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.
Args:
use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster.
resize ((int, int)): Resize the captured image.
"""
if use_video_port:
resolution = self.settings['video_resolution']

View file

@ -1,4 +1,8 @@
# -*- coding: utf-8 -*-
"""
Defines a microscope object, binding a camera and stage with basic functionality.
"""
import numpy as np
from openflexure_stage import OpenFlexureStage
@ -6,12 +10,20 @@ from .camera.pi import StreamingCamera
class Microscope(object):
"""
A basic microscope object.
The camera and stage should already be initialised, and passed as arguments.
Args:
camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object
microscope (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object
"""
def __init__(self, camera: StreamingCamera, stage: OpenFlexureStage):
"""Create the microscope object. The camera and stage should already be initialised."""
print("Assigning camera")
self.camera = camera
self.camera = camera #: :py:class:`openflexure_microscope.camera.pi.StreamingCamera`: Picamera object
print("Assigning stage")
self.stage = stage
self.stage = stage #: :py:class:`openflexure_stage.stage.OpenFlexureStage`: OpenFlexure stage object
self.stage.backlash = np.zeros(3, dtype=np.int)
def __enter__(self):
@ -30,6 +42,11 @@ class Microscope(object):
# Create unified state
@property
def state(self):
"""Dictionary of the basic microscope state.
Return:
dict: Dictionary containing position data, and :py:attr:`openflexure_microscope.camera.base.BaseCamera.state`
"""
state = {}
# Add stage position