Video plate capture: calibration, edge enhance, auto-detect well borders
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
.well-btn {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-template-columns: repeat(var(--well-columns, 6), 1fr);
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
.well-btn {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-template-columns: repeat(var(--well-columns, 6), 1fr);
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
.video-drop-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 80px;
|
||||
margin-top: 8px;
|
||||
padding: 12px 20px;
|
||||
border: 2px dashed #aaa;
|
||||
border-radius: 6px;
|
||||
background: #f9f9f9;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.video-drop-zone:hover,
|
||||
.video-drop-zone.drag-over {
|
||||
border-color: #417690;
|
||||
background: #e8f3fb;
|
||||
}
|
||||
|
||||
.video-drop-zone.has-file {
|
||||
border-style: solid;
|
||||
border-color: #28a745;
|
||||
background: #f0fff4;
|
||||
}
|
||||
|
||||
.video-drop-hint {
|
||||
font-size: 0.9em;
|
||||
color: #555;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.video-drop-zone.has-file .video-drop-hint {
|
||||
color: #28a745;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---- Progress bar ---- */
|
||||
|
||||
.video-progress-wrap {
|
||||
display: none;
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.video-progress-bar-track {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: #ddd;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.video-progress-bar {
|
||||
height: 10px;
|
||||
width: 0%;
|
||||
background: #417690;
|
||||
border-radius: 5px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.video-progress-label {
|
||||
display: block;
|
||||
text-align: right;
|
||||
font-size: 0.8em;
|
||||
color: #417690;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -32,14 +32,16 @@ class ScannerManager {
|
||||
this.step = options.step;
|
||||
this.well = options.well;
|
||||
this.debug = options.debug;
|
||||
this.calib_debug = options.calib_debug;
|
||||
this.calib_center= options.calib_center;
|
||||
this.calib_debug = options.calib_debug;
|
||||
this.draw_debug = options.draw_debug;
|
||||
this.calib_center= options.calib_center;
|
||||
this.previous = options.previous;
|
||||
this.next = options.next;
|
||||
this.set_well = options.set_well;
|
||||
this.well_btn = options.well_btn;
|
||||
this.median = options.median;
|
||||
this.crop = options.crop;
|
||||
this.median = options.median;
|
||||
this.edge_enhance = options.edge_enhance;
|
||||
this.crop = options.crop;
|
||||
this.crop_radius = options.crop_radius;
|
||||
this.calib_auto = options.calib_auto;
|
||||
|
||||
@@ -53,7 +55,7 @@ class ScannerManager {
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
init_controls() {
|
||||
init_controls() {
|
||||
this.up.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "up" }); });
|
||||
this.down.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "down" }); });
|
||||
this.left.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "left" }); });
|
||||
@@ -62,33 +64,35 @@ class ScannerManager {
|
||||
this.goto_0.addEventListener('click', (e) => { this.clear_buttons(); this._send({ type: 'calibrate', topic: "goto_0" }); });
|
||||
this.goto_xy.addEventListener('click', (e) => { this.clear_buttons(); this._send({ type: 'calibrate', topic: "goto_xy" }); });
|
||||
this.xy_base.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "xy_base" }); });
|
||||
|
||||
this.calib_debug.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "calib_debug" }); });
|
||||
this.previous.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "previous" }); });
|
||||
this.next.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "next" }); });
|
||||
this.set_well.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "set_well" }); });
|
||||
|
||||
this.median.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "median" }); });
|
||||
this.crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop" }); });
|
||||
this.median.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "median" }); });
|
||||
this.edge_enhance.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "edge_enhance" }); });
|
||||
this.crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop" }); });
|
||||
this.crop_radius.addEventListener('change',(e) => { this._send({ type: 'calibrate', topic: "crop_radius", value: this.crop_radius.value }); });
|
||||
|
||||
this.well.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "position", value: e.target.value }); });
|
||||
this.step.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "step", value: e.target.value }); });
|
||||
this.feed.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "feed", value: e.target.value }); });
|
||||
this.duration.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "duration", value: e.target.value }); });
|
||||
|
||||
this.test.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "test" }); });
|
||||
this.calib_center.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "center" }); });
|
||||
this.calib_auto.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "auto" }); });
|
||||
|
||||
this.halt.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "halt" }); });
|
||||
|
||||
this.calib_debug.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "calib_debug" }); });
|
||||
this.draw_debug.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "draw_debug" }); });
|
||||
try {
|
||||
|
||||
this.previous.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "previous" }); });
|
||||
this.next.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "next" }); });
|
||||
this.calib_center.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "center" }); });
|
||||
this.calib_auto.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "auto" }); });
|
||||
this.min_area_px.addEventListener('change', (e) => { this._send({ type: 'calibrate', topic: "min_area_px", value: e.target.value }); });
|
||||
this.max_area_ratio.addEventListener('change', (e) => { this._send({ type: 'calibrate', topic: "max_area_ratio", value: e.target.value }); });
|
||||
this.max_planarians.addEventListener('change', (e) => { this._send({ type: 'calibrate', topic: "max_planarians", value: e.target.value}); });
|
||||
this.merge_kernel_size.addEventListener('change', (e) => { this._send({ type: 'calibrate', topic: "merge_kernel_size", value: e.target.value}); });
|
||||
this.min_contour_dist_px.addEventListener('change', (e) => { this._send({ type: 'calibrate', topic: "min_contour_dist_px", value: e.target.value }); });
|
||||
this.draw.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "draw", value: e.target.value }); });
|
||||
} catch(e) {}
|
||||
} catch(e) {console.log(e);}
|
||||
}
|
||||
|
||||
registerSocket(socket) {
|
||||
@@ -104,8 +108,12 @@ class ScannerManager {
|
||||
if (payload.state) {
|
||||
if (payload.state == 'debug') {
|
||||
const span = this.calib_debug.querySelector("span.debug"); span.style.color = payload.value ? '#f0f': '#fff';
|
||||
} else if (payload.state == 'draw_debug') {
|
||||
const span = this.draw_debug.querySelector("span.draw_debug"); span.style.color = payload.value ? '#0ff' : '#888';
|
||||
} else if (payload.state == 'median') {
|
||||
const span = this.median.querySelector("span.median"); span.style.color = payload.value ? '#f0f' : '#fff';
|
||||
} else if (payload.state == 'edge_enhance') {
|
||||
const span = this.edge_enhance.querySelector("span.edge_enhance"); span.style.color = payload.value ? '#0ff' : '#fff';
|
||||
} else if (payload.state == 'crop') {
|
||||
const span = this.crop.querySelector("span.crop"); span.style.color = payload.value ? '#f0f' : '#fff';
|
||||
}
|
||||
@@ -113,7 +121,10 @@ class ScannerManager {
|
||||
}
|
||||
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
|
||||
|
||||
if (payload.buttons) { this.well_btn.innerHTML = payload.buttons; }
|
||||
if (payload.buttons) {
|
||||
this.well_btn.innerHTML = payload.buttons;
|
||||
document.documentElement.style.setProperty('--well-columns', payload.columns);
|
||||
}
|
||||
if (payload.current >= 0) {
|
||||
document.querySelectorAll('button.w3-button.well').forEach(btn => {
|
||||
if (btn.value==payload.current) { btn.classList.add('w3-green'); return; }
|
||||
|
||||
@@ -62,6 +62,7 @@ class ScannerManager {
|
||||
if (payload.buttons) {
|
||||
this.well_btn.innerHTML = payload.buttons;
|
||||
const span = this.crop.querySelector("span.crop"); span.style.color = '#f0f';
|
||||
document.documentElement.style.setProperty('--well-columns', payload.columns);
|
||||
}
|
||||
if (payload.current >= 0) {
|
||||
document.querySelectorAll('button.w3-btn.well').forEach(btn => {
|
||||
@@ -72,7 +73,7 @@ class ScannerManager {
|
||||
} catch(e) { console.log(e); }
|
||||
}
|
||||
|
||||
init() { this.axes = 0; this.cropping = 1; this._send({ type: 'scanner', topic: "init", }); }
|
||||
init() { this.axes = 0; this.cropping = 1; this._send({ type: 'scanner', topic: "init", sid: this.session.value }); }
|
||||
scan() { this._send({ type: 'scanner', topic: "scan", session: this.session.value ? this.session.value: "0" }); }
|
||||
simulate() { this._send({ type: 'scanner', topic: "simulate", session: this.session.value ? this.session.value: "0" }); }
|
||||
halt() { this._send({ type: 'calibrate', topic: "halt" }); }
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function initDropZone(input) {
|
||||
var zone = document.createElement("div");
|
||||
zone.className = "video-drop-zone";
|
||||
|
||||
var hint = document.createElement("span");
|
||||
hint.className = "video-drop-hint";
|
||||
hint.textContent = "⬇ Glisser une vidéo ici ou cliquer pour parcourir";
|
||||
zone.appendChild(hint);
|
||||
|
||||
// Progress bar (hidden by default)
|
||||
var progressWrap = document.createElement("div");
|
||||
progressWrap.className = "video-progress-wrap";
|
||||
var progressTrack = document.createElement("div");
|
||||
progressTrack.className = "video-progress-bar-track";
|
||||
var progressBar = document.createElement("div");
|
||||
progressBar.className = "video-progress-bar";
|
||||
progressTrack.appendChild(progressBar);
|
||||
progressWrap.appendChild(progressTrack);
|
||||
var progressLabel = document.createElement("span");
|
||||
progressLabel.className = "video-progress-label";
|
||||
progressWrap.appendChild(progressLabel);
|
||||
zone.appendChild(progressWrap);
|
||||
|
||||
input.parentNode.insertBefore(zone, input.nextSibling);
|
||||
|
||||
function setFilename(name) {
|
||||
hint.textContent = "✓ " + name;
|
||||
zone.classList.add("has-file");
|
||||
}
|
||||
|
||||
function showProgress(pct) {
|
||||
progressWrap.style.display = "block";
|
||||
progressBar.style.width = pct + "%";
|
||||
progressLabel.textContent = Math.round(pct) + " %";
|
||||
}
|
||||
|
||||
function hideProgress() {
|
||||
progressWrap.style.display = "none";
|
||||
}
|
||||
|
||||
// Drag events
|
||||
zone.addEventListener("dragenter", function (e) { e.preventDefault(); zone.classList.add("drag-over"); });
|
||||
zone.addEventListener("dragover", function (e) { e.preventDefault(); zone.classList.add("drag-over"); });
|
||||
zone.addEventListener("dragleave", function () { zone.classList.remove("drag-over"); });
|
||||
zone.addEventListener("dragend", function () { zone.classList.remove("drag-over"); });
|
||||
|
||||
zone.addEventListener("drop", function (e) {
|
||||
e.preventDefault();
|
||||
zone.classList.remove("drag-over");
|
||||
var files = e.dataTransfer.files;
|
||||
if (!files.length) return;
|
||||
var file = files[0];
|
||||
try {
|
||||
var dt = new DataTransfer();
|
||||
dt.items.add(file);
|
||||
input.files = dt.files;
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
} catch (_) {}
|
||||
setFilename(file.name);
|
||||
});
|
||||
|
||||
// Click on zone → open file picker
|
||||
zone.addEventListener("click", function () { input.click(); });
|
||||
|
||||
// Sync when user picks via dialog
|
||||
input.addEventListener("change", function () {
|
||||
if (input.files && input.files.length > 0) {
|
||||
setFilename(input.files[0].name);
|
||||
}
|
||||
});
|
||||
|
||||
// Pre-fill if a file is already set (edit form)
|
||||
if (input.value) {
|
||||
var parts = input.value.split(/[\\/]/);
|
||||
setFilename(parts[parts.length - 1]);
|
||||
}
|
||||
|
||||
return { showProgress: showProgress, hideProgress: hideProgress, input: input };
|
||||
}
|
||||
|
||||
function interceptFormSubmit(form, dropZones) {
|
||||
form.addEventListener("submit", function (e) {
|
||||
// Only intercept if at least one file input has a file selected
|
||||
var hasFile = dropZones.some(function (dz) {
|
||||
return dz.input.files && dz.input.files.length > 0;
|
||||
});
|
||||
if (!hasFile) return; // normal submit, no big file
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
var data = new FormData(form);
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
// Show progress on the first drop zone that has a file
|
||||
var active = dropZones.find(function (dz) {
|
||||
return dz.input.files && dz.input.files.length > 0;
|
||||
});
|
||||
|
||||
if (active) active.showProgress(0);
|
||||
|
||||
xhr.upload.addEventListener("progress", function (ev) {
|
||||
if (ev.lengthComputable && active) {
|
||||
active.showProgress((ev.loaded / ev.total) * 100);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener("load", function () {
|
||||
if (active) active.hideProgress();
|
||||
// Django admin redirects after save — follow the final URL
|
||||
var finalUrl = xhr.responseURL || form.action;
|
||||
// The response itself is the resulting admin page; navigate to it
|
||||
window.location.href = finalUrl;
|
||||
});
|
||||
|
||||
xhr.addEventListener("error", function () {
|
||||
if (active) {
|
||||
active.hideProgress();
|
||||
active.showProgress(0); // reset bar
|
||||
}
|
||||
alert("Erreur lors de l'envoi du fichier.");
|
||||
});
|
||||
|
||||
xhr.open(form.method || "POST", form.action || window.location.href, true);
|
||||
xhr.send(data);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var inputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
||||
var dropZones = inputs.map(initDropZone);
|
||||
|
||||
// Hook into the closest form that contains these inputs
|
||||
var form = inputs.length && inputs[0].closest("form");
|
||||
if (form && dropZones.length) {
|
||||
interceptFormSubmit(form, dropZones);
|
||||
}
|
||||
});
|
||||
}());
|
||||
Reference in New Issue
Block a user