376 lines
11 KiB
HTML
376 lines
11 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">
|
|
<!-- 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>
|
|
<canvas id="canvas-debug"></canvas>
|
|
|
|
</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 video = document.querySelector("#video");
|
|
|
|
var out = document.querySelector("#output");
|
|
// width of FoV -- will be supplied by FLASK microscope setup.
|
|
var real_width_FoV = 255;
|
|
// opencv stuff
|
|
var canvas_draw = document.getElementById("canvas-draw");
|
|
const context = canvas_draw.getContext('2d');
|
|
|
|
var contours;
|
|
var hierarchy;
|
|
var objects_tracked = [];
|
|
var objects_lost = [];
|
|
|
|
|
|
// 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;
|
|
}
|
|
|
|
|
|
|
|
function do_opencv_magic() {
|
|
|
|
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 = [];
|
|
|
|
//for (let k = 0; k < objects_tracked.length; ++k){
|
|
// if (objects_tracked[k].at(-1)[0] < framecount-1){
|
|
// objects_lost.push(objects_tracked[k]);
|
|
// objects_tracked.splice(k, 1);
|
|
// }
|
|
//}
|
|
//console.log(objects_last);
|
|
|
|
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>
|