Video plate capture: calibration, edge enhance, auto-detect well borders

This commit is contained in:
2026-06-03 17:56:23 +02:00
parent 4b42c03756
commit 9bb8fc1bce
58 changed files with 1699 additions and 274 deletions
+107 -4
View File
@@ -1,4 +1,6 @@
from pathlib import Path
from django.utils.translation import gettext_lazy as _
from django.utils.html import format_html
from django.contrib import admin
from django.db.models import Q
from . import models
@@ -43,14 +45,14 @@ class ConfigurationAdmin(admin.ModelAdmin):
class MultiWellAdmin(admin.ModelAdmin):
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', 'capture_video', 'active',)
ordering = ('label', 'order')
fieldsets = (
(_("Identification"), {
"fields": ("label", "author", "position", "default", "active"),
"fields": ("label", "author", "position", "default", "capture_video", "active"),
}),
(_("Géométrie"), {
"fields": ("cols", "rows", "diameter", "row_def", "row_order"),"classes": ("collapse",),
"fields": ("cols", "rows", "diameter", "crop_radius", "row_def", "row_order"),"classes": ("collapse",),
}),
(_("Déplacement"), {
"fields": ("order", "duration", "xbase", "ybase", "dx", "dy", "feed"),"classes": ("collapse",),
@@ -113,6 +115,107 @@ class SessionAdmin(admin.ModelAdmin):
'scanning_finished_at'
)
@admin.register(models.VideoPlate)
class VideoPlateAdmin(admin.ModelAdmin):
list_display = ['multiwell', 'label', 'video_filename', 'active',
'fps_display', 'duration_display', 'resolution_display', 'uploaded_at']
list_filter = ['multiwell', 'active']
list_editable = ['active']
readonly_fields = [
'native_fps', 'duration_s', 'frame_w', 'frame_h',
'uploaded_at', 'resolution_display', 'video_preview',
]
fields = [
'multiwell', 'label', 'video_file', 'active', 'px_per_mm',
'x_origin_mm', 'y_origin_mm',
'video_preview',
'native_fps', 'duration_s', 'frame_w', 'frame_h', 'uploaded_at',
]
class Media:
css = {'all': ('scanner/css/video_upload.css',)}
js = ('scanner/js/video_upload.js',)
# ------------------------------------------------------------------
# Colonnes liste
# ------------------------------------------------------------------
@admin.display(description=_("Fichier"), ordering='video_file')
def video_filename(self, obj):
return obj.video_filename
@admin.display(description=_("FPS"))
def fps_display(self, obj):
return f"{obj.native_fps:.2f}" if obj.native_fps else ""
@admin.display(description=_("Durée"))
def duration_display(self, obj):
if not obj.duration_s:
return ""
m, s = divmod(int(obj.duration_s), 60)
return f"{m}:{s:02d}"
@admin.display(description=_("Résolution"))
def resolution_display(self, obj):
return obj.resolution
# ------------------------------------------------------------------
# Aperçu vidéo (readonly field)
# ------------------------------------------------------------------
@admin.display(description=_("Aperçu"))
def video_preview(self, obj):
if not obj.video_file:
return ""
return format_html(
'<video src="{}" controls style="max-width:480px;max-height:320px;'
'border-radius:4px;"></video>',
obj.video_file.url,
)
# ------------------------------------------------------------------
# Sauvegarde : extraction des métadonnées
# ------------------------------------------------------------------
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
if obj.video_file:
self._extract_metadata(obj)
def _extract_metadata(self, obj):
try:
import cv2
cap = cv2.VideoCapture(obj.video_file.path)
if cap.isOpened():
fps = cap.get(cv2.CAP_PROP_FPS)
fc = cap.get(cv2.CAP_PROP_FRAME_COUNT)
models.VideoPlate.objects.filter(pk=obj.pk).update(
native_fps = fps,
duration_s = (fc / fps) if fps else None,
frame_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
frame_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
)
cap.release()
except Exception:
pass
# ------------------------------------------------------------------
# Suppression : efface aussi le fichier physique
# ------------------------------------------------------------------
def delete_model(self, request, obj):
if obj.video_file:
Path(obj.video_file.path).unlink(missing_ok=True)
super().delete_model(request, obj)
def delete_queryset(self, request, queryset):
for obj in queryset:
if obj.video_file:
Path(obj.video_file.path).unlink(missing_ok=True)
super().delete_queryset(request, queryset)
admin.site.register(models.Configuration, ConfigurationAdmin)
admin.site.register(models.Well, WellAdmin)
admin.site.register(models.MultiWell, MultiWellAdmin)