This commit is contained in:
2026-05-08 11:48:46 +02:00
parent 1575445df4
commit 563069d3c6
19 changed files with 557 additions and 164 deletions
+1
View File
@@ -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
+11 -4
View File
@@ -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)
+80 -57
View File
@@ -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."""
+24 -8
View File
@@ -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">&#9776;&nbsp;<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">&#x26F6;</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>
+5
View File
@@ -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'),
+26 -8
View File
@@ -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