This commit is contained in:
bach
2026-10-08 15:25:14 +02:00
parent 32e617f67b
commit da4fac3af7
64 changed files with 3921 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""
=============================================================================
VÉRIFIER UNE ANIMATION avant de l'envoyer sur la matrice
Workshop « Vitrine pixels »
=============================================================================
Pour l'équipe design : ce programme examine un export de Krita et dit
s'il est prêt pour la matrice. Il fonctionne sur n'importe quel ordinateur
(il suffit de Python et de Pillow), sans la matrice.
Usage :
python3 outils/verifier_animation.py mon_animation.gif
python3 outils/verifier_animation.py dossier_de_png/
python3 outils/verifier_animation.py animations/ (tout le dossier)
Il affiche un petit rapport :
- taille de l'image, nombre d'images, durée, vitesse ;
- nombre de couleurs (beaucoup de couleurs = souvent de l'anticrénelage,
ces pixels « flous » qui adoucissent les bords dans Krita) ;
- pixels trop sombres, qui seront ÉTEINTS sur la matrice ;
- courant consommé, pour vérifier que l'alimentation suit.
Et il fabrique, dans le dossier « apercus », deux APERÇUS « façon LED » :
- mon_animation_apercu.png : planche avec toutes les images ;
- mon_animation_apercu.gif : l'animation agrandie, à partager.
(Pas dans le dossier des animations : le lecteur les jouerait aussi !)
"""
import os
import sys
from PIL import Image, ImageDraw
# On réutilise le code du lecteur et du moteur (dossier parent).
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import lecteur # noqa: E402
from matrice import LARGEUR, HAUTEUR, TABLE_GAMMA, LUMINOSITE, courant_estime_ma, COURANT_MAX_MA, pixels_de # noqa: E402
DOSSIER_APERCUS = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "apercus")
TAILLE_LED = 14 # taille d'un pixel dans l'aperçu, en points
MAX_PLANCHE = 48 # nombre maximum d'images sur la planche
def pixel_eteint_sur_matrice(r, v, b):
"""Vrai si ce pixel, allumé dans l'image, sera éteint sur la matrice
(une fois la correction gamma et la luminosité appliquées)."""
if (r, v, b) == (0, 0, 0):
return False
return all(TABLE_GAMMA[c] * LUMINOSITE // 255 == 0 for c in (r, v, b))
def dessiner_leds(image, taille=TAILLE_LED):
"""Agrandit une image 32 × 8 en dessinant un rond par pixel."""
sortie = Image.new("RGB", (LARGEUR * taille, HAUTEUR * taille), (12, 12, 12))
crayon = ImageDraw.Draw(sortie)
for y in range(HAUTEUR):
for x in range(LARGEUR):
couleur = image.getpixel((x, y))
if couleur == (0, 0, 0):
couleur = (30, 30, 30) # LED éteinte : gris très sombre
marge = max(1, taille // 7)
crayon.ellipse((x * taille + marge, y * taille + marge,
(x + 1) * taille - marge, (y + 1) * taille - marge), fill=couleur)
return sortie
def verifier(chemin):
nom = os.path.basename(os.path.normpath(chemin))
print("=" * 70)
print(nom)
print("=" * 70)
# Taille d'origine (avant adaptation par le lecteur).
if os.path.isdir(chemin):
fichiers = lecteur.images_du_dossier(chemin)
if not fichiers:
print(" ✗ aucun fichier image dans ce dossier")
return
with Image.open(fichiers[0]) as premiere:
taille = premiere.size
print(f" Type : séquence de {len(fichiers)} fichiers "
f"({lecteur.lire_vitesse(chemin):g} images/s)")
else:
with Image.open(chemin) as fichier:
taille = fichier.size
print(f" Type : {fichier.format}, "
f"{'animé' if getattr(fichier, 'is_animated', False) else 'image fixe'}")
etapes = lecteur.charger(chemin)
duree = sum(d for _, d in etapes)
# --- Taille ---
if taille == (LARGEUR, HAUTEUR):
print(f" Taille : {taille[0]} × {taille[1]} ✓")
elif taille[0] % LARGEUR == 0 and taille[0] // LARGEUR == taille[1] // HAUTEUR \
and taille[1] % HAUTEUR == 0:
print(f" Taille : {taille[0]} × {taille[1]} ✓ (agrandissement × "
f"{taille[0] // LARGEUR}, sera réduit)")
else:
print(f" Taille : {taille[0]} × {taille[1]} ⚠ attendu {LARGEUR} × {HAUTEUR} "
"(l'image sera centrée et coupée)")
# --- Durée ---
if len(etapes) > 1:
print(f" Images : {len(etapes)}, durée {duree:.2f} s "
f"(≈ {len(etapes) / duree:.1f} images/s)")
else:
print(" Images : 1 (image fixe)")
# --- Couleurs ---
couleurs = set()
trop_sombres = 0
for image, _ in etapes:
for c in pixels_de(image):
couleurs.add(c)
for c in couleurs:
if pixel_eteint_sur_matrice(*c):
trop_sombres += 1
alerte = " ⚠ beaucoup ! anticrénelage ou dégradé flou ?" if len(couleurs) > 64 else " ✓"
print(f" Couleurs : {len(couleurs)} différentes{alerte}")
if trop_sombres:
print(f" Trop sombre : {trop_sombres} couleur(s) seront ÉTEINTES sur la matrice "
f"(à luminosité {LUMINOSITE}) ⚠")
else:
print(" Trop sombre : aucune ✓")
# --- Courant ---
courants = [courant_estime_ma(image) for image, _ in etapes]
maxi = max(courants)
moyen = sum(courants) / len(courants)
limite = " ⚠ limité par le logiciel (image assombrie)" if maxi >= COURANT_MAX_MA - 1 else " ✓"
print(f" Courant : moyen {moyen / 1000:.2f} A, maximum {maxi / 1000:.2f} A{limite}")
# --- Aperçus ---
os.makedirs(DOSSIER_APERCUS, exist_ok=True)
base = os.path.join(DOSSIER_APERCUS, os.path.splitext(nom)[0] + "_apercu")
images = [dessiner_leds(image) for image, _ in etapes]
# Planche : les images les unes sous les autres, numérotées.
montrees = images[:MAX_PLANCHE]
l, h = images[0].size
planche = Image.new("RGB", (l + 50, len(montrees) * (h + 6)), (255, 255, 255))
crayon = ImageDraw.Draw(planche)
for i, im in enumerate(montrees):
planche.paste(im, (50, i * (h + 6)))
crayon.text((6, i * (h + 6) + h // 2 - 6), str(i + 1), fill=(0, 0, 0))
planche.save(base + ".png")
print(f" Aperçu : {base}.png", end="")
if len(images) > 1:
durees = [max(20, round(d * 1000)) for _, d in etapes]
images[0].save(base + ".gif", save_all=True, append_images=images[1:],
duration=durees, loop=0)
print(f" et {base}.gif")
else:
print()
def est_sequence(dossier):
"""Vrai si le dossier ne contient que des PNG (et aucun sous-dossier)."""
noms = [n for n in os.listdir(dossier) if not lecteur.est_cache(n)
and n != "images_par_seconde.txt"]
return bool(noms) and all(n.lower().endswith(".png") and
os.path.isfile(os.path.join(dossier, n)) for n in noms)
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
for chemin in sys.argv[1:]:
# Un dossier peut être une SÉQUENCE (que des PNG, pas de sous-dossier)
# ou un dossier d'animations ENTIER : on vérifie alors chaque élément.
if os.path.isdir(chemin) and not est_sequence(chemin):
cibles = lecteur.lister_animations(chemin)
else:
cibles = [chemin]
for cible in cibles:
try:
verifier(cible)
except Exception as erreur:
print(f" ✗ impossible de lire {cible} : {erreur}")
if __name__ == "__main__":
main()