open-organism-observer/observe_bact.html

800 lines
24 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<link href="bootstrap-5.0.2-dist/css/bootstrap.min.css" rel="stylesheet" ></link>
<script src="bootstrap-5.0.2-dist/js/bootstrap.bundle.min.js" ></script>
<script type="text/javascript" src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<meta charset='utf-8'>
<style>
#output{
position: relative;
width: 100%;
}
#video_container{
position: relative;
}
#video {
position:absolute;
z-index: 9;
width: 100%;
}
#canvas-draw{
position:absolute;
z-index:10;
width: 100%;
}
#canvas-debug{
position:absolute;
z-index:10;
width: 20em;
opacity: 100%;
}
#photo {
position:absolute;
z-index:5;
width: 100%;
}
#canvas-temp{
display: none;
}
#tables {
width: 100%;
}
#control-elements {
height:30em;
}
</style>
</head>
<body>
<!-- this canvas is not displayed and only used to generate the photo -->
<canvas id="canvas-temp"></canvas>
<div class="container">
<div class="row">
<div class="col-3">
<label class = "form-label" for="camera_chooser">choose the camera setup</label>
<select class="form-select" name="camera_chooser" id = "camera_chooser"></select>
</div>
<div class="col-3">
<label class = "form-label" for="protocol_chooser">choose the preparation protocol</label>
<select class="form-select" name="protocol_chooser" id = "protocol_chooser"></select>
</div>
<div class="col-3">
<label class = "form-label" for="dilution">change dilution</label>
<input class="form-control" name="dilution" id = "dilution"></input>
</div>
<div class="col-3">
<label class = "form-label" for="sample_chooser">choose the sample</label>
<select class="form-select" name="sample_chooser" id = "sample_chooser"></select>
</div>
</div>
<!-- the output div is used to display the video, the photo and the canvas where you can draw in (and measure) your organisms. -->
<div id="output" class="row">
<div id = "video_container" class="col-9">
<img id="photo" alt="The screen capture will appear in this box." ></img>
<canvas id="canvas-draw"></canvas>
<video autoplay="true" id="video"></video>
</div>
<div id = "control-elements" class="col-3 ">
<div id="organisms-table">
<h2> bacteria-candidates </h2>
<table id= "org-table" style="width:100%">
<thead>
<tr>
<th>
bacteria
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div class="row">
<button class="btn btn-primary float-end" onclick="toggleFullscreen();" id="fullscreen-button">toggle Fullscreen video</button>
<button class="btn btn-primary float-end" onclick="toggle_cam();">Toggle camera</button>
<button class="btn btn-primary float-end" onclick="do_opencv_magic();">opencv</button>
</div>
</div>
</div>
</div>
<script>
function toggleFullscreen() {
let elem = document.querySelector("#output");
canvas_draw.style.width = video.offsetWidth
if (!document.fullscreenElement) {
elem.requestFullscreen().catch((err) => {
alert(
`Error attempting to enable fullscreen mode: ${err.message} (${err.name})`,
);
});
} else {
document.exitFullscreen();
}
}
var db;
let openRequest = indexedDB.open("my_db");
openRequest.onerror = function() {
console.error("Error", openRequest.error);
};
openRequest.onsuccess = function() {
db = openRequest.result;
get_default_camera_setup();
get_default_prep_setup();
get_all_samples();
// continue working with database using db object
};
function get_default_camera_setup(){
const request = db.transaction('defaults')
.objectStore('defaults')
.get(0);
request.onsuccess = ()=> {
default_setup_id = request.result.microscope_setup;
get_all_video_prefs();
// width of FoV -- will be supplied by FLASK microscope setup.
}
request.onerror = (err)=> {
console.error(`Error to get all setups: ${err}`)
}
}
function get_all_video_prefs(){
const request = db.transaction('microscope_setup')
.objectStore('microscope_setup')
.openCursor();
request.onsuccess = ()=> {
let cursor = event.target.result;
if (cursor) {
// Access the current record
make_camera_chooser(cursor.value, cursor.key);
// Move to the next record
cursor.continue();
}
else{
startup();
}
}
request.onerror = (err)=> {
console.error(`Error to get all setups: ${err}`)
}
}
var camera_chooser = document.querySelector("#camera_chooser");
function make_camera_chooser(setup, key){
let cameraID = document.createElement("option");
let text = document.createTextNode(setup.name);
cameraID.value = key;
microscope_setups[key] = setup
if (key == default_setup_id){
cameraID.defaultSelected = true;
text.textContent += " (default)"
microscope_setup = setup;
}
cameraID.appendChild(text);
camera_chooser.appendChild(cameraID);
}
camera_chooser.addEventListener("change", change_cam);
function change_cam(){
microscope_setup = microscope_setups[this.value];
startup();
};
function get_default_prep_setup(){
const request = db.transaction('defaults')
.objectStore('defaults')
.get(1);
request.onsuccess = ()=> {
default_protocol_id = request.result.prep_setup;
get_all_prep_prefs();
// width of FoV -- will be supplied by FLASK microscope setup.
}
request.onerror = (err)=> {
console.error(`Error to get all setups: ${err}`)
}
}
function get_all_prep_prefs(){
const request = db.transaction('prep_protocol')
.objectStore('prep_protocol')
.openCursor();
request.onsuccess = ()=> {
let cursor = event.target.result;
if (cursor) {
// Access the current record
make_protocol_chooser(cursor.value, cursor.key);
// Move to the next record
cursor.continue();
}
}
request.onerror = (err)=> {
console.error(`Error to get all setups: ${err}`)
}
}
var protocol_chooser = document.querySelector("#protocol_chooser");
function make_protocol_chooser(protocol, key){
let protocolID = document.createElement("option");
let text = document.createTextNode(protocol.name);
protocolID.value = key;
prep_protocols[key] = protocol;
if (key == default_protocol_id){
protocolID.defaultSelected = true;
prep_protocol = protocol;
text.textContent += " (default)";
dilution_chooser.value = protocol.Main_Dilution;
}
protocolID.appendChild(text);
protocol_chooser.appendChild(protocolID);
}
protocol_chooser.addEventListener("change", change_protocol);
function change_protocol(){
prep_protocol = prep_protocols[this.value];
dilution_chooser.value = prep_protocol.Main_Dilution;
};
function get_all_samples(){
const request = db.transaction('sample')
.objectStore('sample')
.openCursor();
request.onsuccess = ()=> {
let cursor = event.target.result;
if (cursor) {
// Access the current record
make_sample_chooser(cursor.value, cursor.key);
sample = cursor.key;
// Move to the next record
cursor.continue();
}
}
request.onerror = (err)=> {
console.error(`Error to get all setups: ${err}`)
}
}
var sample_chooser = document.querySelector("#sample_chooser");
function make_sample_chooser(sample, key){
let sampleID = document.createElement("option");
let text = document.createTextNode(sample.Name);
sampleID.value = key;
samples[key] = sample;
sampleID.appendChild(text);
sample_chooser.appendChild(sampleID);
}
sample_chooser.addEventListener("change", change_sample);
function change_sample(){
sample = samples[this.value];
};
var org_table = document.querySelector("#org-table");
function add_organism_candidate(tracked_obj, index){
let newRow = org_table.children[1].insertRow();
let number = newRow.insertCell(0);
let canvas = newRow.insertCell(1);
let area = newRow.insertCell(2);
let choose_frame = newRow.insertCell(3);
let bacteria_type = newRow.insertCell(4);
// number of candidate
number.innerHTML = index;
// canvas
let can_bact = document.createElement('canvas');
canvas.appendChild(can_bact);
console.log('hithere')
console.log(tracked_obj);
cv.imshow(can_bact, tracked_obj[2]);
can_bact.style.height = '100px';
can_bact.id = "bact_candidate_canvas" + index;
// display area of contour
area_text = document.createTextNode(tracked_obj[3]);
area.id = "bact_candidate_area" + index;
area.appendChild(area_text);
// frame chooser
let frame_chooser = document.createElement('input');
frame_chooser.setAttribute("type", "range");
frame_chooser.id = "bact_candidate" + index;
frame_chooser.addEventListener("change", change_frame);
frame_chooser.min = 0;
frame_chooser.value = 0;
let bact_type = document.createElement('form');
bact_type.id = "bact_candidate_type" + index;
bacteria_types = [
"Bacteria", "Cocci", "Vibrio", "Spirilla", "Spirochetes"
]
for (let i=0; i < bacteria_types.length; i++){
input = document.createElement('input');
input.type = 'radio';
input.classList.add('btn-check');
input.name = 'choice';
input.id = 'org-type-'+bacteria_types[i]+''+index;
input.value = bacteria_types[i];
input.addEventListener("click",change_type);
input
label = document.createElement('label');
switch (bacteria_types[i]){
case "Bacteria":
case "Cocci":
label.classList.add("btn", "btn-outline-success");
break;
case "Vibrio":
case "Spirilla":
case "Spirochetes":
label.classList.add("btn", "btn-outline-danger");
break;
}
label.htmlFor = 'org-type-'+bacteria_types[i]+''+index;
label.textContent = bacteria_types[i];
bact_type.appendChild(input);
bact_type.appendChild(label);
}
input = document.createElement('input');
input.type = 'radio';
input.classList.add('btn-check');
input.name = 'choice';
input.id = 'org-type-no'+''+index;
input.value = 'no';
input.checked = 'checked';
label = document.createElement('label');
label.classList.add("btn", "btn-outline-secondary");
label.htmlFor = 'org-type-no'+''+index;
label.textContent = 'no bacterium';
bact_type.appendChild(input);
bact_type.appendChild(label);
bacteria_type.appendChild(bact_type);
choose_frame.appendChild(frame_chooser);
for (let i=0; i<objects_tracked.length; i++){
let chooser = document.getElementById("bact_candidate"+(i+1))
chooser.max = objects_tracked[i].length-1;
}
}
let bacteria = new Map();
function change_type(event){
console.log(event.target)
id = Number(event.target.id.replace(/\D/g, ""));
let c = document.getElementById("bact_candidate"+id);
let v = c.value;
bacteria.set(id, {
'type' : event.target.value,
'picture' : objects_tracked[id-1][v][2],
'area' : objects_tracked[id-1][v][3]
});
update_bact_count();
}
function change_frame(event){
let id = Number(event.target.id.replace(/\D/g, ""));
let canvas = document.getElementById("bact_candidate_canvas"+id);
let area = document.getElementById("bact_candidate_area"+id);
let type = document.getElementById("bact_candidate_type"+id);
console.log(type.choice.value);
area.textContent = objects_tracked[id-1][Number(this.value)][3];
cv.imshow(canvas, objects_tracked[id-1][Number(this.value)][2]);
bacteria.set(id, {
'type' : type.choice.value,
'picture' : objects_tracked[id-1][Number(this.value)][2],
'area' : objects_tracked[id-1][Number(this.value)][3]
});
update_bact_count();
}
let bacilli_count = document.getElementById("Bacilli_count");
let cocci_count = document.getElementById("Cocci_count");
let pathogen_count = document.getElementById("Pathogen_count");
let total_area = document.getElementById("total_area");
function update_bact_count(){
let bc = 0;
let cc = 0;
let pc = 0;
let area = 0;
bacteria.forEach((value, key, map) => {
switch(value.type){
case "Bacteria":
bc += 1
area += value.area;
break;
case "Cocci":
cc += 1
area += value.area;
break;
case "Vibrio":
case "Spirilla":
case "Spirochetes":
pc += 1
area += value.area;
break;
}
}
);
bacilli_count.value = bc;
cocci_count.value = cc;
pathogen_count.value = pc;
total_area.value = area;
}
function submit_FoV(){
bact = []
bacteria.forEach((value, key, map) => {
bact.push([key, value.type, value.area, value.picture])
})
console.log(bacteria);
FoVs.push([bact, [bacilli_count.value, bacilli_count.value*4, cocci_count.value, cocci_count.value*2, pathogen_count.value, pathogen_count.value*2, total_area.value]]);
bacteria.clear();
objects_tracked = [];
while (org_table.children[1].hasChildNodes()) {
org_table.children[1].removeChild(org_table.children[1].lastChild);
}
update_bact_count();
update_FoV_table();
}
var FoV_table = document.getElementById("FoV_table");
function update_FoV_table(){
while (FoV_table.children[1].hasChildNodes()) {
FoV_table.children[1].removeChild(FoV_table.children[1].lastChild);
}
FoVs.forEach(function (value, index){
let newRow = FoV_table.children[1].insertRow();
let number = newRow.insertCell(0);
let bacilli = newRow.insertCell(1);
let cocci = newRow.insertCell(2);
let pathogens = newRow.insertCell(3);
let total_area = newRow.insertCell(4);
let button = newRow.insertCell(5);
number.innerHTML = index+1;
bacilli.innerHTML = value[1][0];
cocci.innerHTML = value[1][2];
pathogens.innerHTML = value[1][4];
total_area.innerHTML = value[1][6];
button.innerHTML = "<button class='btn btn-primary float-end' onclick=remove_FoV(" + index + ")>remove</button>";
});
}
function remove_FoV(index){
FoVs.splice(index, 1);
update_FoV_table();
}
function submit_result(){
let to_send = {
"result" : FoVs,
"sampleID" : sample_chooser.value,
"setupID" : camera_chooser.value,
"prepID" : protocol_chooser.value,
"dilution" : dilution_chooser.value,
"datetime" : new Date,
}
let transaction = db.transaction(["bacterial_scan"], "readwrite");
let objectStore = transaction.objectStore("bacterial_scan");
let add_request = objectStore.add(to_send); // (3)
add_request.onsuccess = (event) => {
console.log("All done!");
};
add_request.onerror = (event) => {
console.log("something went wrong");
// Don't forget to handle errors!
};
}
var video = document.querySelector("#video");
var out = document.querySelector("#output");
// opencv stuff
var canvas_draw = document.getElementById("canvas-draw");
const context = canvas_draw.getContext('2d');
var contours;
var hierarchy;
var objects_tracked = [];
// define a fitness function --> it is the distance from one centeroid to the another
function fit(last_rect, rect){
return (last_rect.x-rect.x)**2 + (last_rect.y-rect.y)**2;
}
var opencv_runing = false;
function do_opencv_magic() {
if (opencv_runing){
streaming = false;
opencv_runing = false;
startup();
}
else {
opencv_runing = true;
streaming = true;
}
var cap = new cv.VideoCapture(video);
let src = new cv.Mat(video.offsetHeight, video.offsetWidth, cv.CV_8UC4);
let dst = new cv.Mat(video.offsetHeight, video.offsetWidth, cv.CV_8UC1);
var rects_old_frame = [];
var objects_last = [];
let contours = new cv.MatVector();
let hierarchy = new cv.Mat();
const FPS = 30;
var framecount = 0;
let objects_tracked_handled = new Set();
let objects_new_handled = new Set();
let rects_handled = new Set();
function processVideo() {
cap.read(src);
framecount+=1;
let objects_this = [];
let fits = [];
let rects = [];
try {
if (!streaming) {
// clean and stop.
src.delete();
dst.delete();
return;
}
let begin = Date.now();
// start processing.
cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY, 0);
cv.threshold(dst, dst, 120, 200, cv.THRESH_BINARY);
// Find contours
cv.findContours(dst, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
// iterate over contours
for (let i = 0; i < contours.size(); ++i) {
//get contour
let cnt = contours.get(i);
// exclude contours with an area < 1000
if (cv.contourArea(cnt) < 1000){
continue;
}
// get the bounding rect of the contour
let rect = cv.boundingRect(cnt);
// prepare an array for fitnes values
rects.push(rect);
let rectangleColor = new cv.Scalar(255, 0, 255,255);
let point1 = new cv.Point(rect.x, rect.y);
let point2 = new cv.Point(rect.x + rect.width, rect.y + rect.height);
cv.rectangle(src, point1, point2, rectangleColor, 2, cv.LINE_AA, 0);
// get all fitness values for this rect.
for (let k = 0; k < objects_tracked.length; ++k){
if (objects_tracked[k].at(-1)[0] == framecount-1){
fits.push([k, 'NaN', rects.length-1, fit(objects_tracked[k].at(-1)[1], rect)]);
}
}
for (let j = 0; j < objects_last.length; ++j){
fits.push(['NaN', j, rects.length-1, fit(objects_last[j], rect)]);
}
}
// sort fits array
// Sort the Array
fits.sort(function(a, b){return a[3]-b[3]});
// iterate over fits to associate objects.
objects_tracked_handled.clear();
objects_new_handled.clear();
rects_handled.clear();
for (i = 0; i < fits.length; ++i) {
let ind_tracked = fits[i][0];
let ind_new = fits[i][1];
let ind_rect = fits[i][2];
let fit = fits[i][3];
console.log(fit);
if (fit > 100000) {
break;
}
else if (ind_new == 'NaN' && !(objects_tracked_handled.has(ind_tracked)) && !(rects_handled.has(ind_rect))){
objects_tracked[ind_tracked].push([framecount, rects[ind_rect]]);
objects_tracked_handled.add(ind_tracked);
rects_handled.add(ind_rect);
let rectangleColor = new cv.Scalar(255, 0, 255,255);
point1 = new cv.Point(rects[ind_rect].x, rects[ind_rect].y);
cv.putText(src, String(ind_tracked+objects_lost.length), point1, cv.FONT_HERSHEY_COMPLEX, 1, rectangleColor, 1, cv.LINE_8);
}
}
for (i = 0; i < fits.length; ++i) {
let ind_tracked = fits[i][0];
let ind_new = fits[i][1];
let ind_rect = fits[i][2];
let fit = fits[i][3];
console.log(fit);
if (fit > 1000) {
break;
}
else if (ind_tracked == 'NaN' && !(objects_new_handled.has(ind_new)) && !(rects_handled.has(ind_rect))){
objects_tracked.push([[framecount-1, objects_last[ind_new]], [framecount, rects[ind_rect]]]);
objects_new_handled.add(ind_new);
rects_handled.add(ind_rect);
}
}
for (let i = 0; i < rects.length; ++i){
if (!(rects_handled.has(i))) {
objects_this.push(rects[i]);
}
}
//cv.drawContours(dst, contours, i, color, 1, cv.LINE_8, hierarchy, 100);
console.log([objects_new_handled.size, objects_tracked_handled.size]);
objects_last = objects_this;
let contoursColor = new cv.Scalar(0, 0, 255,255);
cv.drawContours(src, contours, -1, contoursColor, 1 );
cv.imshow("canvas-draw", src);
// schedule the next one.
let delay = 1000/FPS - (Date.now() - begin);
setTimeout(processVideo, delay);
} catch (err) {
console.log(cv.exceptionFromPtr(err));
contours.delete();
hierarchy.delete();
cap.delete();
}
};
// schedule the first one.
setTimeout(processVideo, 0);
};
// init video switching get IDs of video inputs
var deviceIDs = [];
navigator.mediaDevices
.enumerateDevices()
.then((devices) => {
devices.forEach((device) => {
if (device.kind=="videoinput"){
deviceIDs.push(device.deviceId);
}
console.log(`${device.kind}: ${device.label} id = ${device.deviceId}`);
});
})
.catch((err) => {
console.error(`${err.name}: ${err.message}`);
});
// toggle between cameras
var device_index = 0;
function toggle_cam(){
device_index += 1
if (device_index>=deviceIDs.length){
device_index=0
};
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({ video: {
deviceId: deviceIDs[device_index],
}, })
.then(function (stream) {
video.srcObject = stream;
})
.catch(function (error) {
console.log("Something went wrong!");
});
}
};
let streaming = false;
function startup() {
output = document.getElementById("output");
video = document.getElementById("video");
canvas = document.getElementById("canvas-temp");
canvas_draw = document.getElementById("canvas-draw");
photo = document.getElementById("photo");
startButton = document.getElementById("start-button");
navigator.mediaDevices
.getUserMedia({ video: {width: { ideal: 99999} , height: { ideal: 99999 }}, audio: false })
.then((stream) => {
video.srcObject = stream;
video.play();
})
.catch((err) => {
console.error(`An error occurred: ${err}`);
});
video.addEventListener(
"canplay",
(ev) => {
if (!streaming) {
video_aspect = video.videoWidth / video.videoHeight;
video.style.aspectRatio = video_aspect;
video.height = video.offsetHeight;
video.width = video.offsetWidth;
photo.style.aspectRatio = video_aspect;
canvas.style.aspectRatio = video_aspect;
canvas_draw.style.aspectRatio = video_aspect;
canvas_draw.height = canvas_draw.height*5;
canvas_draw.width = canvas_draw.height*video_aspect;
streaming = true;
}
},
false,
);
}
window.addEventListener("load", startup, false);
</script>
</body>
</html>