Commit d75efa63 authored by Joel Collins's avatar Joel Collins
Browse files

Added capture and capture previews to web UI

parent 49331d36
Loading
Loading
Loading
Loading
+128 −20
Original line number Diff line number Diff line
// TODO: Remember a position and restore that position
// TODO: Right click to auto-focus, centered on that region
/*
TODO: New capture panel
TODO: Make delete button work

TODO: Recordings list
TODO: New recording panel
TODO: Somehow have Python make funky thumbnails for videos?

TODO: Remember a position and restore that position
TODO: Right click to auto-focus, centered on that region

TODO: Box to specify device IP
TODO: Dynamically add stream image src from JS (allows streaming from an external IP)
*/

var baseURI = "/api/v1"

@@ -42,7 +54,36 @@ function getCaptures() {
}

function updateCaptures(response) {
    console.log(response)
    // Clear captures list
    var capturesNode = document.getElementById("captures");
    while (capturesNode.firstChild) {
        capturesNode.removeChild(capturesNode.firstChild);
    }

    // For each capture in the response array
    response.forEach(function(element) {
        // Store the capture object URI
        element_uri = `${baseURI}/capture/${element.id}`

        // Generate inner HTML from capture object
        html = `
        <div class="clearfix">
            <div class="left-col"><img src="${element_uri}/thumbnail"></div>
            <div class="right-col"> 
                <b>${element.file}</b><br>
                <a href="${element_uri}/download" target="_blank">View</a> <a href="${element_uri}/download?as_attachment=true">Download</a> Delete <a href="${element_uri}/" target="_blank">JSON</a>
                <br>
            </div>
        </div>
        `

        // Create a new capture div, and append HTML
        var div = document.createElement("div");
        div.className = "capture";
        div.innerHTML = html;
        capturesNode.appendChild(div);
    });

}

