2025-01-16 03:30:32 +01:00
|
|
|
<!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%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#photo {
|
|
|
|
|
position:absolute;
|
|
|
|
|
z-index:5;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#canvas-temp{
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#tables {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#control-elements {
|
|
|
|
|
height:30em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
2025-02-25 18:11:42 +01:00
|
|
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
|
|
|
|
<div class="container-fluid">
|
|
|
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarTogglerDemo03" aria-controls="navbarTogglerDemo03" aria-expanded="false" aria-label="Toggle navigation">
|
|
|
|
|
<span class="navbar-toggler-icon"></span>
|
|
|
|
|
</button>
|
|
|
|
|
<a class="navbar-brand" href="#">Open Microscopy App</a>
|
|
|
|
|
<div class="collapse navbar-collapse" id="navbarTogglerDemo03">
|
|
|
|
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
|
|
|
|
<li class="nav-item dropdown">
|
|
|
|
|
<a class="nav-link dropdown-toggle" href="#" id="navbarScrollingDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
|
|
|
Settings
|
|
|
|
|
</a>
|
|
|
|
|
<ul class="dropdown-menu" aria-labelledby="navbarScrollingDropdown">
|
|
|
|
|
<li><a class="dropdown-item" href="/setup_microscope.html">Setup Camera</a></li>
|
|
|
|
|
<li><a class="dropdown-item" href="/sample_prep_protocol.html">Setup Preparation Protocol</a></li>
|
|
|
|
|
<li><hr class="dropdown-divider"></li>
|
|
|
|
|
<li><a class="dropdown-item" href="#">Defaults</a></li>
|
|
|
|
|
</ul>
|
|
|
|
|
</li>
|
|
|
|
|
<li class="nav-item">
|
|
|
|
|
<a class="nav-link active" aria-current="page" href="/sample_intake.html">add sample</a>
|
|
|
|
|
</li>
|
|
|
|
|
<li class="nav-item dropdown">
|
|
|
|
|
<a class="nav-link dropdown-toggle" href="#" id="navbarScrollingDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
|
|
|
Observe
|
|
|
|
|
</a>
|
|
|
|
|
<ul class="dropdown-menu" aria-labelledby="navbarScrollingDropdown">
|
|
|
|
|
<li><a class="dropdown-item" href="/observe_nema.html">Nematodes</a></li>
|
|
|
|
|
<li><a class="dropdown-item" href="/observe_main.html">Filamentous and Protozoa</a></li>
|
|
|
|
|
<li><a class="dropdown-item" href="/observe_bact.html">Bacteria</a></li>
|
|
|
|
|
</ul>
|
|
|
|
|
</li>
|
|
|
|
|
<li class="nav-item">
|
|
|
|
|
<a class="nav-link" href="/create_report.html">generate report</a>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</nav>
|
2025-01-16 03:30:32 +01:00
|
|
|
<!-- this canvas is not displayed and only used to generate the photo -->
|
|
|
|
|
<canvas id="canvas-temp"></canvas>
|
|
|
|
|
|
|
|
|
|
<div class="container">
|
2025-02-05 14:06:29 +01:00
|
|
|
<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>
|
2025-02-25 18:12:16 +01:00
|
|
|
|
2025-01-16 03:30:32 +01:00
|
|
|
<!-- 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>
|
|
|
|
|
|
2025-02-05 14:06:48 +01:00
|
|
|
<div id = "control-elements" class="col-3">
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="col">
|
|
|
|
|
<label class = "form-label" for="Bacilli_count">
|
|
|
|
|
Bacilli
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col">
|
|
|
|
|
<input class="form-control" type="number" id="Bacilli_count"
|
|
|
|
|
name="Bacilli_count" value=0>
|
|
|
|
|
</input>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="col">
|
|
|
|
|
<label class = "form-label" for="Cocci_count">
|
|
|
|
|
Cocci
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col">
|
|
|
|
|
<input class="form-control" type="number" id="Cocci_count"
|
|
|
|
|
name="Cocci_count" value=0>
|
|
|
|
|
</input>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="col">
|
|
|
|
|
<label class = "form-label" for="Pathogen_count">
|
|
|
|
|
Pathogen
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col">
|
|
|
|
|
<input class="form-control" type="number" id="Pathogen_count"
|
|
|
|
|
name="Pathogen_count" value=0>
|
|
|
|
|
</input>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="col">
|
|
|
|
|
<label class = "form-label" for="total_area">
|
|
|
|
|
Total area
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col">
|
|
|
|
|
<input class="form-control" type="number" id="total_area"
|
|
|
|
|
name="total_area" value=0>
|
|
|
|
|
</input>
|
|
|
|
|
|
|
|
|
|
</div>
|
2025-01-16 03:30:32 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-01-17 02:21:13 +01:00
|
|
|
<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="do_opencv_magic();">opencv</button>
|
|
|
|
|
</div>
|
2025-01-16 03:30:32 +01:00
|
|
|
</div>
|
2025-01-17 02:21:13 +01:00
|
|
|
|
2025-02-05 14:06:48 +01:00
|
|
|
<div id="organisms-table">
|
|
|
|
|
<h2> Bacteria Candidates </h2>
|
|
|
|
|
<table id= "org-table" style="width:100%">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>
|
|
|
|
|
Number
|
|
|
|
|
</th>
|
|
|
|
|
<th>
|
|
|
|
|
Image
|
|
|
|
|
</th>
|
|
|
|
|
<th>
|
|
|
|
|
Size [um²]
|
|
|
|
|
</th>
|
|
|
|
|
<th>
|
|
|
|
|
Choose a Frame
|
|
|
|
|
</th>
|
|
|
|
|
<th>
|
|
|
|
|
Choose bacterium Type
|
|
|
|
|
</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
<button class="btn btn-primary float-end" onclick="submit_FoV();">submit FoV</button>
|
2025-01-16 03:30:32 +01:00
|
|
|
</div>
|
2025-02-05 14:06:48 +01:00
|
|
|
<div id = "FoV_table_div">
|
|
|
|
|
<h2> Fields of View </h2>
|
|
|
|
|
<table id= "FoV_table" style="width:100%">
|
|
|
|
|
<thead>
|
|
|
|
|
<th>
|
|
|
|
|
# FoV
|
|
|
|
|
</th>
|
|
|
|
|
<th>
|
|
|
|
|
Bacilli
|
|
|
|
|
</th>
|
|
|
|
|
<th>
|
|
|
|
|
Cocci
|
|
|
|
|
</th>
|
|
|
|
|
<th>
|
|
|
|
|
Pathogens
|
|
|
|
|
</th>
|
|
|
|
|
<th>
|
|
|
|
|
total Volume
|
|
|
|
|
</th>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
|
|
|
|
</table>
|
|
|
|
|
<button class="btn btn-primary float-end" onclick="submit_result();">submit result</button>
|
2025-01-16 03:30:32 +01:00
|
|
|
|
2025-02-05 14:06:48 +01:00
|
|
|
</div>
|
2025-01-16 03:30:32 +01:00
|
|
|
</div>
|
2025-02-05 14:06:48 +01:00
|
|
|
</body>
|
2025-01-16 03:30:32 +01:00
|
|
|
|
|
|
|
|
<script>
|
2025-02-05 14:06:48 +01:00
|
|
|
var microscope_setups = {};
|
|
|
|
|
var microscope_setup;
|
|
|
|
|
var default_setup_id;
|
|
|
|
|
var prep_protocols = {};
|
|
|
|
|
var prep_protocol;
|
|
|
|
|
var default_protocol_id;
|
|
|
|
|
var sample;
|
|
|
|
|
var samples = {};
|
|
|
|
|
var FoVs = [];
|
|
|
|
|
var dilution_chooser = document.querySelector("#dilution");
|
|
|
|
|
|
|
|
|
|
var result = [];
|
|
|
|
|
|
2025-01-16 03:30:32 +01:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-05 14:06:29 +01:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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!
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-16 03:30:32 +01:00
|
|
|
|
|
|
|
|
var video = document.querySelector("#video");
|
|
|
|
|
|
|
|
|
|
var out = document.querySelector("#output");
|
2025-02-25 18:12:16 +01:00
|
|
|
|
2025-01-16 03:30:32 +01:00
|
|
|
var canvas_draw = document.getElementById("canvas-draw");
|
2025-02-05 14:06:29 +01:00
|
|
|
|
2025-01-16 03:30:32 +01:00
|
|
|
const context = canvas_draw.getContext('2d');
|
|
|
|
|
|
|
|
|
|
var contours;
|
|
|
|
|
var hierarchy;
|
|
|
|
|
var objects_tracked = [];
|
|
|
|
|
|
2025-01-17 02:21:13 +01:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-23 14:21:03 +01:00
|
|
|
var opencv_runing = false;
|
2025-01-17 02:21:13 +01:00
|
|
|
|
2025-01-16 03:30:32 +01:00
|
|
|
function do_opencv_magic() {
|
|
|
|
|
|
2025-01-23 14:21:03 +01:00
|
|
|
if (opencv_runing){
|
|
|
|
|
streaming = false;
|
|
|
|
|
opencv_runing = false;
|
|
|
|
|
|
|
|
|
|
startup();
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
opencv_runing = true;
|
|
|
|
|
streaming = true;
|
|
|
|
|
}
|
2025-01-16 03:30:32 +01:00
|
|
|
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);
|
2025-02-05 14:07:15 +01:00
|
|
|
let draw = new cv.Mat(video.offsetHeight, video.offsetWidth, cv.CV_8UC4);
|
|
|
|
|
|
|
|
|
|
|
2025-01-16 03:30:32 +01:00
|
|
|
var rects_old_frame = [];
|
|
|
|
|
var objects_last = [];
|
|
|
|
|
let contours = new cv.MatVector();
|
|
|
|
|
let hierarchy = new cv.Mat();
|
|
|
|
|
const FPS = 30;
|
|
|
|
|
var framecount = 0;
|
2025-01-17 02:21:13 +01:00
|
|
|
let objects_tracked_handled = new Set();
|
|
|
|
|
let objects_new_handled = new Set();
|
|
|
|
|
let rects_handled = new Set();
|
|
|
|
|
|
2025-01-16 03:30:32 +01:00
|
|
|
function processVideo() {
|
|
|
|
|
cap.read(src);
|
2025-02-05 14:07:15 +01:00
|
|
|
cap.read(draw);
|
2025-01-16 03:30:32 +01:00
|
|
|
framecount+=1;
|
|
|
|
|
let objects_this = [];
|
2025-01-17 02:21:13 +01:00
|
|
|
let fits = [];
|
|
|
|
|
let rects = [];
|
2025-02-05 14:07:15 +01:00
|
|
|
let areas = [];
|
|
|
|
|
let cropped_image = [];
|
|
|
|
|
|
2025-01-16 03:30:32 +01:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!streaming) {
|
|
|
|
|
// clean and stop.
|
|
|
|
|
src.delete();
|
|
|
|
|
dst.delete();
|
2025-02-05 14:07:15 +01:00
|
|
|
draw.delete();
|
2025-01-16 03:30:32 +01:00
|
|
|
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
|
2025-02-05 14:07:53 +01:00
|
|
|
let contoursColor = new cv.Scalar(0, 0, 255,255);
|
|
|
|
|
|
|
|
|
|
cv.drawContours(draw, contours, -1, contoursColor, 1 );
|
|
|
|
|
cv.drawContours(src, contours, -1, contoursColor, 1 );
|
|
|
|
|
|
2025-01-16 03:30:32 +01:00
|
|
|
for (let i = 0; i < contours.size(); ++i) {
|
2025-01-17 02:21:13 +01:00
|
|
|
//get contour
|
2025-01-16 03:30:32 +01:00
|
|
|
let cnt = contours.get(i);
|
|
|
|
|
// exclude contours with an area < 1000
|
|
|
|
|
if (cv.contourArea(cnt) < 1000){
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-02-05 14:07:15 +01:00
|
|
|
|
2025-01-16 03:30:32 +01:00
|
|
|
// get the bounding rect of the contour
|
|
|
|
|
let rect = cv.boundingRect(cnt);
|
2025-02-05 14:07:15 +01:00
|
|
|
//let rect = cv.minAreaRect(cnt)
|
|
|
|
|
|
|
|
|
|
let cropped_ref = src.roi(rect);
|
2025-01-17 02:21:13 +01:00
|
|
|
// prepare an array for fitnes values
|
|
|
|
|
rects.push(rect);
|
2025-02-05 14:07:15 +01:00
|
|
|
let sample = new cv.Mat(rect.height, rect.width, cv.CV_8UC4);
|
|
|
|
|
cropped_ref.copyTo(sample);
|
|
|
|
|
cropped_image.push(sample);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let area = cv.contourArea(cnt);
|
|
|
|
|
let s = src.size();
|
|
|
|
|
s = microscope_setup.FoV_Width / s.width;
|
|
|
|
|
areas.push(area*Math.pow(s,2));
|
|
|
|
|
|
2025-01-17 02:21:13 +01:00
|
|
|
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);
|
2025-02-05 14:07:15 +01:00
|
|
|
//box = cv.boxPoints(rect);
|
|
|
|
|
//box = np.int0(box);
|
|
|
|
|
cv.rectangle(draw, point1, point2, rectangleColor, 2, cv.LINE_AA, 0);
|
|
|
|
|
//cv.drawContours(draw,[box],0,rectangleColor,2);
|
|
|
|
|
//console.log(rect);
|
|
|
|
|
// get all fitness values for this rect. first for already tracked rects
|
2025-01-16 03:30:32 +01:00
|
|
|
for (let k = 0; k < objects_tracked.length; ++k){
|
2025-01-17 02:21:13 +01:00
|
|
|
if (objects_tracked[k].at(-1)[0] == framecount-1){
|
|
|
|
|
fits.push([k, 'NaN', rects.length-1, fit(objects_tracked[k].at(-1)[1], rect)]);
|
2025-01-16 03:30:32 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-02-05 14:07:15 +01:00
|
|
|
// get all fitness values for rects that have not been tracked.
|
2025-01-16 03:30:32 +01:00
|
|
|
for (let j = 0; j < objects_last.length; ++j){
|
2025-02-05 14:07:15 +01:00
|
|
|
fits.push(['NaN', j, rects.length-1, fit(objects_last[j][1], rect)]);
|
2025-01-17 02:21:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 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];
|
|
|
|
|
if (fit > 100000) {
|
|
|
|
|
break;
|
2025-01-16 03:30:32 +01:00
|
|
|
}
|
2025-01-17 02:21:13 +01:00
|
|
|
else if (ind_new == 'NaN' && !(objects_tracked_handled.has(ind_tracked)) && !(rects_handled.has(ind_rect))){
|
2025-02-05 14:07:15 +01:00
|
|
|
objects_tracked[ind_tracked].push([framecount, rects[ind_rect], cropped_image[ind_rect], areas[ind_rect]]);
|
2025-01-17 02:21:13 +01:00
|
|
|
objects_tracked_handled.add(ind_tracked);
|
|
|
|
|
rects_handled.add(ind_rect);
|
2025-01-16 03:30:32 +01:00
|
|
|
let rectangleColor = new cv.Scalar(255, 0, 255,255);
|
2025-01-17 02:21:13 +01:00
|
|
|
point1 = new cv.Point(rects[ind_rect].x, rects[ind_rect].y);
|
2025-02-05 14:07:15 +01:00
|
|
|
cv.putText(draw, String(ind_tracked), point1, cv.FONT_HERSHEY_COMPLEX, 1, rectangleColor, 1, cv.LINE_8);
|
2025-01-17 02:21:13 +01:00
|
|
|
}
|
2025-01-16 03:30:32 +01:00
|
|
|
|
2025-01-17 02:21:13 +01:00
|
|
|
}
|
|
|
|
|
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];
|
|
|
|
|
if (fit > 1000) {
|
|
|
|
|
break;
|
2025-01-16 03:30:32 +01:00
|
|
|
}
|
2025-01-17 02:21:13 +01:00
|
|
|
else if (ind_tracked == 'NaN' && !(objects_new_handled.has(ind_new)) && !(rects_handled.has(ind_rect))){
|
2025-02-05 14:07:15 +01:00
|
|
|
objects_tracked.push([objects_last[ind_new], [framecount, rects[ind_rect], cropped_image[ind_rect], areas[ind_rect]]]);
|
|
|
|
|
add_organism_candidate(objects_last[ind_new], objects_tracked.length);
|
2025-01-17 02:21:13 +01:00
|
|
|
objects_new_handled.add(ind_new);
|
|
|
|
|
rects_handled.add(ind_rect);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0; i < rects.length; ++i){
|
|
|
|
|
if (!(rects_handled.has(i))) {
|
2025-02-05 14:07:15 +01:00
|
|
|
objects_this.push([framecount, rects[i], cropped_image[i], areas[i]]);
|
2025-01-17 02:21:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-01-16 03:30:32 +01:00
|
|
|
|
|
|
|
|
objects_last = objects_this;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-02-05 14:07:39 +01:00
|
|
|
cv.imshow("canvas-draw", draw);
|
2025-01-16 03:30:32 +01:00
|
|
|
|
|
|
|
|
// schedule the next one.
|
|
|
|
|
let delay = 1000/FPS - (Date.now() - begin);
|
|
|
|
|
setTimeout(processVideo, delay);
|
2025-02-05 14:07:39 +01:00
|
|
|
//video.requestVideoFrameCallback(processVideo);
|
|
|
|
|
|
2025-01-16 03:30:32 +01:00
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log(cv.exceptionFromPtr(err));
|
|
|
|
|
contours.delete();
|
|
|
|
|
hierarchy.delete();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// schedule the first one.
|
2025-02-05 14:07:39 +01:00
|
|
|
setTimeout(processVideo, 0);
|
|
|
|
|
//video.requestVideoFrameCallback(processVideo);
|
2025-01-16 03:30:32 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let streaming = false;
|
|
|
|
|
function startup() {
|
|
|
|
|
navigator.mediaDevices
|
2025-02-05 14:07:39 +01:00
|
|
|
.getUserMedia(microscope_setup.video_prefs)
|
2025-01-16 03:30:32 +01:00
|
|
|
.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_draw.style.aspectRatio = video_aspect;
|
2025-01-17 03:54:59 +01:00
|
|
|
canvas_draw.height = canvas_draw.height*5;
|
|
|
|
|
canvas_draw.width = canvas_draw.height*video_aspect;
|
2025-01-16 03:30:32 +01:00
|
|
|
|
|
|
|
|
streaming = true;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
</html>
|