8x32 PI
This commit is contained in:
Executable
+187
@@ -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()
|
||||
Reference in New Issue
Block a user