Video plate capture: calibration, edge enhance, auto-detect well borders
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user