calibration

This commit is contained in:
2026-05-03 13:24:25 +02:00
parent 0f22451516
commit ce32bee3e8
12 changed files with 235 additions and 55 deletions
+4 -2
View File
@@ -396,9 +396,11 @@ EXPORT_DESTINATIONS = ["local", "remote"]
TRACKER_TUBE_AXIS = "vertical" TRACKER_TUBE_AXIS = "vertical"
TRACKER_MIN_AREA = 8 # surface min planaire px x px TRACKER_MIN_AREA = 20 # surface min planaire px x px
TRACKER_MAX_AREA_RATIO = 0.1 # 5% de la frame = surface max acceptable TRACKER_MAX_AREA_RATIO = 0.1 # 5% de la frame = surface max acceptable
TRACKER_MAX_PLANARIANS = 3 TRACKER_MAX_PLANARIANS = 1
TRACKER_MERGE_KERNEL_SIZE = 15 # augmenter si fragments résiduels
TRACKER_MIN_CONTOUR_DIST_PX = 40 # augmenter si IDs multiples persistent
CALIBRATION_AUTO_DURATION = 45.0 CALIBRATION_AUTO_DURATION = 45.0
+25 -10
View File
@@ -51,6 +51,16 @@ class VideoCaptureInterface(abc.ABC):
# Cadence par défaut en images par seconde # Cadence par défaut en images par seconde
DEFAULT_FPS: float = 5.0 DEFAULT_FPS: float = 5.0
DEFAULT_TRACKER_CONFIG = dict(
tube_axis = settings.TRACKER_TUBE_AXIS,
min_area_px = settings.TRACKER_MIN_AREA,
max_area_ratio = settings.TRACKER_MAX_AREA_RATIO,
max_planarians = settings.TRACKER_MAX_PLANARIANS,
merge_kernel_size = settings.TRACKER_MERGE_KERNEL_SIZE,
min_contour_dist_px = settings.TRACKER_MIN_CONTOUR_DIST_PX,
)
def __init__(self, fps: float = DEFAULT_FPS, use_tracking: bool = False, display=None, parent=None, jpeg_quality=85): def __init__(self, fps: float = DEFAULT_FPS, use_tracking: bool = False, display=None, parent=None, jpeg_quality=85):
""" """
Initialise l'interface de capture. Initialise l'interface de capture.
@@ -75,13 +85,9 @@ class VideoCaptureInterface(abc.ABC):
self._tracker = None self._tracker = None
self._metrics = None self._metrics = None
self._paramss = None self._paramss = None
if use_tracking:
self._tracker = PlanarianTracker( # Tracker générique, pour simulation
tube_axis = settings.TRACKER_TUBE_AXIS, self.on_test_well_change(**self.DEFAULT_TRACKER_CONFIG)
min_area_px = settings.TRACKER_MIN_AREA,
max_area_ratio = settings.TRACKER_MAX_AREA_RATIO,
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
@@ -91,14 +97,21 @@ class VideoCaptureInterface(abc.ABC):
self.align_detection = None # résultat du test self.align_detection = None # résultat du test
def on_test_well_change(self, **cfg):
if self.use_tracking and cfg:
logger.warning(f"tracking conf: {cfg}")
self._tracker = PlanarianTracker(**cfg)
def on_well_change(self, cfg): 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 Construit les métriques aussi
""" """
if self.use_tracking and self._tracker: if not self.use_tracking:
self._tracker.reset() return
self._params = ExperimentParams(cfg.to_params_dict()) self._params = ExperimentParams(cfg.to_params_dict())
self._metrics = self._params.build_metrics() self._metrics = self._params.build_metrics()
self._tracker = PlanarianTracker( self._tracker = PlanarianTracker(
@@ -106,6 +119,8 @@ class VideoCaptureInterface(abc.ABC):
min_area_px = self._params.min_area_px, min_area_px = self._params.min_area_px,
max_area_ratio = self._params.max_area_ratio, max_area_ratio = self._params.max_area_ratio,
max_planarians = self._params.planarian_count, max_planarians = self._params.planarian_count,
merge_kernel_size = settings.TRACKER_MERGE_KERNEL_SIZE,
min_contour_dist_px = settings.TRACKER_MIN_CONTOUR_DIST_PX,
) )
@@ -262,7 +277,7 @@ class VideoCaptureInterface(abc.ABC):
frame = annotated if annotated is not None else frame frame = annotated if annotated is not None else frame
## ##
# mode racking # mode racking
if self._tracker is not None: if self.use_tracking:
ts = datetime.now(timezone.utc).timestamp() ts = datetime.now(timezone.utc).timestamp()
frame, metrics = self._tracker.process(frame, ts) frame, metrics = self._tracker.process(frame, ts)
## ##
+58 -15
View File
@@ -4,22 +4,13 @@ modules/planarian_tracker.py
Détection et suivi multi-individus de planaires dans un tube. Détection et suivi multi-individus de planaires dans un tube.
Supporte de 1 à MAX_PLANARIANS planaires par tube. Supporte de 1 à MAX_PLANARIANS planaires par tube.
Etat inter-frame indépendant par individu : position, timestamp, compteur de perte (lost), flag active.
Quand un individu n'est pas détecté pendant MAX_LOST_FRAMES (5) frames consécutives, il est marqué perdu et son slot se libère.
Algorithme hongrois (scipy.optimize.linear_sum_assignment) dans _hungarian_assign()
— construit une matrice de coût distance euclidienne entre les slots actifs et les nouvelles détections, puis trouve l'association de coût minimal.
Une association est rejetée si la distance dépasse MAX_ASSOC_DIST_PX (80px)
— évite les sauts aberrants entre planaires proches.
Stratégie : Stratégie :
- Soustraction de fond MOG2 (léger sur Raspberry Pi 4) - Soustraction de fond MOG2 (léger sur Raspberry Pi 4)
- Détection de tous les contours valides (surface >= min_area_px) - Détection de tous les contours valides (surface >= min_area_px)
- Association frame-à-frame par distance euclidienne minimale - Association frame-à-frame par distance euclidienne minimale
via algorithme hongrois (scipy.optimize.linear_sum_assignment) via algorithme hongrois (scipy.optimize.linear_sum_assignment)
- Un état inter-frame indépendant par individu (PlanarianState) - Un état inter-frame indépendant par individu (PlanarianState)
- Retourne une liste de résultats, un par individu suivi: champ planarian_id (index 0-based). - Retourne une liste de résultats, un par individu suivi
Created on 25 avr. 2026 Created on 25 avr. 2026
@author: denis @author: denis
@@ -175,6 +166,8 @@ class PlanarianTracker:
min_area_px: int = 20, min_area_px: int = 20,
max_area_ratio: float = 0.10, max_area_ratio: float = 0.10,
max_planarians: int = 1, max_planarians: int = 1,
merge_kernel_size: int = 15,
min_contour_dist_px:int = 40,
): ):
""" """
Args: Args:
@@ -183,6 +176,10 @@ class PlanarianTracker:
max_area_ratio : surface maximale d'un contour en fraction de la frame (défaut 10%) max_area_ratio : surface maximale d'un contour en fraction de la frame (défaut 10%)
filtre les faux positifs du fond non encore appris par MOG2 filtre les faux positifs du fond non encore appris par MOG2
max_planarians : nombre maximum de planaires à suivre simultanément (1-10) max_planarians : nombre maximum de planaires à suivre simultanément (1-10)
merge_kernel_size : taille du kernel elliptique de fusion des fragments (px).
Régler ≈ largeur du planaire en pixels. Défaut : 15.
min_contour_dist_px : distance min entre deux contours pour les considérer
comme individus distincts. Défaut : 40px.
""" """
self.tube_axis = tube_axis self.tube_axis = tube_axis
self.min_area_px = min_area_px self.min_area_px = min_area_px
@@ -192,6 +189,16 @@ class PlanarianTracker:
# Un état inter-frame par slot individu # Un état inter-frame par slot individu
self._states = [PlanarianState(i) for i in range(self.max_planarians)] self._states = [PlanarianState(i) for i in range(self.max_planarians)]
# Taille du kernel de fusion morphologique (pixels) —
# doit être proche de la largeur du planaire en pixels.
# Trop petit : fragments non fusionnés → IDs multiples.
# Trop grand : deux planaires proches fusionnés en un seul.
self.merge_kernel_size = merge_kernel_size
# Distance minimale en pixels entre deux contours distincts.
# En-dessous : le plus petit est considéré comme fragment du plus grand.
self.min_contour_dist_px = min_contour_dist_px
# Soustracteur de fond adaptatif MOG2 # Soustracteur de fond adaptatif MOG2
self._bg_sub = self._make_bg_sub() self._bg_sub = self._make_bg_sub()
@@ -217,6 +224,8 @@ class PlanarianTracker:
s.reset() s.reset()
self._bg_sub = self._make_bg_sub() self._bg_sub = self._make_bg_sub()
self._warmup_count = 0 self._warmup_count = 0
# Les paramètres morphologiques (merge_kernel_size, min_contour_dist_px)
# sont conservés — ils ne dépendent pas du puits
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Interface principale # Interface principale
@@ -253,9 +262,19 @@ class PlanarianTracker:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
fg_mask = self._bg_sub.apply(gray) fg_mask = self._bg_sub.apply(gray)
kernel = np.ones((3, 3), np.uint8) # --- Morphologie : fusion des fragments du corps ---
fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_OPEN, kernel) # Un planaire ondulant est souvent segmenté en plusieurs contours
fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_CLOSE, kernel) # (tête, milieu, queue). La dilatation fusionne les fragments proches
# avant la détection des contours.
# Kernel 3×3 : supprime le bruit fin (OPEN)
# Kernel merge_kernel : fusionne les fragments du corps (CLOSE + DILATE)
noise_kernel = np.ones((3, 3), np.uint8)
merge_kernel = cv2.getStructuringElement(
cv2.MORPH_ELLIPSE, (self.merge_kernel_size, self.merge_kernel_size)
)
fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_OPEN, noise_kernel)
fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_CLOSE, merge_kernel)
fg_mask = cv2.dilate(fg_mask, merge_kernel, iterations=1)
# Warmup MOG2 : les premières WARMUP_FRAMES frames retournent du bruit # Warmup MOG2 : les premières WARMUP_FRAMES frames retournent du bruit
# (fond non encore appris) — on les alimente mais on ne détecte rien # (fond non encore appris) — on les alimente mais on ne détecte rien
@@ -279,8 +298,33 @@ class PlanarianTracker:
reverse=True, reverse=True,
) )
# --- Suppression des fragments résiduels trop proches ---
# Si plusieurs contours valides ont leur centre à moins de
# min_contour_dist_px les uns des autres, seul le plus grand est conservé.
# Évite les cas où la fusion morphologique est incomplète.
filtered = []
for c in valid:
M = cv2.moments(c)
if M["m00"] == 0:
continue
cx_c = int(M["m10"] / M["m00"])
cy_c = int(M["m01"] / M["m00"])
too_close = False
for kept in filtered:
Mk = cv2.moments(kept)
if Mk["m00"] == 0:
continue
cx_k = int(Mk["m10"] / Mk["m00"])
cy_k = int(Mk["m01"] / Mk["m00"])
dist = np.sqrt((cx_c - cx_k)**2 + (cy_c - cy_k)**2)
if dist < self.min_contour_dist_px:
too_close = True
break
if not too_close:
filtered.append(c)
# Limiter au nombre maximum de planaires attendus # Limiter au nombre maximum de planaires attendus
valid = valid[:self.max_planarians] valid = filtered[:self.max_planarians]
# --- Calcul des centres de masse des contours détectés --- # --- Calcul des centres de masse des contours détectés ---
detections = [] # liste de (cx, cy, area, contour) detections = [] # liste de (cx, cy, area, contour)
@@ -489,4 +533,3 @@ class PlanarianTracker:
"axial_pos": 0.0, "axial_pos": 0.0,
"timestamp": ts, "timestamp": ts,
} }
+4 -2
View File
@@ -65,8 +65,9 @@ class ExperimentConfig(models.Model):
) )
max_area_ratio = models.FloatField( max_area_ratio = models.FloatField(
default=0.05, default=0.10,
verbose_name=_("Filtre surface max acceptable"), verbose_name=_("Surface max contour (fraction de la frame)"),
help_text=_("Ratio de la surface du puits, ex: 0.10 pour 10%"),
) )
planarian_count = models.IntegerField( planarian_count = models.IntegerField(
@@ -131,6 +132,7 @@ class ExperimentConfig(models.Model):
"thresh_mobile": self.thresh_mobile, "thresh_mobile": self.thresh_mobile,
"tube_axis": self.tube_axis, "tube_axis": self.tube_axis,
"min_area_px": self.min_area_px, "min_area_px": self.min_area_px,
"max_area_ratio": self.max_area_ratio,
"planarian_count": self.planarian_count, "planarian_count": self.planarian_count,
"thigmotaxis_wall_dist_mm": self.thigmotaxis_wall_dist_mm, "thigmotaxis_wall_dist_mm": self.thigmotaxis_wall_dist_mm,
"photo_mode": self.photo_mode, "photo_mode": self.photo_mode,
+49 -1
View File
@@ -1,8 +1,8 @@
from django.utils.translation import gettext_lazy as _
from django.contrib import admin from django.contrib import admin
from django.db.models import Q from django.db.models import Q
from . import models from . import models
class WellAdmin(admin.ModelAdmin): class WellAdmin(admin.ModelAdmin):
model = models.Well model = models.Well
list_display = ('name', 'author',) list_display = ('name', 'author',)
@@ -10,10 +10,57 @@ class WellAdmin(admin.ModelAdmin):
class ConfigurationAdmin(admin.ModelAdmin): class ConfigurationAdmin(admin.ModelAdmin):
list_display = ('name', 'author', 'capture_type', 'video_width_capture', 'video_height_capture', 'video_frame_rate', 'active',) list_display = ('name', 'author', 'capture_type', 'video_width_capture', 'video_height_capture', 'video_frame_rate', 'active',)
fieldsets = (
(_("Identification"), {
"fields": ("name", "author", "active"),
}),
(_("Dashboard"), {
"fields": ("sidebar_width", "default_grid_columns",),"classes": ("collapse",),
}),
(_("opencv"), {
"fields": ("opencv_fourcc_format", "opencv_video_type"),"classes": ("collapse",),
}),
(_("Grbl"), {
"fields": ("grbl_xmax", "grbl_ymax"),
"classes": ("collapse",),
}),
(_("Camera"), {
"fields": ("capture_type", "webcam_device_index", "image_quality", "video_jpeg_quality", "video_frame_rate", "video_width_capture", "video_height_capture"),
"classes": ("collapse",),
}),
(_("Calibration"), {
"fields": ("calibration_crop_radius", "calibration_default_multiwell", "calibration_default_feed", "calibration_default_step", "calibration_default_duration"),
"classes": ("collapse",),
}),
(_("Tracking"), {
"fields": ("tracking", "min_area_px", "max_area_ratio", "max_planarians", "merge_kernel_size", "min_contour_dist_px"),
"classes": ("collapse",),
}),
)
class MultiWellAdmin(admin.ModelAdmin): class MultiWellAdmin(admin.ModelAdmin):
list_filter = ('author', ) list_filter = ('author', )
list_display = ('label', 'position', 'author', 'order', 'xbase', 'ybase', 'duration', 'feed', 'default', 'well_position', 'active',) list_display = ('label', 'position', 'author', 'order', 'xbase', 'ybase', 'duration', 'feed', 'default', 'well_position', 'active',)
fieldsets = (
(_("Identification"), {
"fields": ("label", "author", "position", "default", "active"),
}),
(_("Géométrie"), {
"fields": ("cols", "rows", "diameter", "row_def", "row_order"),"classes": ("collapse",),
}),
(_("Déplacement"), {
"fields": ("order", "duration", "xbase", "ybase", "dx", "dy", "feed"),"classes": ("collapse",),
}),
(_("Positions générées"), {
"fields": ("well_position",),
}),
)
class WellPositionAdmin(admin.ModelAdmin): class WellPositionAdmin(admin.ModelAdmin):
list_filter = ('author', 'multiwell') list_filter = ('author', 'multiwell')
list_display = ('multiwell__position', 'well__name', 'order', 'x', 'y', 'px_per_mm', 'author',) list_display = ('multiwell__position', 'well__name', 'order', 'x', 'y', 'px_per_mm', 'author',)
@@ -29,6 +76,7 @@ class ExperimentAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'multiwell', 'created', 'started', 'finished') list_display = ('title', 'author', 'multiwell', 'created', 'started', 'finished')
readonly_fields = ('created', 'started', 'finished', ) readonly_fields = ('created', 'started', 'finished', )
class SessionExperimentInlineAdmin(admin.TabularInline): class SessionExperimentInlineAdmin(admin.TabularInline):
model = models.SessionExperiment model = models.SessionExperiment
fk_name = 'session' fk_name = 'session'
+5
View File
@@ -28,6 +28,11 @@ class DefaultConfig:
calibration_default_step: float = 1.0 calibration_default_step: float = 1.0
calibration_default_duration: float = 3.0 calibration_default_duration: float = 3.0
tracking: bool = False tracking: bool = False
min_area_px: int = 20
max_area_ratio: float = 0.10
max_planarians: int = 1
merge_kernel_size: int = 15
min_contour_dist_px: int = 40
class ScannerConstants: class ScannerConstants:
+10 -3
View File
@@ -66,6 +66,13 @@ 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) calibration_default_duration = models.FloatField(_("Duruée calibration"), help_text=_("Durée de pose entre chaque puits en s"), default=3.0)
# tracking # tracking
tracking = models.BooleanField(_("Suivi"), help_text=_("Suivi et analyse des planaires"), default=False) tracking = models.BooleanField(_("Suivi"), help_text=_("Suivi et analyse des planaires"), default=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)
# #
active = models.BooleanField(_("Actif"), default=False) active = models.BooleanField(_("Actif"), default=False)
@@ -97,18 +104,18 @@ class Well(models.Model):
class MultiWell(models.Model): class MultiWell(models.Model):
# Identification
label = models.CharField(_("Label"), help_text=_("Label du multi-puit"), max_length=100, null=True, blank=True) label = models.CharField(_("Label"), help_text=_("Label du multi-puit"), max_length=100, null=True, blank=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True) author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
position = models.CharField(_("Position"), help_text=_('Position du multi-puits sur la table'), unique=True, max_length=8, choices=MULTIWELL_POSITION, null=True, blank=False) position = models.CharField(_("Position"), help_text=_('Position du multi-puits sur la table'), unique=True, max_length=8, choices=MULTIWELL_POSITION, null=True, blank=False)
default = models.BooleanField(_("Par défaut"), help_text=_('Multi-puit par défaut'), default=False) default = models.BooleanField(_("Par défaut"), help_text=_('Multi-puit par défaut'), default=False)
# Configuration
cols = models.PositiveSmallIntegerField(_("Colonnes"), help_text=_('Nombre de colonnes'), blank=False, default=6) cols = models.PositiveSmallIntegerField(_("Colonnes"), help_text=_('Nombre de colonnes'), blank=False, default=6)
rows = models.PositiveSmallIntegerField(_("Lignes"), help_text=_('Nombre de lignes'), blank=False, default=4) rows = models.PositiveSmallIntegerField(_("Lignes"), help_text=_('Nombre de lignes'), blank=False, default=4)
diameter = models.FloatField(_("Diamètre"), help_text=_('Diamètre des tubes en mm'), blank=False, default=16.0) diameter = models.FloatField(_("Diamètre"), help_text=_('Diamètre des tubes en mm'), blank=False, default=16.0)
row_def = models.CharField(_("Définition"), help_text=_('Définition des lignes'), max_length=16, null=True, blank=False, default="A,B,C,D") row_def = models.CharField(_("Définition"), help_text=_('Définition des lignes'), max_length=16, null=True, blank=False, default="A,B,C,D")
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") 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) 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'), blank=False, default=120)
xbase = models.FloatField(_("Origine X"), help_text=_('Base origine X en mm'), blank=False, default=50.0) xbase = models.FloatField(_("Origine X"), help_text=_('Base origine X en mm'), blank=False, default=50.0)
+10
View File
@@ -78,6 +78,7 @@ class MultiWellManager:
def __init__(self, process): def __init__(self, process):
self.process = process self.process = process
self.cnc_controller = process.grbl self.cnc_controller = process.grbl
self.stop_playing = Event() self.stop_playing = Event()
self.well_iterator = None self.well_iterator = None
self.multiwel = None self.multiwel = None
@@ -85,6 +86,15 @@ class MultiWellManager:
self.set_multiwell() self.set_multiwell()
self.scan_thread = None self.scan_thread = None
self.test_thread = None self.test_thread = None
self.tracker_config = dict(
tube_axis = settings.TRACKER_TUBE_AXIS,
min_area_px = self.process.conf.min_area_px,
max_area_ratio = self.process.conf.max_area_ratio,
max_planarians = self.process.conf.max_planarians,
merge_kernel_size = self.process.conf.merge_kernel_size,
min_contour_dist_px = self.process.conf.min_contour_dist_px,
)
def set_default_values(self, feed=None, step=None, duration=None): def set_default_values(self, feed=None, step=None, duration=None):
self._feed = feed or self.process.conf.calibration_default_feed self._feed = feed or self.process.conf.calibration_default_feed
+6
View File
@@ -468,6 +468,12 @@ class ScannerProcess(Task):
msg = self.manager.set_well_position() msg = self.manager.set_well_position()
self._send(**msg) self._send(**msg)
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)
self.manager.tracker_config[topic] = value
self.cam.on_test_well_change(**self.manager.tracker_config)
self._send(state=topic, msg=f"Value changed {value}")
self._send( self._send(
xbase=self.manager.xbase, xbase=self.manager.xbase,
ybase=self.manager.ybase, ybase=self.manager.ybase,
@@ -10,5 +10,3 @@
align-items: center; align-items: center;
} }
@@ -42,6 +42,13 @@ class ScannerManager {
this.crop = options.crop; this.crop = options.crop;
this.crop_radius = options.crop_radius; this.crop_radius = options.crop_radius;
this.calib_auto = options.calib_auto; 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;
} }
init_controls() { init_controls() {
@@ -71,6 +78,13 @@ class ScannerManager {
this.calib_center.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "center" }); }); 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.calib_auto.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "auto" }); });
this.halt.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "halt" }); }); 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 }); });
} }
registerSocket(socket) { registerSocket(socket) {
@@ -133,12 +133,12 @@
</div> </div>
<div class="w3-row w3-row-padding"> <div class="w3-row w3-row-padding">
<div class="w3-half"> <div class="w3-half">
<button id="_calib_debug" class="w3-button w3-dark-xlight w3-round-large w3-padding-1" title="{% trans 'Ce mode permet les réglages graphiques' %}"> <button id="_calib_debug" class="w3-button w3-dark-xlight w3-round-large w3-padding-1 w3-block" title="{% trans 'Ce mode permet les réglages graphiques' %}">
{% trans 'Debug' %}<br><i class="fa-solid fa-triangle-exclamation w3-text-amber w3-xlarge"></i> {% trans 'Debug' %}<br><i class="fa-solid fa-triangle-exclamation w3-text-amber w3-xlarge"></i>
</button> </button>
</div> </div>
<div class="w3-half"> <div class="w3-half">
<button id="_median" class="w3-button w3-dark-xlight w3-round-large w3-padding-1"> <button id="_median" class="w3-button w3-dark-xlight w3-round-large w3-padding-1 w3-block">
{% trans 'Axes' %}<br><i class="fa-solid fa-crosshairs w3-text-amber w3-xlarge"></i> {% trans 'Axes' %}<br><i class="fa-solid fa-crosshairs w3-text-amber w3-xlarge"></i>
</button> </button>
</div> </div>
@@ -148,6 +148,31 @@
</button> </button>
</div> </div>
</div> </div>
{% if conf.tracking %}
<div class="w3-row w3-row-padding w3-light-grey w3-margin-1 w3-round w3-round-large">
<div class="w3-col">{% trans 'Configuration du tracking' %}</div>
<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' %}
</div>
<div class="w3-half">
<input id="_max_area_ratio" type="number" min="0.05" max="1.0" step="0.05" class="w3-round" value="{{ conf.max_area_ratio|stringformat:"s" }}"
title="{% trans 'surface maximale de contour en fraction de la frame (défaut 10%)' %}"><br>{% trans 'Max contour' %}
</div>
<div class="w3-half">
<input id="_merge_kernel_size" type="number" min="5" max="320" step="1" class="w3-round" value="{{ conf.merge_kernel_size }}"
title="{% trans 'Taille du kernel elliptique de fusion des fragments (px)' %}"><br>{% trans 'Taille kernel' %}
</div>
<div class="w3-half">
<input id="_min_contour_dist_px" type="number" min="10" max="320" step="1" class="w3-round" value="{{ conf.min_contour_dist_px }}"
title="{% trans 'Distance min entre deux contours pour les considérer comme individus distincts. Défaut: 40px' %}"><br>{% trans 'Distance min' %}
</div>
<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>
{% endif %}
<div class="w3-row w3-row-padding"> <div class="w3-row w3-row-padding">
<div class="w3-half w3-margin-top"> <div class="w3-half w3-margin-top">
<span>{% trans 'Rayon' %} px</span><br> <span>{% trans 'Rayon' %} px</span><br>
@@ -227,7 +252,12 @@
well_btn : sId("_well_btn"), well_btn : sId("_well_btn"),
median : sId("_median"), median : sId("_median"),
crop : sId("_crop"), crop : sId("_crop"),
crop_radius : sId("_crop_radius") crop_radius : sId("_crop_radius"),
min_area_px : sId("_min_area_px"),
max_area_ratio : sId("_max_area_ratio"),
max_planarians : sId("_max_planarians"),
merge_kernel_size : sId("_merge_kernel_size"),
min_contour_dist_px : sId("_min_contour_dist_px")
}; };
</script> </script>
<script src="/static/scanner/js/calibration.js"></script> <script src="/static/scanner/js/calibration.js"></script>