planarian

This commit is contained in:
2026-05-02 22:00:41 +02:00
parent 5b4b1e63a6
commit 309e2d4f45
6 changed files with 55 additions and 21 deletions
+18 -5
View File
@@ -24,8 +24,10 @@ from typing import Optional, Callable, TYPE_CHECKING
from django.conf import settings from django.conf import settings
from modules.planarian_tracker import PlanarianTracker from modules.planarian_tracker import PlanarianTracker
from modules.planarian_metrics import ExperimentParams
from modules.tube_aligner import TubeAligner from modules.tube_aligner import TubeAligner
if TYPE_CHECKING: if TYPE_CHECKING:
from .circular_crop import CircularCrop # Evite l'import circulaire au runtime from .circular_crop import CircularCrop # Evite l'import circulaire au runtime
@@ -58,6 +60,7 @@ class VideoCaptureInterface(abc.ABC):
self._fps: float = fps self._fps: float = fps
self.display = display self.display = display
self.parent = parent self.parent = parent
self.use_tracking = use_tracking
self.jpeg_quality = jpeg_quality self.jpeg_quality = jpeg_quality
self._interval: float = 1.0 / fps # Intervalle en secondes entre chaque capture self._interval: float = 1.0 / fps # Intervalle en secondes entre chaque capture
self._running: bool = False # Indique si la capture est en cours self._running: bool = False # Indique si la capture est en cours
@@ -70,6 +73,8 @@ class VideoCaptureInterface(abc.ABC):
self._error_occured = False self._error_occured = False
self._tracker = None self._tracker = None
self._metrics = None
self._paramss = None
if use_tracking: if use_tracking:
self._tracker = PlanarianTracker( self._tracker = PlanarianTracker(
tube_axis = settings.TRACKER_TUBE_AXIS, tube_axis = settings.TRACKER_TUBE_AXIS,
@@ -77,8 +82,7 @@ class VideoCaptureInterface(abc.ABC):
max_area_ratio = settings.TRACKER_MAX_AREA_RATIO, max_area_ratio = settings.TRACKER_MAX_AREA_RATIO,
max_planarians = settings.TRACKER_MAX_PLANARIANS, max_planarians = settings.TRACKER_MAX_PLANARIANS,
) )
self._aligner = TubeAligner( self._aligner = TubeAligner(
grbl_threshold_px = 20, # au-delà → correction GRBL grbl_threshold_px = 20, # au-delà → correction GRBL
dead_zone_px = 5, # en-dessous → rien à faire dead_zone_px = 5, # en-dessous → rien à faire
@@ -87,13 +91,22 @@ class VideoCaptureInterface(abc.ABC):
self.align_detection = None # résultat du test self.align_detection = None # résultat du test
def on_well_change(self): def on_well_change(self, cfg):
""" """
Appelé par le CNC lors du changement de puits. Appelé par le CNC lors du changement de puits.
Réinitialise le fond appris et l'état inter-frame du tracker. Réinitialise le fond appris et l'état inter-frame du tracker.
Construit les métriques aussi
""" """
if self._tracker: if self.use_tracking and self._tracker:
self._tracker.reset() self._tracker.reset()
self._params = ExperimentParams(cfg.to_params_dict())
self._metrics = self._params.build_metrics()
self._tracker = PlanarianTracker(
tube_axis = self._params.tube_axis,
min_area_px = self._params.min_area_px,
max_area_ratio = self._params.max_area_ratio,
max_planarians = self._params.planarian_count,
)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
+1 -1
View File
@@ -30,7 +30,7 @@ class ExperimentConfigAdmin(admin.ModelAdmin):
"classes": ("collapse",), "classes": ("collapse",),
}), }),
(_("Tracker"), { (_("Tracker"), {
"fields": ("tube_axis", "min_area_px", "planarian_count"), "fields": ("tube_axis", "min_area_px", "max_area_ratio", "planarian_count"),
"classes": ("collapse",), "classes": ("collapse",),
}), }),
(_("Thigmotactisme"), { (_("Thigmotactisme"), {
+6
View File
@@ -63,6 +63,12 @@ class ExperimentConfig(models.Model):
default=20, default=20,
verbose_name=_("Surface min détection (px²)"), verbose_name=_("Surface min détection (px²)"),
) )
max_area_ratio = models.FloatField(
default=0.05,
verbose_name=_("Filtre surface max acceptable"),
)
planarian_count = models.IntegerField( planarian_count = models.IntegerField(
default=1, default=1,
verbose_name=_("Nombre de planaires"), verbose_name=_("Nombre de planaires"),
@@ -172,6 +172,14 @@
<span class="w3-text-red w3-small">{{ form.min_area_px.errors|join:", " }}</span> <span class="w3-text-red w3-small">{{ form.min_area_px.errors|join:", " }}</span>
{% endif %} {% endif %}
</div> </div>
<div class="w3-col m4 s12 w3-margin-bottom">
<label class="w3-text-grey"><b>{{ form.max_area_ratio.label }}</b></label>
{{ form.max_area_ratio }}
{% if form.max_area_ratio.errors %}
<span class="w3-text-red w3-small">{{ form.max_area_ratio.errors|join:", " }}</span>
{% endif %}
</div>
<div class="w3-col m4 s12 w3-margin-bottom"> <div class="w3-col m4 s12 w3-margin-bottom">
<label class="w3-text-grey"><b>{{ form.planarian_count.label }}</b></label> <label class="w3-text-grey"><b>{{ form.planarian_count.label }}</b></label>
+21 -12
View File
@@ -14,6 +14,7 @@ from threading import Thread, Event
from django.utils import timezone from django.utils import timezone
from django.utils.html import mark_safe from django.utils.html import mark_safe
from django.conf import settings from django.conf import settings
from planarian.models import ExperimentConfig
from . import models from . import models
@@ -120,22 +121,31 @@ class MultiWellManager:
return mark_safe("\n".join(multiwells)) return mark_safe("\n".join(multiwells))
def _grid_scanning_capture(self, uuid, duration): #def _grid_scanning_capture(self, uuid, duration):
def _grid_scanning_capture(self, experiment, well_position):
well = well_position.well
multiwell = experiment.multiwell
# Paramètres d'une expérience PlanarianScanner
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 self.process.data.uuid = uuid
self.process.data.record = True self.process.data.record = True
# reset PlanarianTracker => on_well_change
self.process.cam.on_well_change()
start = time.monotonic() start = time.monotonic()
while not self.stop_playing.is_set(): while not self.stop_playing.is_set():
if time.monotonic() - start > duration: if time.monotonic() - start > multiwell.duration:
break break
self.cnc_controller.wait_for(1.0) self.cnc_controller.wait_for(1.0)
logger.info(f"Arrêter l'enregistrement {uuid}")
self.process.data.record = False self.process.data.record = False
self.process.data.uuid = None self.process.data.uuid = None
msg = f"{uuid}: capture done"
logger.info(msg)
self.process._send(scan_state=msg)
def _grid_scanning(self, experiment, xnext=0, ynext=0): def _grid_scanning(self, experiment, xnext=0, ynext=0):
@@ -150,15 +160,14 @@ class MultiWellManager:
break break
self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed) self.cnc_controller.move_to(wl.x, wl.y, feed=wl.multiwell.feed)
uuid = f'{self.process.data.session}-{multiwell.position}-{wl.well.name}' #uuid = f'{self.process.data.session}-{multiwell.position}-{wl.well.name}'
self._grid_scanning_capture(uuid, multiwell.duration) #self._grid_scanning_capture(uuid, multiwell.duration)
self._grid_scanning_capture(experiment, wl)
## change file ## change file
if self.process.conf.capture_type == 'file': if self.process.conf.capture_type == 'file':
self.process.cam._error_occured = True self.process.cam._error_occured = True
self.process._send(scan_state=f"{uuid}: capture")
logger.info(f"Scan terminé — retour à l'origine (X={xnext:.1f} Y={ynext:.1f})") 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) self.cnc_controller.move_to(xnext, ynext, feed=multiwell.feed*2)
+1 -3
View File
@@ -162,9 +162,7 @@ class ScannerProcess(Task):
return CircularCrop(radius=radius, strategy=CropStrategy.CROP_JPEG, jpeg_quality=self.image_quality) return CircularCrop(radius=radius, strategy=CropStrategy.CROP_JPEG, jpeg_quality=self.image_quality)
def start(self, *args, **kwargs): def start(self, *args, **kwargs):
try: try:
logger.warning("ScannerProcess==================", self.name)
self.conf = ScannerConstants().get() self.conf = ScannerConstants().get()
self.use_tracking = self.conf.tracking self.use_tracking = self.conf.tracking