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
+49 -1
View File
@@ -1,18 +1,65 @@
from django.utils.translation import gettext_lazy as _
from django.contrib import admin
from django.db.models import Q
from . import models
class WellAdmin(admin.ModelAdmin):
model = models.Well
list_display = ('name', 'author',)
class ConfigurationAdmin(admin.ModelAdmin):
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):
list_filter = ('author', )
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):
list_filter = ('author', 'multiwell')
@@ -28,6 +75,7 @@ class ExperimentAdmin(admin.ModelAdmin):
list_filter = ('session_experiments__session', 'author', )
list_display = ('title', 'author', 'multiwell', 'created', 'started', 'finished')
readonly_fields = ('created', 'started', 'finished', )
class SessionExperimentInlineAdmin(admin.TabularInline):
model = models.SessionExperiment
+6 -1
View File
@@ -28,8 +28,13 @@ class DefaultConfig:
calibration_default_step: float = 1.0
calibration_default_duration: float = 3.0
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:
def __init__(self):
+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)
# tracking
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)
@@ -97,18 +104,18 @@ class Well(models.Model):
class MultiWell(models.Model):
# Identification
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)
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)
# Configuration
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)
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_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)
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):
self.process = process
self.cnc_controller = process.grbl
self.stop_playing = Event()
self.well_iterator = None
self.multiwel = None
@@ -85,6 +86,15 @@ class MultiWellManager:
self.set_multiwell()
self.scan_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):
self._feed = feed or self.process.conf.calibration_default_feed
+7 -1
View File
@@ -467,7 +467,13 @@ class ScannerProcess(Task):
elif topic == 'set_well':
msg = self.manager.set_well_position()
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(
xbase=self.manager.xbase,
ybase=self.manager.ybase,
@@ -10,5 +10,3 @@
align-items: center;
}
@@ -42,6 +42,13 @@ class ScannerManager {
this.crop = options.crop;
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;
}
init_controls() {
@@ -71,6 +78,13 @@ class ScannerManager {
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.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) {
@@ -133,12 +133,12 @@
</div>
<div class="w3-row w3-row-padding">
<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>
</button>
</div>
<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>
</button>
</div>
@@ -148,7 +148,32 @@
</button>
</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">
<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' %}"
@@ -227,7 +252,12 @@
well_btn : sId("_well_btn"),
median : sId("_median"),
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 src="/static/scanner/js/calibration.js"></script>