simulation

This commit is contained in:
2026-05-04 10:32:14 +02:00
parent ebdeb5f651
commit 3ecf0a1b6b
3 changed files with 554 additions and 497 deletions
+18 -35
View File
@@ -4,53 +4,36 @@
# A1..A6, B1..B6, C1..C6, D1..D6
#
PATH="data"
PATH="./media/simulation"
default_width=1000 # px
default_height=1000 # px
default_diameter=16.0 # mm
declare -A arguments=(
# key count len width sec fps seed thigmotaxis bg-color arena-color arena-border shadow-color body-color body-dark body-light head-color
["F0"]="1 0.90 0.30 60 5 64 0.45 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5A7896 #3C5570 #8CA0B4 #46645F"
)
# key count length width fps duration seed bg-color arena-color arena-border shadow-color body-color body-dark body-light head-color thresh-immobile thresh-mobile thigmotaxis photo-mode photo-strength photo-x photo-y photo-sine-freq photo-radius chemo-strength chemo-x chemo-y chemo-radius avoid-strength avoid-radius aggreg-strength aggreg-radius chem-repulsion chem-decay
["A1"]="3 0.40 0.30 5 60 64 #EBEBEB #FAFAFA #8C8C8C #C8C8C8 #A5A5A5 #373737 #D2D2D2 #828282 0.2 1.5 0.45 none 0.50 0.50 0.50 0.10 0.30 0.0 0.70 0.70 2.0 1.0 3.0 0.0 6.0 0.0 0.95"
["A2"]="1 0.42 0.32 5 60 96 #EBEBEB #FAFAFA #8C8C8C #C8C8C8 #A5A5A5 #373737 #D2D2D2 #828282 0.2 1.5 0.70 fixed 0.50 0.50 0.50 0.10 0.30 0.0 0.70 0.70 2.0 0.0 3.0 0.0 6.0 0.0 0.95"
["A3"]="1 0.50 0.40 5 60 128 #EBEBEB #FAFAFA #8C8C8C #C8C8C8 #A5A5A5 #373737 #D2D2D2 #828282 0.2 1.5 0.70 radial 0.50 0.50 0.50 0.10 0.30 0.5 0.70 0.70 2.0 0.0 3.0 0.0 6.0 0.0 0.95"
declare -A arguments2=(
# key count len width sec fps seed thigmotaxis bg-color arena-color arena-border shadow-color body-color body-dark body-light head-color
["D1"]="3 0.90 0.30 60 5 64 0.45 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5A7896 #3C5570 #8CA0B4 #46645F"
["D2"]="2 0.75 0.40 60 5 96 0.50 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5B7896 #3D5570 #8DA0B4 #47645F"
["D3"]="1 0.80 0.50 60 5 42 0.60 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["D4"]="1 0.85 0.40 60 5 28 0.70 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["D5"]="3 0.60 0.35 60 5 132 0.65 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["D6"]="2 0.65 0.35 60 5 256 0.85 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["C6"]="1 0.90 0.30 60 5 64 0.45 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5A7896 #3C5570 #8CA0B4 #46645F"
["C5"]="3 0.75 0.40 60 5 96 0.50 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5B7896 #3D5570 #8DA0B4 #47645F"
["C4"]="2 0.80 0.50 60 5 42 0.60 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["C3"]="1 0.85 0.40 60 5 28 0.70 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["C2"]="2 0.60 0.35 60 5 132 0.65 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["C1"]="3 0.65 0.35 60 5 256 0.85 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["B1"]="2 0.90 0.30 60 5 64 0.45 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5A7896 #3C5570 #8CA0B4 #46645F"
["B2"]="1 0.75 0.40 60 5 96 0.50 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5B7896 #3D5570 #8DA0B4 #47645F"
["B3"]="1 0.80 0.50 60 5 42 0.60 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["B4"]="3 0.85 0.40 60 5 28 0.70 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["B5"]="1 0.60 0.35 60 5 132 0.65 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["B6"]="2 0.65 0.35 60 5 256 0.85 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["A6"]="1 0.90 0.30 60 5 64 0.45 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5A7896 #3C5570 #8CA0B4 #46645F"
["A5"]="1 0.75 0.40 60 5 96 0.50 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5B7896 #3D5570 #8DA0B4 #47645F"
["A4"]="3 0.80 0.50 60 5 42 0.60 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["A3"]="1 0.85 0.40 60 5 28 0.70 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["A2"]="1 0.60 0.35 60 5 132 0.65 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
["A1"]="4 0.65 0.35 60 5 256 0.85 #D4DADC #BDBDF0 #B4AFA8 #BEBCB6 #5C7896 #3E5570 #8EA0B4 #48645F"
)
for key in "${!arguments[@]}"; do
args="${arguments[$key]}"
read -r count length width duration fps seed thigmotaxis bg_color arena_color arena_border shadow_color body_color body_dark body_light head_color <<< "$args"
read -r count length width fps duration seed bg_color arena_color arena_border shadow_color \
body_color body_dark body_light head_color thresh_immobile thresh_mobile thigmotaxis \
photo_mode photo_strength photo_x photo_y photo_sine_freq photo_radius chemo_strength chemo_x chemo_y chemo_radius \
avoid_strength avoid_radius aggreg_strength aggreg_radius chem_repulsion chem_decay <<< "$args"
echo "==== Exécution de $PATH/$key.mp4"
./planarian_sim.py --output "$PATH/$key.mp4" --default_width "$default_width" --default_height "$default_height" --default_diameter "$default_diameter" \
--count "$count" --length "$length" --width "$width" --duration "$duration" --fps "$fps" --seed "$seed" --thigmotaxis "$thigmotaxis" \
--bg-color "$bg_color" --arena-color "$arena_color" --arena-border "$arena_border" --shadow-color "$shadow_color" \
--body-color "$body_color" --body-dark "$body_dark" --body-light "$body_light" --head-color "$head_color" --no-csv
./planarian_sim.py --output "$PATH/$key.mp4" --default_width "$default_width" --default_height "$default_height" --default_diameter "$default_diameter" --no-csv \
--count "$count" --length "$length" --width "$width" --duration "$duration" --fps "$fps" --seed "$seed" \
--bg-color "$bg_color" --arena-color "$arena_color" --arena-border "$arena_border" --shadow-color "$shadow_color" \
--body-color "$body_color" --body-dark "$body_dark" --body-light "$body_light" --head-color "$head_color" \
--thresh-immobile "$thresh_immobile" --thresh-mobile "$thresh_mobile" --thigmotaxis "$thigmotaxis" \
--photo-mode "$photo_mode" --photo-strength "$photo_strength" --photo-x "$photo_x" --photo-y "$photo_y" --photo-sine-freq "$photo_sine_freq" --photo-radius "$photo_radius" \
--chemo-strength "$chemo_strength" --chemo-x "$chemo_x" --chemo-y "$chemo_y" --chemo-radius "$chemo_radius" \
--avoid-strength "$avoid_strength" --avoid-radius "$avoid_radius" --aggreg-strength "$aggreg_strength" --aggreg-radius "$aggreg_radius" \
--chem-repulsion "$chem_repulsion" --chem-decay "$chem_decay"
done
File diff suppressed because it is too large Load Diff
+97 -110
View File
@@ -1,9 +1,7 @@
#!../.venv/bin/python
"""
Planaire simulation de mouvement aléatoire
==========================================
Espace circulaire de 16mm de diamètre, 500x500px paramètrable
Planaria random movement simulation - top view
Espace circulaire de 16mm de diamètre, 500x500px
Supporte plusieurs planaires avec paramètres configurables via arguments CLI.
Export CSV par planaire compatible EthoVision XT.
@@ -13,55 +11,17 @@ Comportements simulés :
- Chimiotactisme : attraction vers une source de nourriture (--chemo-strength)
- Inter-individus : évitement de contact, agrégation, répulsion chimique
Seuils EthoVision par défaut (configurables en arguments) :
Immobile : déplacement < 0.2 mm/s
Mobile : 0.2 à 1.5 mm/s
Très mobile : > 1.5 mm/s
EthoVision CSV frames CSV summary
========== ========== ===========
movedCenter-pointTotalmm total_distance_mm movedCenter_pointTotal_mm
VelocityCenter-pointMeanmm/s velocity_mm_s velocity_mean_mm_s
MovementMoving moving, duration_moving_s movement_moving_duration_s
MovementNot Moving duration_stopped_s movement_not_moving_duration_s
ImmobileFrequency / Duration mobility_state mobility_immobile_frequency/duration_s
MobileFrequency / Duration mobility_state mobility_mobile_frequency/duration_s
Highly mobileFrequency / Duration mobility_state mobility_highly_mobile_frequency/duration_s
Métriques calculées :
- Distance totale parcourue (mm) → movedCenter-pointTotalmm
- Vitesse instantanée (mm/s) → VelocityCenter-pointMeanmm/s
- Durée cumulée en mouvement (s) → MovementMoving
- Durée cumulée à l'arrêt (s) → MovementNot Moving
- Fréquence et durée par état de mobilité → Mobility state (EthoVision)
- Distance à la paroi (mm) → thigmotactisme
Comportements simulés :
- Thigmotactisme : attraction vers la paroi (--thigmotaxis)
- Phototactisme : fuite de la lumière (--photo-mode, --photo-strength)
- Chimiotactisme : attraction vers une source de nourriture (--chemo-strength)
- Inter-individus : évitement de contact, agrégation, répulsion chimique
Usage:
python3 planarian_sim.py [options]
python3 planaire_sim.py [options]
Exemples:
python3 planarian_sim.py
python3 planarian_sim.py --count 5 --fps 25 --duration 20
python3 planarian_sim.py --count 3 --length 8.0 --width 1.2
python3 planarian_sim.py --bg-color "#E0DAD4" --arena-color "#F0EBE0"- -thigmotaxis 0.7
python3 planarian_sim.py --bg-color beige --arena-color ivory --shadow-color lightgray
python3 planarian_sim.py --bg-color beige --arena-color "#FAF0E0" --shadow-color "160 155 148"
python3 planarian_sim.py --count 5 --thigmotaxis 0.4
python3 planarian_sim.py --count 5 --photo-mode fixed --photo-x 0.2 --photo-y 0.2 --photo-strength 0.6
python3 planarian_sim.py --count 5 --chemo-x 0.7 --chemo-y 0.5 --chemo-strength 0.5
python3 planarian_sim .py --count 5 --avoid-strength 0.6 --aggreg-strength 0.2
python3 planaire_sim.py --count 5 --thigmotaxis 0.4
python3 planaire_sim.py --count 5 --photo-mode fixed --photo-x 0.2 --photo-y 0.2 --photo-strength 0.6
python3 planaire_sim.py --count 5 --chemo-x 0.7 --chemo-y 0.5 --chemo-strength 0.5
python3 planaire_sim.py --count 5 --avoid-strength 0.6 --aggreg-strength 0.2
"""
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'home.settings')
import csv
import cv2
@@ -72,11 +32,15 @@ except ImportError:
HAS_METRICS = False
import numpy as np
import math
import os
import random
import argparse
import re
from django.conf import settings
CSV_DIR = str(settings.MEDIA_ROOT / "simulation" / "planarian_sim_csv")
VIDEO_PATH = str(settings.MEDIA_ROOT / "simulation" / "planarian_simulation.mp4")
# ---------------------------------------------------------------------------
# Noms CSS courants → BGR
# ---------------------------------------------------------------------------
@@ -154,20 +118,19 @@ def parse_args():
# --- Paramètres vidéo ---
vg = parser.add_argument_group("Paramètres vidéo")
vg.add_argument("--fps", type=int, default=10, help="Images par seconde")
vg.add_argument("--duration", type=int, default=10, help="Durée en secondes")
vg.add_argument("--output", type=str, default="planaire_simulation.mp4",
help="Fichier vidéo de sortie")
vg.add_argument("--seed", type=int, default=42, help="Graine aléatoire")
vg.add_argument("--default_width", type=int, default=500, help="Image: largeur par défaut px")
vg.add_argument("--default_height", type=int, default=500, help="Image: hauteur par défautpx")
vg.add_argument("--default_diameter", type=float, default=16.0, help="Diamètre tube par défaut mm")
vg.add_argument("--fps", type=int, default=10, help="Images par seconde")
vg.add_argument("--duration", type=int, default=10, help="Durée en secondes")
vg.add_argument("--output", type=str, default=VIDEO_PATH, help="Fichier vidéo de sortie")
vg.add_argument("--seed", type=int, default=42, help="Graine aléatoire")
# --- Morphologie ---
pg = parser.add_argument_group("Morphologie du planaire")
pg.add_argument("--length", type=float, default=6.0, help="Longueur en mm")
pg.add_argument("--width", type=float, default=0.8, help="Largeur max en mm")
pg.add_argument("--length", type=float, default=1.0, help="Longueur en mm")
pg.add_argument("--width", type=float, default=0.35, help="Largeur max en mm")
pg.add_argument("--count", type=int, default=1, help="Nombre de planaires (1-20)")
# --- Thigmotactisme ---
@@ -239,7 +202,7 @@ def parse_args():
# --- Export CSV ---
eg = parser.add_argument_group("Export métriques")
eg.add_argument("--csv-dir", type=str, default=".", help="Répertoire de sortie CSV")
eg.add_argument("--csv-dir", type=str, default=CSV_DIR, help="Répertoire de sortie CSV")
eg.add_argument("--no-csv", action="store_true", help="Désactiver l'export CSV")
# --- Couleurs ---
@@ -248,21 +211,29 @@ def parse_args():
"Formats : #RRGGBB | R G B (RGB) | nom CSS (beige, tan, white…)"
)
kg.add_argument("--bg-color", nargs='+', action=ColorAction,
default=(212, 218, 220), metavar="COULEUR", help="Fond extérieur")
default=(235, 235, 235), metavar="COULEUR",
help="Fond extérieur (vue dessous, lumière transmise) $EBEBEB")
kg.add_argument("--arena-color", nargs='+', action=ColorAction,
default=(222, 228, 230), metavar="COULEUR", help="Intérieur de l'arène")
default=(250, 250, 250), metavar="COULEUR",
help="Intérieur arène — blanc éclairé par transmission $FAFAFA")
kg.add_argument("--arena-border", nargs='+', action=ColorAction,
default=(168, 175, 180), metavar="COULEUR", help="Bordure de l'arène")
default=(140, 140, 140), metavar="COULEUR",
help="Bordure arène $8C8C8C — légèrement plus sombre que l'arène")
kg.add_argument("--shadow-color", nargs='+', action=ColorAction,
default=(182, 188, 190), metavar="COULEUR", help="Ombre portée")
default=(200, 200, 200), metavar="COULEUR",
help="Ombre portée — très légère sous lumière transmise $C8C8C8")
kg.add_argument("--body-color", nargs='+', action=ColorAction,
default=(150, 120, 90), metavar="COULEUR", help="Corps principal")
default=(165, 165, 165), metavar="COULEUR",
help="Corps — gris translucide moyen $A5A5A5")
kg.add_argument("--body-dark", nargs='+', action=ColorAction,
default=(110, 85, 60), metavar="COULEUR", help="Pigmentation sombre")
default=(55, 55, 55), metavar="COULEUR",
help="Contour sombre net du corps $373737 — pour le contraste et la lisibilité")
kg.add_argument("--body-light", nargs='+', action=ColorAction,
default=(180, 160, 140), metavar="COULEUR", help="Reflet ventral")
default=(210, 210, 210), metavar="COULEUR",
help="Centre du corps — plus clair par transparence $D2D2D2")
kg.add_argument("--head-color", nargs='+', action=ColorAction,
default=(130, 100, 70), metavar="COULEUR", help="Tête")
default=(130, 130, 130), metavar="COULEUR",
help="Tête — légèrement plus sombre que le corps $828282 — pour la différencier du reste du corps")
args = parser.parse_args()
@@ -544,23 +515,27 @@ class Planaire:
self.width_px = max(3, int(cfg.planaire_width_px * random.uniform(0.75, 1.25)))
# --- Palette de couleur individuelle (5 familles naturalistes) ---
# Palettes grises — vue de dessous, lumière transmise par le dessus.
# Teinte uniforme gris moyen, seul le niveau de gris varie légèrement
# entre individus pour les distinguer visuellement.
PALETTES = [
{"body": ( 90, 120, 150), "dark": (55, 80, 105), "light": (140, 160, 180), "head": ( 65, 95, 125)},
{"body": ( 70, 110, 160), "dark": (45, 75, 120), "light": (120, 150, 185), "head": ( 50, 85, 140)},
{"body": ( 55, 80, 110), "dark": (35, 55, 80), "light": (100, 130, 155), "head": ( 40, 60, 90)},
{"body": (105, 118, 132), "dark": (70, 85, 98), "light": (150, 162, 172), "head": ( 85, 100, 115)},
{"body": ( 60, 115, 155), "dark": (40, 80, 115), "light": (110, 155, 185), "head": ( 45, 90, 135)},
{"body": (165, 165, 165), "dark": (50, 50, 50), "light": (210, 210, 210), "head": (130, 130, 130)},
{"body": (150, 150, 150), "dark": (45, 45, 45), "light": (200, 200, 200), "head": (118, 118, 118)},
{"body": (178, 178, 178), "dark": (58, 58, 58), "light": (218, 218, 218), "head": (142, 142, 142)},
{"body": (158, 158, 158), "dark": (48, 48, 48), "light": (205, 205, 205), "head": (125, 125, 125)},
{"body": (172, 172, 172), "dark": (55, 55, 55), "light": (215, 215, 215), "head": (138, 138, 138)},
]
palette = PALETTES[random.randint(0, len(PALETTES) - 1)]
def jitter(color, amount=12):
"""Ajoute une légère variation aléatoire à une couleur BGR."""
return tuple(max(0, min(255, c + random.randint(-amount, amount))) for c in color)
def jitter(color, amount=5):
"""Variation individuelle minimale — teinte grise très uniforme."""
v = random.randint(-amount, amount)
return tuple(max(0, min(255, c + v)) for c in color)
self.body_color = jitter(palette["body"])
self.body_dark = jitter(palette["dark"], 8)
self.body_light = jitter(palette["light"], 8)
self.head_color = jitter(palette["head"], 8)
self.body_dark = jitter(palette["dark"], 3)
self.body_light = jitter(palette["light"], 3)
self.head_color = jitter(palette["head"], 3)
self.shadow_color = tuple(cfg.shadow_color)
# --- Sensibilités individuelles (variation ±30% autour des valeurs globales) ---
@@ -927,39 +902,43 @@ class Planaire:
if n < 2:
return
shadow_offset = (2, 2)
# --- Vue de dessous, lumière transmise par le dessus ---
# Couche 1 : ombre très légère (décalée 1px) — lumière quasi-uniforme
for i in range(n - 1):
t = i / max(n - 1, 1)
w = max(1, int(self._body_width_at(t) * 0.85))
p1 = (int(self.body_history[i][0]) + shadow_offset[0],
int(self.body_history[i][1]) + shadow_offset[1])
p2 = (int(self.body_history[i+1][0]) + shadow_offset[0],
int(self.body_history[i+1][1]) + shadow_offset[1])
w = max(1, int(self._body_width_at(t)))
p1 = (int(self.body_history[i][0]) + 1,
int(self.body_history[i][1]) + 1)
p2 = (int(self.body_history[i+1][0]) + 1,
int(self.body_history[i+1][1]) + 1)
cv2.line(frame, p1, p2, self.shadow_color, w)
# Couche 2 : corps gris uniforme (teinte de base, sans gradient)
for i in range(n - 1):
t = i / max(n - 1, 1)
w = max(1, int(self._body_width_at(t)))
p1 = (int(self.body_history[i][0]), int(self.body_history[i][1]))
p2 = (int(self.body_history[i+1][0]), int(self.body_history[i+1][1]))
color = tuple(int(self.head_color[c] * (1-t) + self.body_light[c] * t) for c in range(3))
cv2.line(frame, p1, p2, color, w)
cv2.line(frame, p1, p2, self.body_color, w)
# Couche 3 : contour sombre net (liseré caractéristique vue de dessous)
# Dessiné en 2 passes : largeur w+2 (contour) puis w-2 (remplissage corps)
for i in range(n - 1):
t = i / max(n - 1, 1)
w = max(1, int(self._body_width_at(t)))
p1 = (int(self.body_history[i][0]), int(self.body_history[i][1]))
p2 = (int(self.body_history[i+1][0]), int(self.body_history[i+1][1]))
cv2.line(frame, p1, p2, self.body_dark, w + 2) # contour
cv2.line(frame, p1, p2, self.body_color, max(1, w - 1)) # remplissage
# Couche 4 : centre clair — lumière transmise au travers du corps
for i in range(n - 1):
t = i / max(n - 1, 1)
if 0.08 < t < 0.85:
w = max(1, int(self._body_width_at(t) * 0.28))
if 0.10 < t < 0.90:
w = max(1, int(self._body_width_at(t) * 0.35))
p1 = (int(self.body_history[i][0]), int(self.body_history[i][1]))
p2 = (int(self.body_history[i+1][0]), int(self.body_history[i+1][1]))
cv2.line(frame, p1, p2, self.body_dark, w)
for i in range(n - 1):
t = i / max(n - 1, 1)
if 0.15 < t < 0.75:
w = max(1, int(self._body_width_at(t) * 0.18))
p1 = (int(self.body_history[i][0]), int(self.body_history[i][1]))
p2 = (int(self.body_history[i+1][0]), int(self.body_history[i+1][1]))
cv2.line(frame, p1, p2, (160, 175, 190), w)
cv2.line(frame, p1, p2, self.body_light, w)
head = self.body_history[0]
neck = self.body_history[min(3, n - 1)]
@@ -972,14 +951,27 @@ class Planaire:
right_ear = (int(head[0] + math.cos(head_angle - 1.8) * lw),
int(head[1] + math.sin(head_angle - 1.8) * lw))
pts = np.array([tip, left_ear, right_ear], dtype=np.int32)
cv2.fillPoly(frame, [pts], self.head_color)
cv2.polylines(frame, [pts], True, self.body_dark, 1)
# Contour sombre net puis remplissage gris uniforme
cv2.fillPoly(frame, [pts], self.body_dark)
# Remplissage légèrement rétréci pour laisser le contour visible
inner_tip = (
int(head[0] + math.cos(head_angle) * (self.width_px * 0.3)),
int(head[1] + math.sin(head_angle) * (self.width_px * 0.3))
)
ilw = lw * 0.6
inner_l = (int(head[0] + math.cos(head_angle + 1.8) * ilw),
int(head[1] + math.sin(head_angle + 1.8) * ilw))
inner_r = (int(head[0] + math.cos(head_angle - 1.8) * ilw),
int(head[1] + math.sin(head_angle - 1.8) * ilw))
pts_inner = np.array([inner_tip, inner_l, inner_r], dtype=np.int32)
cv2.fillPoly(frame, [pts_inner], self.body_color)
eye_d = lw * 0.6
# Yeux (photorécepteurs) : points sombres nets
eye_d = lw * 0.55
for side in [1.3, -1.3]:
ex = int(head[0] + math.cos(head_angle + side) * eye_d * 0.7)
ey = int(head[1] + math.sin(head_angle + side) * eye_d * 0.7)
cv2.circle(frame, (ex, ey), max(1, self.width_px // 5), (30, 40, 50), -1)
ex = int(head[0] + math.cos(head_angle + side) * eye_d * 0.65)
ey = int(head[1] + math.sin(head_angle + side) * eye_d * 0.65)
cv2.circle(frame, (ex, ey), max(1, self.width_px // 6), self.body_dark, -1)
# ---------------------------------------------------------------------------
@@ -1152,14 +1144,9 @@ def main():
np.random.seed(args.seed)
# --- Constantes dérivées ---
#WIDTH, HEIGHT = 500, 500
#TOTAL_FRAMES = args.fps * args.duration
#MM_TO_PX = 420 / 16.0 # ~26.25 px/mm
WIDTH, HEIGHT = args.default_width, args.default_height
WIDTH, HEIGHT = 500, 500
TOTAL_FRAMES = args.fps * args.duration
MM_TO_PX = (args.default_width - 80) / args.default_diameter # ~26.25 px/mm
MM_TO_PX = 420 / 16.0 # ~26.25 px/mm
ARENA_RADIUS_PX = int(8 * MM_TO_PX)
ARENA_CENTER = (WIDTH // 2, HEIGHT // 2)