calibration
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,22 +97,31 @@ 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._metrics = self._params.build_metrics()
|
self._params = ExperimentParams(cfg.to_params_dict())
|
||||||
self._tracker = PlanarianTracker(
|
self._metrics = self._params.build_metrics()
|
||||||
tube_axis = self._params.tube_axis,
|
self._tracker = PlanarianTracker(
|
||||||
min_area_px = self._params.min_area_px,
|
tube_axis = self._params.tube_axis,
|
||||||
max_area_ratio = self._params.max_area_ratio,
|
min_area_px = self._params.min_area_px,
|
||||||
max_planarians = self._params.planarian_count,
|
max_area_ratio = self._params.max_area_ratio,
|
||||||
)
|
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)
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -171,10 +162,12 @@ class PlanarianTracker:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
tube_axis: str = "vertical",
|
tube_axis: str = "vertical",
|
||||||
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:
|
||||||
@@ -182,7 +175,11 @@ class PlanarianTracker:
|
|||||||
min_area_px : surface minimale d'un contour pour être considéré valide (px²)
|
min_area_px : surface minimale d'un contour pour être considéré valide (px²)
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,18 +1,65 @@
|
|||||||
|
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',)
|
||||||
|
|
||||||
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')
|
||||||
@@ -28,6 +75,7 @@ class ExperimentAdmin(admin.ModelAdmin):
|
|||||||
list_filter = ('session_experiments__session', 'author', )
|
list_filter = ('session_experiments__session', 'author', )
|
||||||
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
|
||||||
|
|||||||
@@ -28,8 +28,13 @@ 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:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -467,7 +467,13 @@ class ScannerProcess(Task):
|
|||||||
elif topic == 'set_well':
|
elif topic == 'set_well':
|
||||||
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,7 +148,32 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-row w3-row-padding ">
|
{% 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-half w3-margin-top">
|
<div class="w3-half w3-margin-top">
|
||||||
<span>{% trans 'Rayon' %} px</span><br>
|
<span>{% trans 'Rayon' %} px</span><br>
|
||||||
<input id="_crop_radius" type="number" min="100" max="1200" step="1" class="w3-round w3-input" title="{% trans 'Rayon du recadrement' %}"
|
<input id="_crop_radius" type="number" min="100" max="1200" step="1" class="w3-round w3-input" title="{% trans 'Rayon du recadrement' %}"
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user