tracking
This commit is contained in:
@@ -28,6 +28,7 @@ class DefaultConfig:
|
||||
calibration_default_step: float = 1.0
|
||||
calibration_default_duration: float = 3.0
|
||||
tracking: bool = False
|
||||
tube_axis: str = 'vertical'
|
||||
min_area_px: int = 20
|
||||
max_area_ratio: float = 0.10
|
||||
max_planarians: int = 1
|
||||
|
||||
@@ -37,6 +37,11 @@ CAPTURE_TYPE = [
|
||||
('webcam', _("Webcam")),
|
||||
('file', _("mp4")),
|
||||
]
|
||||
|
||||
TUBE_AXIS_TYPE = [
|
||||
('vertical', _("Vertical")),
|
||||
('horizontal', _("Horizontal")),
|
||||
]
|
||||
|
||||
class Configuration(models.Model):
|
||||
name = models.CharField(_("Nom de la Configuration"), help_text=_("Nom de la configuration"), max_length=100, null=True, blank=False, default=_("Configuration par défaut"))
|
||||
@@ -66,14 +71,15 @@ class Configuration(models.Model):
|
||||
calibration_default_duration = models.FloatField(_("Duruée calibration"), help_text=_("Durée de pose entre chaque puits en s"), default=3.0)
|
||||
# tracking
|
||||
tracking = models.BooleanField(_("Suivi"), help_text=_("Suivi et analyse des planaires"), default=False)
|
||||
tube_axis = models.CharField(_("Axe du puit"), help_text=_("Axe du tube"), default='vertical', max_length=16, choices=TUBE_AXIS_TYPE, null=True, blank=False)
|
||||
min_area_px = models.PositiveIntegerField(_("Surface minimale"), help_text=_("surface minimale d'un contour pour être considéré valide (px²)"), default=20)
|
||||
max_area_ratio = models.FloatField(_("surface maximale "), help_text=_("surface maximale d'un contour en fraction de la frame (défaut 10%)"), default=0.10)
|
||||
max_planarians = models.PositiveIntegerField(_("Max planaire"), help_text=_("nombre maximum de planaires à suivre simultanément (1-10)"), default=1)
|
||||
merge_kernel_size = models.PositiveIntegerField(_("Taille du kernel"), help_text=_("taille du kernel elliptique de fusion des fragments (px)"), default=15)
|
||||
min_contour_dist_px = models.PositiveIntegerField(_("Distance <contour>"), help_text=_("Distance min entre deux contours pour les considérer comme individus distincts. Défaut : 40px."), default=40)
|
||||
merge_kernel_size = models.PositiveIntegerField(_("Taille du kernel"), help_text=_("taille du kernel elliptique de fusion des fragments (px). Augmenter si fragments résiduels"), default=15)
|
||||
min_contour_dist_px = models.PositiveIntegerField(_("Distance <contour>"), help_text=_("Distance min entre deux contours pour les considérer comme individus distincts. Défaut : 40px. Augmenter si IDs multiples persistent"), default=40)
|
||||
#
|
||||
active = models.BooleanField(_("Actif"), default=False)
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def active_config(cls):
|
||||
@@ -115,7 +121,7 @@ class MultiWell(models.Model):
|
||||
row_order = models.CharField(_("Ordre ligne"), help_text=_('Ordre ligne de puit. Lecture en serpentin dans le sens des +- X'), max_length=16, null=True, blank=False, default="D,C,B,A")
|
||||
# Balayage
|
||||
order = models.PositiveSmallIntegerField(_("Ordre"), help_text=_('Ordre de lecture du multi-puit'), blank=False, default=0)
|
||||
duration = models.PositiveIntegerField(_("Durée"), help_text=_('Durée du film en secondes'), blank=False, default=120)
|
||||
duration = models.PositiveIntegerField(_("Durée"), help_text=_('Durée du film en secondes por la calibration'), blank=False, default=10)
|
||||
xbase = models.FloatField(_("Origine X"), help_text=_('Base origine X en mm'), blank=False, default=50.0)
|
||||
ybase = models.FloatField(_("Origine Y"), help_text=_('Base origine Y en mm'), blank=False, default=50.0)
|
||||
|
||||
@@ -231,6 +237,7 @@ class Experiment(models.Model):
|
||||
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
||||
multiwell = models.ForeignKey(MultiWell, verbose_name=_("Multi-puits"), on_delete=models.SET_NULL, null=True, blank=True)
|
||||
duration = models.PositiveIntegerField(_("Durée"), help_text=_('Durée de la prise de vue en secondes'), blank=False, default=120)
|
||||
created = models.DateTimeField(_("Date de création"), default=timezone.now)
|
||||
started = models.DateTimeField (_("Date de début"), null=True, blank=True)
|
||||
finished = models.DateTimeField (_("Date de fin"), null=True, blank=True)
|
||||
|
||||
@@ -149,66 +149,83 @@ class MultiWellManager:
|
||||
|
||||
#def _grid_scanning_capture(self, uuid, duration):
|
||||
def _grid_scanning_capture(self, experiment, well_position, simulate=False):
|
||||
well = well_position.well
|
||||
multiwell = experiment.multiwell
|
||||
if self.process.use_tracking:
|
||||
logger.info("# Paramètres d'une expérience PlanarianScanner")
|
||||
try:
|
||||
well = well_position.well
|
||||
multiwell = experiment.multiwell
|
||||
if self.process.use_tracking:
|
||||
cfg = ExperimentConfig.objects.filter(experiment_id=experiment.id, well_id=well.id).first()
|
||||
if not cfg:
|
||||
raise Exception(f"Configuration d'expérience introuvable pour {experiment} / {well}")
|
||||
# reset PlanarianTracker => on_well_change
|
||||
self.process.cam.on_well_change(cfg, draw_contours=False)
|
||||
|
||||
cfg = ExperimentConfig.objects.get(experiment_id=experiment.id, well_id=well.id)
|
||||
# reset PlanarianTracker => on_well_change
|
||||
self.process.cam.on_well_change(cfg)
|
||||
|
||||
uuid = f'{self.process.data.session}-{multiwell.position}-{well.name}'
|
||||
## start recording
|
||||
self.process.data.uuid = uuid
|
||||
if not simulate:
|
||||
self.process.data.record = True
|
||||
|
||||
start = time.monotonic()
|
||||
while not self.stop_playing.is_set():
|
||||
if time.monotonic() - start > multiwell.duration:
|
||||
break
|
||||
self.cnc_controller.wait_for(0.1)
|
||||
uuid = f'{self.process.data.session}-{multiwell.position}-{well.name}'
|
||||
## start recording
|
||||
self.process.data.uuid = uuid
|
||||
if not simulate:
|
||||
self.process.data.record = True
|
||||
|
||||
self.process.data.record = False
|
||||
self.process.data.uuid = None
|
||||
|
||||
msg = f"{uuid}: capture done"
|
||||
start = time.monotonic()
|
||||
while not self.stop_playing.is_set():
|
||||
if time.monotonic() - start > multiwell.duration:
|
||||
break
|
||||
self.cnc_controller.wait_for(0.1)
|
||||
|
||||
self.process.data.record = False
|
||||
self.process.data.uuid = None
|
||||
|
||||
msg = f"{uuid}: capture done..."
|
||||
except Exception as e:
|
||||
msg = f"error during capture - {e}"
|
||||
logger.error(msg)
|
||||
|
||||
logger.info(msg)
|
||||
self.process._send(scan_state=msg)
|
||||
|
||||
self.process._send(scan_state=msg)
|
||||
|
||||
|
||||
def _capture_file_simulation(self, name):
|
||||
vf = settings.MEDIA_ROOT / 'simulation' / f'{name}.mp4'
|
||||
if vf.exists():
|
||||
self.process.cam._video_file = str(vf)
|
||||
self.process.cam._error_occured = True
|
||||
logger.info(f"Simulating capture with file {vf}")
|
||||
|
||||
|
||||
def _grid_scanning(self, experiment, xnext=0, ynext=0, simulate=False):
|
||||
multiwell = experiment.multiwell
|
||||
wells = models.WellPosition.objects.filter(multiwell_id=multiwell.id).order_by('order').all()
|
||||
cam = self.process.cam
|
||||
cam._aligner.set_tube_diameter(multiwell.diameter)
|
||||
try:
|
||||
multiwell = experiment.multiwell
|
||||
wells = models.WellPosition.objects.filter(multiwell_id=multiwell.id).order_by('order').all()
|
||||
cam = self.process.cam
|
||||
cam._aligner.set_tube_diameter(multiwell.diameter)
|
||||
|
||||
for wl in wells:
|
||||
if self.stop_playing.is_set():
|
||||
break
|
||||
self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed)
|
||||
|
||||
## change file
|
||||
if self.process.conf.capture_type == 'file':
|
||||
self._capture_file_simulation(wl.well.name)
|
||||
|
||||
self._grid_scanning_capture(experiment, wl, simulate=simulate)
|
||||
msg =f"Scan terminé — retour à l'origine (X={xnext:.1f} Y={ynext:.1f})"
|
||||
logger.info(msg)
|
||||
self.process._send(state='scan_finished', msg=msg)
|
||||
|
||||
except Exception as e:
|
||||
msg = f"Error during grid scanning - {e}"
|
||||
logger.error(msg)
|
||||
self.process._send(state='error', msg=msg)
|
||||
|
||||
self.stop_playing = Event()
|
||||
for wl in wells:
|
||||
if self.stop_playing.is_set():
|
||||
break
|
||||
self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed)
|
||||
|
||||
## change file
|
||||
if self.process.conf.capture_type == 'file':
|
||||
vf = settings.MEDIA_ROOT / 'simulation' / f'{wl.well.name}.mp4'
|
||||
if vf.exists():
|
||||
self.process.cam._video_file = str(vf)
|
||||
self.process.cam._error_occured = True
|
||||
|
||||
logger.info(f"Simulating capture with file {vf}")
|
||||
|
||||
self._grid_scanning_capture(experiment, wl, simulate=simulate)
|
||||
|
||||
|
||||
logger.info(f"Scan terminé — retour à l'origine (X={xnext:.1f} Y={ynext:.1f})")
|
||||
self.cnc_controller.move_to(xnext, ynext, feed=multiwell.feed*2)
|
||||
finally:
|
||||
self.cnc_controller.move_to(xnext, ynext, feed=multiwell.feed*2)
|
||||
|
||||
|
||||
def _start_scanning(self, session, experiments, simulate=False):
|
||||
try:
|
||||
self.process.cam._aligner.debug = False
|
||||
self.stop_playing.clear()
|
||||
|
||||
xynext = []
|
||||
for obs in experiments:
|
||||
xynext.append((obs.multiwell.xbase, obs.multiwell.ybase))
|
||||
@@ -240,6 +257,8 @@ class MultiWellManager:
|
||||
msg = f"Session {session.name} terminée à {session.finished} après {session.finished - started} secondes."
|
||||
logger.info(msg)
|
||||
self.process._send(scan_state=msg)
|
||||
except Exception as e:
|
||||
logger.error("Error during scanning process", e)
|
||||
finally:
|
||||
self.scan_thread = None
|
||||
|
||||
@@ -265,20 +284,28 @@ class MultiWellManager:
|
||||
|
||||
def previous_well(self):
|
||||
wl = self.well_iterator.previous()
|
||||
if self.process.conf.capture_type == 'file':
|
||||
self._capture_file_simulation(wl.well.name)
|
||||
self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed)
|
||||
return {"state": "previous", "msg": f">>> ({wl.x}, {wl.y})"}
|
||||
return {"state": "previous", "msg": f">>> {wl.well.name}: ({wl.x}, {wl.y})"}
|
||||
|
||||
|
||||
def next_well(self):
|
||||
wl = self.well_iterator.next()
|
||||
if self.process.conf.capture_type == 'file':
|
||||
self._capture_file_simulation(wl.well.name)
|
||||
|
||||
self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed)
|
||||
return {"state": "next", "msg": f">>> ({wl.x}, {wl.y})"}
|
||||
return {"state": "next", "msg": f">>> {wl.well.name}: ({wl.x}, {wl.y})"}
|
||||
|
||||
|
||||
def goto_well(self, numwell):
|
||||
wl = self.well_iterator.seek(numwell)
|
||||
if self.process.conf.capture_type == 'file':
|
||||
self._capture_file_simulation(wl.well.name)
|
||||
|
||||
self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed)
|
||||
return {"state": "goto", "msg": f">>> ({wl.x}, {wl.y})"}
|
||||
return {"state": "goto", "msg": f">>> {wl.well.name}: ({wl.x}, {wl.y})"}
|
||||
|
||||
|
||||
def set_well_position(self):
|
||||
@@ -293,7 +320,7 @@ class MultiWellManager:
|
||||
|
||||
|
||||
def _scanning_test(self, auto=False):
|
||||
self.stop_playing = Event()
|
||||
self.stop_playing.clear()
|
||||
cam = self.process.cam
|
||||
cam._aligner.set_tube_diameter(self.multiwell.diameter)
|
||||
duration = self.duration if not auto else settings.CALIBRATION_AUTO_DURATION
|
||||
@@ -356,7 +383,6 @@ class MultiWellManager:
|
||||
self.test_thread = Thread(target=self._scanning_test, args=(auto, ), daemon=True)
|
||||
self.test_thread.start()
|
||||
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return self._position
|
||||
@@ -421,14 +447,12 @@ class MultiWellManager:
|
||||
def dy(self, value):
|
||||
self._dy = value
|
||||
|
||||
|
||||
def get_well_order(self):
|
||||
wl = self.well_iterator.get_current()
|
||||
if wl:
|
||||
return wl.order
|
||||
return None
|
||||
|
||||
|
||||
def set_position(self):
|
||||
x, y = self.cnc_controller.get_mpos()
|
||||
self.cnc_controller.wait_for(2.0)
|
||||
@@ -438,7 +462,6 @@ class MultiWellManager:
|
||||
wl.x, wl.y = x, y
|
||||
wl.px_per_mm = self.px_per_mm
|
||||
wl.save()
|
||||
|
||||
|
||||
def calib_toggle_debug(self):
|
||||
""" Active / désactive le mode debug sur le stream."""
|
||||
|
||||
@@ -21,12 +21,19 @@ from celery.exceptions import Ignore
|
||||
from celery.utils.log import get_task_logger
|
||||
from redis import Redis
|
||||
from dataclasses import dataclass
|
||||
from modules import reductstore, grbl, utils, planarian_metrics
|
||||
from modules import reductstore, utils, planarian_metrics
|
||||
|
||||
## camera devices
|
||||
from modules.circular_crop import CircularCrop, CropStrategy
|
||||
from .multiwell import MultiWellManager
|
||||
from .constants import ScannerConstants
|
||||
|
||||
# CNC
|
||||
if not settings.GRBL_SIMULATION:
|
||||
from modules.grbl import GRBLController # @UnusedImport
|
||||
else:
|
||||
from modules.grbl_simulator import GRBLController # @Reimport
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
@@ -223,7 +230,7 @@ class ScannerProcess(Task):
|
||||
self.cam._active_median = False
|
||||
self.cam.set_circular_crop(None)
|
||||
|
||||
self.grbl = grbl.GRBLController(
|
||||
self.grbl = GRBLController(
|
||||
send_callback=self._display,
|
||||
x_max=self.conf.grbl_xmax,
|
||||
y_max=self.conf.grbl_ymax
|
||||
@@ -328,6 +335,8 @@ class ScannerProcess(Task):
|
||||
self._send(state='serial', msg=f"Connected {self.grbl.port}")
|
||||
|
||||
self.grbl.go_origin(feed=feed)
|
||||
if self.conf.capture_type == 'file':
|
||||
self.manager._capture_file_simulation('zero')
|
||||
self.grbl.wait_for(2.0)
|
||||
|
||||
|
||||
@@ -363,6 +372,7 @@ class ScannerProcess(Task):
|
||||
self.cam.set_circular_crop(self.crop)
|
||||
self.cam._active_median = False
|
||||
self.grbl.go_origin(feed=self.manager.feed)
|
||||
self.cam.set_draw_contours(False)
|
||||
|
||||
elif topic == 'scan' or topic == 'simulate':
|
||||
logger.info(f"==== Scan {cmd}")
|
||||
@@ -371,7 +381,6 @@ class ScannerProcess(Task):
|
||||
if sid == "0":
|
||||
self._send(state='error', msg=str(_('La session est nulle!...')))
|
||||
else:
|
||||
|
||||
try:
|
||||
self.cam._active_median = False
|
||||
simulate = (topic=='simulate')
|
||||
@@ -397,7 +406,8 @@ class ScannerProcess(Task):
|
||||
position = cmd.get("position")
|
||||
self.manager.set_multiwell(position)
|
||||
self.cam.set_circular_crop(None)
|
||||
self.cam._active_median = False
|
||||
self.cam._active_median = False
|
||||
self.cam.set_draw_contours(False)
|
||||
buttons = self.manager.multiwell_buttons()
|
||||
|
||||
elif topic == 'up':
|
||||
@@ -444,10 +454,15 @@ class ScannerProcess(Task):
|
||||
elif topic == 'goto_0':
|
||||
self.grbl.go_origin(feed=self.manager.feed)
|
||||
self.manager.well_iterator.reset()
|
||||
self.manager.well_iterator.seek(0)
|
||||
if self.conf.capture_type == 'file':
|
||||
self.manager._capture_file_simulation('zero')
|
||||
|
||||
elif topic == 'goto_xy':
|
||||
self.grbl.move_to(self.manager.xbase, self.manager.ybase, feed=self.manager.feed)
|
||||
self.manager.well_iterator.seek(0)
|
||||
wl = self.manager.well_iterator.seek(0)
|
||||
if self.conf.capture_type == 'file':
|
||||
self.manager._capture_file_simulation(wl.well.name)
|
||||
|
||||
elif topic == 'xy_base':
|
||||
self.manager.set_position()
|
||||
@@ -495,9 +510,10 @@ class ScannerProcess(Task):
|
||||
msg = self.manager.set_well_position()
|
||||
self._send(**msg)
|
||||
|
||||
elif topic == 'track':
|
||||
self.cam.use_tracking = value=="1"
|
||||
self._send(state=topic, msg=f"Tracking: {self.cam.use_tracking}")
|
||||
elif topic == 'draw':
|
||||
draw = (value=="1")
|
||||
self.cam.set_draw_contours(draw)
|
||||
self._send(state=topic, msg=f"Tracking contour: {draw}")
|
||||
|
||||
elif topic in ['min_area_px', 'max_area_ratio', 'max_planarians', 'merge_kernel_size', 'min_contour_dist_px']:
|
||||
value = int(value) if topic in ['min_area_px', 'max_planarians', 'merge_kernel_size', 'min_contour_dist_px'] else float(value)
|
||||
|
||||
@@ -43,12 +43,14 @@ class ScannerManager {
|
||||
this.crop_radius = options.crop_radius;
|
||||
this.calib_auto = options.calib_auto;
|
||||
|
||||
this.min_area_px = options.min_area_px;
|
||||
this.max_area_ratio = options.max_area_ratio;
|
||||
this.max_planarians = options.max_planarians;
|
||||
this.merge_kernel_size = options.merge_kernel_size;
|
||||
this.min_contour_dist_px = options.min_contour_dist_px;
|
||||
this.track = options.track;
|
||||
try {
|
||||
this.min_area_px = options.min_area_px;
|
||||
this.max_area_ratio = options.max_area_ratio;
|
||||
this.max_planarians = options.max_planarians;
|
||||
this.merge_kernel_size = options.merge_kernel_size;
|
||||
this.min_contour_dist_px = options.min_contour_dist_px;
|
||||
this.draw = options.draw;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
init_controls() {
|
||||
@@ -79,12 +81,14 @@ class ScannerManager {
|
||||
this.calib_auto.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "auto" }); });
|
||||
this.halt.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "halt" }); });
|
||||
|
||||
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.track.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "track", value: e.target.value }); });
|
||||
try {
|
||||
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) {}
|
||||
}
|
||||
|
||||
registerSocket(socket) {
|
||||
@@ -99,16 +103,6 @@ class ScannerManager {
|
||||
if (payload.xy) { this.x.textContent=payload.x.toFixed(2); this.y.textContent=payload.y.toFixed(2); }
|
||||
if (payload.state) { this.debug.insertAdjacentHTML('afterbegin', `<li>[ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}</li>`); }
|
||||
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
|
||||
|
||||
/*
|
||||
if (payload.detected && use_tracking) {
|
||||
this.cx.textContent = payload.cx; this.cy.textContent = payload.cy;
|
||||
this.speed_px_s.textContent = payload.speed_px_s;
|
||||
this.axial_speed.textContent = payload.axial_speed;
|
||||
this.axial_pos.textContent = payload.axial_pos;
|
||||
this.area_px.textContent = payload.area_px;
|
||||
this.frame_count.textContent = payload.count;
|
||||
}*/
|
||||
|
||||
if (payload.buttons) { this.well_btn.innerHTML = payload.buttons; }
|
||||
if (payload.current >= 0) {
|
||||
@@ -117,7 +111,6 @@ class ScannerManager {
|
||||
btn.classList.remove('w3-green');
|
||||
});
|
||||
}
|
||||
|
||||
} catch(e) { console.log(e); }
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,43 @@
|
||||
<a href="#" class="w3-bar-item w3-mobile w3-btn w3-hover-opacity w3-right w3-xlarge w3-text-green" onclick="goFullscreen()" title="{% trans 'Ecran plein' %}">
|
||||
<i class="fa-solid fa-maximize"></i>
|
||||
</a>
|
||||
{% if request.user.is_superuser %}
|
||||
<div class="w3-dropdown-hover w3-right">
|
||||
<button class="w3-button w3-dark-plus">☰ <span>{% trans "Administrations" %}</span></button>
|
||||
<div class="w3-dropdown-content w3-bar-block w3-border w3-dark-low" style="right:.25em;">
|
||||
<hr class="w3-bar-item divider w3-grey">
|
||||
|
||||
<a href="{% url 'scanner:admin' %}" class="w3-bar-item w3-btn w3-hover-opacity" title="{% trans "Accès django" %}">
|
||||
<i class="fa-solid fa-gear w3-text-pink"></i> {% trans "Base de données" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:supervisor' %}" class="w3-bar-item w3-btn w3-hover-opacity" target="_blank" title="{% trans "Accès système" %}">
|
||||
<i class="fa-solid fa-gears w3-text-purple"></i> {% trans "Supervision des services" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'scanner:reductstore' %}" class="w3-bar-item w3-btn w3-hover-opacity" target="_blank" title="{% trans "Accès système" %}">
|
||||
<img class="w3-no-padding" src="/static/img/reductstore.png" style="width: 24px"> {% trans "Base de données Reductstore" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:adminer' %}" class="w3-bar-item w3-btn w3-hover-opacity" target="_blank" title="{% trans "Accès système" %}">
|
||||
<img class="w3-no-padding" src="/static/img/adminer.png" style="width: 28px"> {% trans "Base de données mariadb" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'scanner:logs_scheduler' %}" class="w3-bar-item w3-btn w3-hover-opacity" target="_blank">
|
||||
<i class="fa-regular fa-file-lines w3-text-orange"></i> {% trans "Logs des planifications" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:logs_worker' %}" class="w3-bar-item w3-btn w3-hover-opacity" target="_blank">
|
||||
<i class="fa-regular fa-file-lines w3-text-amber"></i> {% trans "Logs des services" %}
|
||||
</a>
|
||||
|
||||
<hr class="w3-bar-item divider w3-grey">
|
||||
<form method="post" action="{% url 'logout' %}">
|
||||
{% csrf_token %}
|
||||
<button class="w3-bar-item w3-btn w3-hover-opacity" type="submit">
|
||||
<i class="fa-solid fa-right-from-bracket w3-text-red"></i> {% trans 'Déconnexion' %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block sidebar %}
|
||||
@@ -63,51 +100,28 @@
|
||||
{% block sidebar_list %}
|
||||
<div class="w3-dark-light w3-padding-small">
|
||||
{% block sidebar_command %}{% endblock %}
|
||||
|
||||
{% if request.user.is_superuser %}
|
||||
<div>
|
||||
<div class="w3-dark-light w3-margin-top-6">
|
||||
<a href="#" class="w3-btn w3-padding-small" onclick="toggleDisplay(sId('_admin'))"><span class="w3-text-orange w3-xlarge">⛶</span>
|
||||
{% trans "Administration" %}
|
||||
</a>
|
||||
</div>
|
||||
<div id="_admin" style="display:none">
|
||||
<a href="{% url 'scanner:admin' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left">
|
||||
<i class="fa-solid fa-gear w3-text-pink"></i> {% trans "Administration base de données" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:reductstore' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left" target="_blank">
|
||||
<img class="w3-no-padding" src="/static/img/reductstore.png" style="width: 24px"> {% trans "Base de données Reductstore" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:adminer' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left" target="_blank">
|
||||
<img class="w3-no-padding" src="/static/img/adminer.png" style="width: 28px"> {% trans "Base de données mariadb" %}
|
||||
</a>
|
||||
<!--a href="{% url 'scanner:portainer' %}" class="w3-bar-item w3-btn w3-hover-opacity" w3-margin-left target="_blank">
|
||||
<img class="w3-no-padding" src="/static/img/portainer.png" style="width: 28px"> {% trans "Portainer" %}
|
||||
</a-->
|
||||
<a href="{% url 'scanner:supervisor' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left" target="_blank">
|
||||
<i class="fa-solid fa-gears w3-text-purple"></i> {% trans "Supervisor" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:logs_scheduler' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left" target="_blank">
|
||||
<i class="fa-regular fa-file-lines w3-text-orange"></i> {% trans "Logs des planifications" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:logs_worker' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-left" target="_blank">
|
||||
<i class="fa-regular fa-file-lines w3-text-amber"></i> {% trans "Logs des services" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block sidebar_smenu %}
|
||||
{% if request.user.is_superuser and request.user.is_staff %}
|
||||
<a href="{% url 'scanner:calibration' %}" class="w3-bar-item w3-btn w3-hover-opacity w3-margin-bottom">
|
||||
{% if request.user.is_superuser or request.user.is_staff %}
|
||||
<a href="{% url 'scanner:session_view' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||
<i class="fa-solid fa-gear w3-text-purple w3-xlarge"></i> {% trans "Création de sessions d'expériences" %}
|
||||
</a>
|
||||
<hr class="w3-bar-item divider w3-grey">
|
||||
<a href="{% url 'scanner:experimentconfig_view' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||
<i class="fa-solid fa-gears w3-text-pink w3-xlarge"></i> {% trans "Configuration des expériences" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:experiment_view' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||
<i class="fa-solid fa-gear w3-text-pink w3-xlarge"></i> {% trans "Gestions des expériences" %}
|
||||
</a>
|
||||
<a href="{% url 'scanner:calibration' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||
<i class="fa-solid fa-wrench w3-text-red w3-xlarge"></i> {% trans "Calibration" %}
|
||||
</a>
|
||||
</a>
|
||||
<hr class="w3-bar-item divider w3-grey w3-margin-bottom">
|
||||
{% endif %}
|
||||
{% if conf.tracking %}
|
||||
<a href="{% url 'planarian:experiment-list' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||
<i class="fa-solid fa-vials w3-text-lime w3-xlarge"></i> {% trans "Préparation des expériences" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{% url 'scanner:scanning' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||
<i class="fa-solid fa-film w3-text-green w3-xlarge""></i> {% trans "Balayage multi-puits" %}
|
||||
</a>
|
||||
|
||||
@@ -149,10 +149,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% if conf.tracking %}
|
||||
<div class="w3-row w3-row-padding w3-light-grey w3-margin-1 w3-round w3-round-large w3-margin-top">
|
||||
<div class="w3-col"><input id="_track_it" type="checkbox" value="0" onclick="if (this.checked){this.value='1';}else{ this.value='0';}">
|
||||
{% trans 'Configuration du tracking' %}
|
||||
</div>
|
||||
<div class="w3-row w3-row-padding w3-light-grey w3-padding-small w3-margin-1 w3-round w3-round-large w3-margin-top w3-left-align w3-paddin-top">
|
||||
<div class="w3-half">
|
||||
<input id="_min_area_px" type="number" min="5" max="100" step="1" class="w3-round" value="{{ conf.min_area_px }}"
|
||||
title="{% trans 'surface minimale de contour pour être considéré valide (px carré)' %}"><br>{% trans 'Min contour' %}
|
||||
@@ -172,7 +169,11 @@
|
||||
<div class="w3-half">
|
||||
<input id="_max_planarians" type="number" min="1" max="10" step="1" class="w3-round" value="{{ conf.max_planarians }}"
|
||||
title="{% trans 'Nombre maximum de planaires à suivre simultanément (1-10)' %}"><br>{% trans 'Planaires' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w3-half">
|
||||
<input id="_draw" type="checkbox" value="0" onclick="if (this.checked){this.value='1';}else{ this.value='0';}" title="{% trans 'Dessiner le contour' %}">
|
||||
<span class="w3-nbsp">{% trans 'Contour' %}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="w3-row w3-row-padding">
|
||||
@@ -260,7 +261,7 @@
|
||||
max_planarians : sId("_max_planarians"),
|
||||
merge_kernel_size : sId("_merge_kernel_size"),
|
||||
min_contour_dist_px : sId("_min_contour_dist_px"),
|
||||
track : sId("_track_it")
|
||||
draw : sId("_draw")
|
||||
};
|
||||
</script>
|
||||
<script src="/static/scanner/js/calibration.js"></script>
|
||||
|
||||
@@ -14,6 +14,11 @@ urlpatterns = [
|
||||
path('logs/worker', views.supervisor_worker, name='logs_worker'),
|
||||
path('logs/scheduler', views.supervisor_scheduler, name='logs_scheduler'),
|
||||
|
||||
path('session/view', views.admin_session_view, name='session_view'),
|
||||
path('experiment/view', views.admin_experiment_view, name='experiment_view'),
|
||||
path('experiment/config/view', views.admin_experimentconfig_view, name='experimentconfig_view'),
|
||||
path('periodictask/view', views.admin_periodictask_view, name='periodictask_view'),
|
||||
|
||||
path('scanning/', views.scanning_view, name='scanning'),
|
||||
path('images/', views.images_view, name='images'),
|
||||
path('replay/', views.replay_view, name='replay'),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#
|
||||
from asgiref.sync import async_to_sync
|
||||
import base64, json
|
||||
from django.shortcuts import render, redirect
|
||||
from django.shortcuts import render #, redirect
|
||||
from django.http import JsonResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.http import require_GET, require_POST
|
||||
@@ -12,7 +12,7 @@ from reduct.time import unix_timestamp_to_iso
|
||||
from modules.system_stats import get_cached_stats, start_background_updater
|
||||
from modules import reductstore
|
||||
|
||||
from .tasks import download_video, export_all_images, export_all_videos, supervisor_restart_service
|
||||
from .tasks import download_video, export_all_images, export_all_videos
|
||||
from .process import CameraRecordManager, cameraDB
|
||||
from . import models
|
||||
from .constants import ScannerConstants
|
||||
@@ -39,6 +39,8 @@ def global_context(request, **ctx):
|
||||
app_title=settings.APP_TITLE,
|
||||
app_sub_title=settings.APP_SUB_TITLE,
|
||||
domain_server=settings.DOMAIN_SERVER,
|
||||
local_ip_server=settings.LOCAL_IP_SERVER,
|
||||
host_port=settings.SERVER_HOST_PORT,
|
||||
conf=conf,
|
||||
default_position = default_multiwell.position or 'HD',
|
||||
export_destination=settings.EXPORT_DESTINATIONS,
|
||||
@@ -49,29 +51,45 @@ def global_context(request, **ctx):
|
||||
def admin_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/'))
|
||||
|
||||
@login_required
|
||||
def admin_session_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/scanner/session/'))
|
||||
|
||||
@login_required
|
||||
def admin_experiment_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/scanner/experiment/'))
|
||||
|
||||
@login_required
|
||||
def admin_experimentconfig_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/planarian/experimentconfig/'))
|
||||
|
||||
@login_required
|
||||
def admin_periodictask_view(request):
|
||||
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/django_celery_beat/periodictask/'))
|
||||
|
||||
@login_required
|
||||
def reductstore_view(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.DOMAIN_SERVER}:8383/'))
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:8383/'))
|
||||
|
||||
@login_required
|
||||
def adminer_view(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.DOMAIN_SERVER}/adminer/'))
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}/adminer/'))
|
||||
|
||||
@login_required
|
||||
def portainer_view(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.DOMAIN_SERVER}:9000/'))
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9000/'))
|
||||
|
||||
@login_required
|
||||
def supervisor_view(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.DOMAIN_SERVER}:9001/'))
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/'))
|
||||
|
||||
@login_required
|
||||
def supervisor_worker(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.DOMAIN_SERVER}:9001/logtail/test_tube:services'))
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/logtail/test_tube:services'))
|
||||
|
||||
@login_required
|
||||
def supervisor_scheduler(request):
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.DOMAIN_SERVER}:9001/logtail/test_tube:planification'))
|
||||
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/logtail/test_tube:planification'))
|
||||
|
||||
## Mainboard
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user