Files
vitrine-pixels/8x32 PI/outils/verifier_animation.py
T
bachandClaude Opus 5.5 2d4b0d5a5f 8x32 PI : interface web, matrice verticale, compatibilité Raspbian 11
- Interface web (serveur_web.py, Flask, gabarits dans web/) : dépôt de GIF,
  PNG animés et séquences de PNG, aperçus animés, ordre de lecture,
  activer / désactiver, corbeille, « jouer maintenant » et « suivante ».
  Service systemd vitrine-web, activé par installer.sh.
- Lecteur : réagit aux demandes de la page web (post-it dans
  /run/vitrine-pixels), écrit l'animation en cours, éteint la matrice
  proprement sur SIGTERM.
- Matrice posée debout : réglage ROTATION (90 = image 8 × 32) dans
  matrice.py ; images couchées tournées automatiquement ; exemples,
  animations codées, aperçus et page web adaptés au format vertical.
- Compatibilité Raspbian 11 Bullseye : partition /boot, Pillow 8.1, Flask 1.1.
- Doc : nouveau guide INTERFACE_WEB.md ; README, GUIDE_KRITA, MODE_VITRINE,
  CODER_EN_PYTHON mis à jour (format 8 × 32, adresses Wi-Fi / câble du Pi).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:08:18 +02:00

196 lines
7.9 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 = 24 # 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 : toutes les images, numérotées. Matrice couchée : les unes sous
# les autres ; matrice debout : côte à côte, comme une bande dessinée.
montrees = images[:MAX_PLANCHE]
l, h = images[0].size
if h > l:
planche = Image.new("RGB", (len(montrees) * (l + 8), h + 24), (255, 255, 255))
crayon = ImageDraw.Draw(planche)
for i, im in enumerate(montrees):
planche.paste(im, (i * (l + 8), 24))
crayon.text((i * (l + 8) + l // 2 - 6, 6), str(i + 1), fill=(0, 0, 0))
else:
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()