function updateTextBoxes() {
@@ -56,9 +97,9 @@ function updateTextBoxes() {
}

function updateStagePositions(response) {
    document.getElementById('x_abs').value = response["x"];
    document.getElementById('y_abs').value = response["y"];
    document.getElementById('z_abs').value = response["z"];
    document.getElementById('x_abs').value = response.x;
    document.getElementById('y_abs').value = response.y;
    document.getElementById('z_abs').value = response.z;
}

function getStagePositions() {
@@ -69,13 +110,79 @@ function getStagePositions() {
    safeRequest("GET", baseURI+"/position", null, updatePositionCallback, false)
}

// Capture methods
function newCapture(filename, keep_on_disk, use_video_port, resizeWidth=null, resizeHeight=null) {
    // Make a position request
    function newCaptureCallback(response, status) {
        if (status != 200) {
            alert("Capture failed.");
        }
        console.log(status, response);
        getCaptures();  // Update full list of captures when done
    }

    payload = {
        "filename": filename,
        "keep_on_disk": keep_on_disk,
        "use_video_port": use_video_port
    }

    if ((resizeWidth) && (resizeHeight)) {
        payload.size = {
            "width": resizeWidth,
            "height": resizeHeight
        }
    }

    console.log(payload);
    safeRequest("POST", baseURI+"/capture/", payload, newCaptureCallback);
}

function newCaptureFromInput() {
    captureFilenameInput = document.getElementById('captureFilenameInput');
    captureKeepOnDiskCheck = document.getElementById('captureKeepOnDiskCheck');
    captureFullResolutionCheck = document.getElementById('captureFullResolutionCheck');
    captureResizeCheck = document.getElementById('captureResizeCheck');
    captureWidthInput = document.getElementById('captureWidthInput')
    captureHeightInput = document.getElementById('captureHeightInput')

    // Get filename if one is given
    if (captureFilenameInput.value === "") {
        filename = null;
    }
    else {
        filename = captureFilenameInput.value;
    }

    if (captureResizeCheck.checked) {
        resizeWidth = Number(captureWidthInput.value);
        resizeHeight = Number(captureHeightInput.value);
    }
    else {
        resizeWidth = null;
        resizeHeight = null;
    }

    newCapture(filename, captureKeepOnDiskCheck.checked, !(captureFullResolutionCheck.checked), resizeWidth, resizeHeight);

}

function newCaptureResizeToggle(checkbox) {
    captureWidthInput = document.getElementById('captureWidthInput')
    captureHeightInput = document.getElementById('captureHeightInput')

    captureWidthInput.disabled = !(checkbox.checked)
    captureHeightInput.disabled = !(checkbox.checked)
}

// Move stage methods
// TODO: Combine setStagePositions and moveStagePositions, with bool argument for 'absolute'

function setStagePositions(x_abs, y_abs, z_abs) {
    // Make a position request
    function absMoveCallback(response, status) {
        if (status == 400) {
            alert("Stage cannot be moved further than the safe range.");
        if (status != 200) {
            alert("Error moving stage.");
        }
        console.log(status, response);
        updateStagePositions(response);
@@ -86,8 +193,8 @@ function setStagePositions(x_abs, y_abs, z_abs) {
function moveStagePositions(x_rel, y_rel, z_rel) {
    // Make a position request
    function relMoveCallback(response, status) {
        if (status == 400) {
            alert("Stage cannot be moved further than the safe range.");
        if (status != 200) {
            alert("Error moving stage.");
        }
        console.log(status, response);
        updateStagePositions(response);
@@ -131,9 +238,9 @@ function clickHotspotImage(event) {
window.addEventListener('wheel', function(e) {
    multiplier = 1/100;
    z_delta = e.deltaY * multiplier * focusVelocity;
    if ((document.getElementById("scrollFocusCheck").checked == true) && (e.target.parentNode.classList["value"] == "column middle")) {
    if ((document.getElementById("scrollFocusCheck").checked == true) && (e.target.parentNode.classList.value == "column middle")) {
        console.log(z_delta);
        console.log(e.target.parentNode.classList["value"]);
        console.log(e.target.parentNode.classList.value);
        // Make a position request
        safeRequest("POST", baseURI+"/position", { "absolute": false, "z": z_delta}, keyMoveCallback)
    }
@@ -196,21 +303,21 @@ function safeRequest(method, url, payload, callback, lock=true, force=false) {
        var xhr = new XMLHttpRequest();   // new HttpRequest instance 
        xhr.open(method, url);

        if (lock == true) {
            requestLock = true;
        if (lock == true) {  // If request is using the lock
            requestLock = true;  // Hold the lock
        }

        xhr.onload = function (e) {
            if (xhr.readyState === 4) {
                if (lock == true) {  // If request is using the lock
                    requestLock = false;  // Release the lock
                }

                if (xhr.status !== 200) {
                    console.error(xhr.statusText);
                } 
                callback(JSON.parse(xhr.responseText), xhr.status);
            }

            if (lock == true) {
                requestLock = false;
            }
        };

        if (method == "POST" || method == "PUT") {
@@ -221,6 +328,7 @@ function safeRequest(method, url, payload, callback, lock=true, force=false) {

    }
    else {
        console.log("Cannot start new request while previous requets is pending...")
        console.log("Cannot start the following request while the previous requets is pending:")
        console.log(method, url, payload)
    }
}
+7 −1
Original line number Diff line number Diff line
@@ -10,7 +10,7 @@ body {
}

/* Preview sizing */
img {  
.middle img {  
    width: 100%;
    height: 100%;
    object-fit: contain
@@ -68,8 +68,14 @@ img {

#captures {
    font-size:12px;
    margin-top: 20px;
}

.capture {
    background-color: #d3d3d3;
    margin-bottom: 5px;
}

.capture img {
    width: 60px;
}
 No newline at end of file
+16 −7
Original line number Diff line number Diff line
@@ -28,14 +28,14 @@
          <div class="clearfix">
              <div class="left-col">FOV width:</div> 
              <div class="right-col"> 
                <input type="text" id="fovXText" size="4" onchange="fovX = Number(this.value); updateTextBoxes();">
                <input type="number" id="fovXText" onchange="fovX = Number(this.value); updateTextBoxes();" style="width: 7em" >
              </div> 
          </div>

          <div class="clearfix">
              <div class="left-col">FOV height:</div> 
              <div class="right-col"> 
                  <input type="text" id="fovYText" size="4" onchange="fovY = Number(this.value); updateTextBoxes();">
                  <input type="number" id="fovYText" onchange="fovY = Number(this.value); updateTextBoxes();" style="width: 7em" >
              </div> 
          </div>
        </p>
@@ -72,16 +72,25 @@
      </div>

      <div class="column right">
        <h2>Capture</h2>
        Filename: <br>
        <input type="text" id="captureFilenameInput" placeholder="Leave blank for default"><br>
        Store on Pi: <input type="checkbox" id="captureKeepOnDiskCheck"><br>
        Full resolution: <input type="checkbox" id="captureFullResolutionCheck"><br>
        Resize capture: <input type="checkbox" id="captureResizeCheck" onclick="newCaptureResizeToggle(this);"><br>
        <input type="number" id="captureWidthInput" value="640" style="width: 7em" disabled>
        <input type="number" id="captureHeightInput" value="480" style="width: 7em" disabled><br>

        <button type="button" onclick="newCaptureFromInput();">Capture</button>

        <h2>Captures</h2>
        <div id="captures">
          <p>Not yet implemented.</p>
          <div class="capture">Capture 1</div>
        </div>
        <button type="button" onclick="getCaptures();">Update</button>
        <div id="captures"></div>
        
      </div>

      <div class="column middle">
        <img src="/api/v1/stream" ondblclick="clickHotspotImage(event)" ;="" style="width: 100%;">
        <img src="/api/v1/stream" ondblclick="clickHotspotImage(event);">
      </div>

    </div>
+72 −2
Original line number Diff line number Diff line
@@ -14,7 +14,7 @@ import datetime

from flask import (
    Flask, render_template, Response,
    redirect, request, jsonify, send_file)
    redirect, request, jsonify, send_file, abort)


import numpy as np
@@ -119,8 +119,10 @@ def position():

    return jsonify(microscope.state['position'])


# Capture routes
@app.route(uri('/capture'), methods=['GET', 'POST', 'PUT'])

@app.route(uri('/capture/'), methods=['GET', 'POST', 'PUT'])
def capture():
    """Return JSONified microscope state"""
    global microscope
@@ -173,5 +175,73 @@ def capture():

        return jsonify(metadata_array)


@app.route(uri('/capture/<capture_uuid>/'), methods=['GET', 'DELETE'])
def get_capture(capture_uuid):
    """Return JSONified capture by UUID"""
    global microscope

    print(capture_uuid)
    capture_obj = microscope.camera.image_from_id(capture_uuid)

    if not capture_obj:
        return abort(404)  # 404 Not Found

    # Get capture metadata
    capture_metadata = capture_obj.metadata

    # Add API routes to returned metadata
    uri_dict = {
        'uri': {'metadata': uri('/capture/{}/'.format(capture_uuid))}
    }

    # If available, also add download link
    if capture_metadata['available']:
        uri_dict['uri']['download'] = uri('/capture/{}/download'.format(capture_uuid))

    capture_metadata.update(uri_dict)

    if request.method == 'DELETE':
        print("DELETE NOT YET IMPLEMENETED")
        return jsonify(capture_metadata)

    else:  # GET requests
        return jsonify(capture_metadata)


@app.route(uri('/capture/<capture_uuid>/download'), methods=['GET'])
def download_capture(capture_uuid):
    """Return capture file by UUID"""
    global microscope

    print(capture_uuid)
    capture_obj = microscope.camera.image_from_id(capture_uuid)

    if not capture_obj:
        return abort(404)  # 404 Not Found

    as_attachment = get_bool(request.args.get('as_attachment'))

    return send_file(
        capture_obj.data,
        mimetype='image/jpeg',
        as_attachment=as_attachment,
        attachment_filename=capture_obj.filename)

@app.route(uri('/capture/<capture_uuid>/thumbnail'), methods=['GET'])
def thumb_capture(capture_uuid):
    """Return capture thumbnail by UUID"""
    global microscope

    print(capture_uuid)
    capture_obj = microscope.camera.image_from_id(capture_uuid)

    if not capture_obj:
        return abort(404)  # 404 Not Found

    return send_file(
        capture_obj.thumbnail,
        mimetype='image/jpeg')

if __name__ == '__main__':
    app.run(host='0.0.0.0', threaded=True, debug=True, use_reloader=False)
+29 −0
Original line number Diff line number Diff line
@@ -4,6 +4,10 @@ import os
import datetime
import copy
import logging
from PIL import Image

pil_formats = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF']
thumbnail_size = (60, 60)


class StreamObject(object):
@@ -19,6 +23,9 @@ class StreamObject(object):
        self.id = uuid.uuid4().hex
        logging.info("Created {}".format(self.id))

        # Store file format
        self.format = fmt

        # Create file name
        iterator = 0
        f_path, f_name = self.build_file_path(filename, folder, fmt)
@@ -55,6 +62,9 @@ class StreamObject(object):
        # Object lock
        self.locked = False

        # Thumbnail (populated only for JPEG captures)
        self.thumb_bytes = None

    def __enter__(self):
        """Create StreamObject in context, to auto-clean disk data."""
        logging.debug("Entering context for {}.\
@@ -190,6 +200,25 @@ class StreamObject(object):
        """Return a byte string of the capture data."""
        return self.data.getvalue()

    @property
    def thumbnail(self) -> io.BytesIO:
        # If no thumbnail exists, try and make one
        if not self.thumb_bytes:
            print("Building thumbnail")
            if self.format.upper() in pil_formats:
                im = Image.open(self.data)
                im.thumbnail(thumbnail_size)

                self.thumb_bytes = io.BytesIO()
                im.save(self.thumb_bytes, self.format)
                self.thumb_bytes.seek(0)
        else:
            self.thumb_bytes.seek(0)

        # Copy the buffer, to avoid closing the file
        data = io.BytesIO(self.thumb_bytes.getbuffer())
        return data

    def load_file(self) -> bool:
        """Load data stored on disk to the in-memory stream."""
        if self.file_exists:  # If data file exists