Let’s Build : générateur de GIF de disque en rotation
Lors de mon arrivée dans l’équipe support de Transloadit, je me familiarisais avec nos services lorsque j’ai découvert une question Stack Overflow qui a piqué ma curiosité, car elle me permettait d’associer ma passion pour la musique à notre API. La question était de savoir si nos services pouvaient servir à générer l’animation d’un disque qui tourne, et j’ai trouvé l’idée suffisamment intéressante pour en faire un article de blog.

Cet article aborde plusieurs pratiques intéressantes que vous voudrez peut-être utiliser dans vos projets et montre la polyvalence du service Transloadit. Nous verrons notamment comment utiliser notre SDK Python avec des bibliothèques externes pour combiner encodage interne et externe, comment combiner de manière asynchrone deux Templates, et comment utiliser FFmpeg pour étendre les capacités de notre API. Alors, sans plus attendre, entrons dans le vif du sujet !
Prérequis
Outre notre SDK Python mentionné plus haut, nous devons télécharger quelques autres outils pour notre projet du jour. Nous utiliserons OpenCV pour effectuer des opérations bit à bit, en tandem avec NumPy pour faciliter le traitement d’images en local. Les deux paquets seront installés à l’aide de l’installateur de paquets pip.
En plus de ces paquets, nous avons besoin de quelques ressources, que vous pouvez télécharger via les liens ci-dessous :
Nous allons découper le code en morceaux pour les besoins des explications de cet article, mais si vous souhaitez télécharger le projet complet, consultez ce dépôt GitHub.
Configuration du projet
Il nous faut un dossier de projet contenant un nouveau fichier Python et un dossier Assets/. Placez les
images téléchargées dans Assets/, en nommant la pochette d’album okcomputer.jpg et l’arrière-plan du disque vinyl.png.
Nous pouvons maintenant installer tous les paquets nécessaires. Pour cela, exécutez les commandes
suivantes dans votre console :
python3 -m venv .venv
source .venv/bin/activate
pip install pytransloadit
pip install numpy
pip install opencv-python
pip install requests
pip install future
Créez un nouveau fichier Python et importez nos paquets.
from __future__ import print_function
from transloadit import client
import os
import requests
import cv2
import numpy as np
Ensuite, créez une instance du client Transloadit.
tl = client.Transloadit(os.environ['TRANSLOADIT_KEY'], os.environ['TRANSLOADIT_SECRET'])
Attribuez à ces deux variables d’environnement l’Auth Key et l’Auth Secret de votre Workspace. Exécutez le script en local ou sur un serveur de confiance ; ne distribuez pas le secret aux utilisateurs finaux.
Pour terminer, créons un ensemble de variables globales pour plus tard. Assurez-vous que img_name
correspond au nom de l’image de l’album et que vinyl_path correspond au nom de l’image du disque vinyle.
img_name = 'okcomputer'
img_path = 'Assets/{name}.jpg'.format(name=img_name)
vinyl_path = 'Assets/vinyl.png'
remove_bg_location = 'Assets/trimmed_image.png'
Nous sommes maintenant prêts à créer notre premier Template !
Les Templates
Rendez-vous dans votre console Transloadit et créez un Template vide. Notez bien votre ID de Template pour plus tard. Ci-dessous, nous avons copié le code de notre Template, qui n’est autre que la recette JSON de votre magie Transloadit. Nous allons passer en revue chaque Step ci-dessous et expliquer ce qui se passe.
{
"steps": {
":original": {
"robot": "/upload/handle"
},
"resize": {
"use": ":original",
"robot": "/image/resize",
"format": "png",
"resize_strategy": "fillcrop",
"width": 350,
"height": 350,
"imagemagick_stack": "v3.0.1"
}
}
}
Notre premier Step :original gère l’envoi de notre fichier d’entrée. Si vous souhaitez
importer un fichier depuis un emplacement tiers, vous pouvez consulter tous nos
services pris en charge.
Ensuite, resize se charge de recadrer notre image et définit le format PNG. C’est nécessaire pour
que l’image soit correctement mise à l’échelle sur le disque vinyle à la fin.
Utiliser nos Templates
Comme nous utilisons plusieurs Templates, nous devons créer un petit script Python pour les relier entre eux. Pour cela, nous analysons le résultat de l’Assembly de notre premier Template, puis nous récupérons la valeur d’URL temporaire obtenue et la transmettons à notre second Template.
La fonction ci-dessous reçoit l’ID de Template noté précédemment, le chemin du fichier
de notre image et le nom du dernier Step de notre Template (dans notre exemple,
ce serait resize). Elle comporte aussi quelques paramètres facultatifs, comme le choix de renvoyer
ou non l’URL et, surtout, fields. Grâce au paramètre fields, nous pouvons transmettre des variables
à notre Template afin de modifier les paramètres de notre Template directement
depuis notre client 👌
def useTemplate(templateID, file_path='', result_name='', get_url=True, fields=None):
assembly = tl.new_assembly({'template_id': templateID, 'fields': fields or {}})
if file_path:
with open(file_path, 'rb') as upload:
assembly.add_file(upload)
assembly_response = assembly.create(retries=5, wait=True)
else:
assembly_response = assembly.create(retries=5, wait=True)
if get_url:
if assembly_response.data.get('ok') != 'ASSEMBLY_COMPLETED':
raise RuntimeError('Assembly did not complete successfully')
assembly_url = assembly_response.data.get('results').get(result_name)[0].get('ssl_url')
return assembly_url
Nous devons aussi créer une fonction utilitaire supplémentaire pour télécharger notre image
redimensionnée dans le dossier Assets/.
def downloadImage(url, location):
r = requests.get(url, timeout=30)
r.raise_for_status()
with open(location, 'wb') as image:
image.write(r.content)
Supprimer l’arrière-plan
Ce Template est simple : il active le canal alpha de notre image PNG, puis rend transparents tous les pixels noirs. Cela signifie toutefois qu’il faut faire attention lorsqu’on utilise une pochette d’album à fond noir, comme Dark Side of the Moon de Pink Floyd.
{
"steps": {
":original": {
"robot": "/upload/handle"
},
"trimmed": {
"use": ":original",
"robot": "/image/resize",
"alpha": "Activate",
"type": "TrueColor",
"transparent": "0,0,0",
"imagemagick_stack": "v3.0.1"
}
}
}
Superposer l’image
Place maintenant à la magie des champs ✨. Nous allons prendre l’URL de l’image issue de notre dernière Assembly et la transmettre comme champ à notre Template. Le Robot /image/resize utilisera l’image comme filigrane pour la superposer à notre disque vinyle.
{
"steps": {
":original": {
"robot": "/upload/handle"
},
"watermark": {
"use": ":original",
"robot": "/image/resize",
"watermark_url": "${fields.url}",
"watermark_size": "33%",
"watermark_position": "center",
"imagemagick_stack": "v3.0.1"
}
}
}
En faire un GIF !
Voici notre dernier Template. Il peut sembler intimidant, mais il est assez simple une fois décomposé.
{
"steps": {
"import": {
"robot": "/http/import",
"url": "${fields.url}",
"result": true
},
"animated": {
"robot": "/video/merge",
"use": "import",
"result": true,
"duration": "${fields.duration}",
"framerate": "${fields.framerate}",
"ffmpeg_stack": "v7",
"ffmpeg": {
"vf": "rotate=PI*t:c=white",
"f": "gif",
"pix_fmt": "rgb24",
"loop": "0"
}
}
}
}
Le premier Step import sert, eh bien, à importer notre fichier. Nous utilisons notre
Robot /http/import pour éviter de télécharger en local
l’image issue de l’Assembly précédente. Il faut pour cela une URL à partir de laquelle
importer. Heureusement, nous pouvons facilement utiliser le champ que nous avons créé pour
transmettre une URL depuis notre script Python.
C’est ici que nous utilisons FFmpeg pour créer l’effet de rotation souhaité. Nous appliquons
l’option vidéo rotate à l’image superposée, en définissant l’angle en radians comme pi multiplié
par le temps écoulé t en secondes. Cela donne un tour complet toutes les deux secondes. c=white
remplit de blanc l’arrière-plan découvert. L’option de sortie GIF loop=0 fait se répéter
l’animation finale indéfiniment ; elle est distincte du filtre vidéo.
Tout assembler en Python
Nous devrions maintenant avoir toutes les pièces nécessaires pour créer notre GIF de vinyle en rotation !
Commençons par redimensionner l’image et la télécharger en local :
resize_url = useTemplate('RESIZE_IMAGE_TEMPLATE_ID', img_path, 'resize')
resized_image_location = 'Assets/resized_image.png'
downloadImage(resize_url, resized_image_location)
Ensuite, nous devons créer notre fonction de masquage d’image. Appelons-la maskImage et donnons-lui
un argument img_path.
def maskImage(img_path):
img = cv2.imread(img_path)
mask = np.zeros(img.shape, dtype=np.uint8)
mask = cv2.circle(mask, (175, 175), 175, (255, 255, 255), -1)
mask = cv2.circle(mask, (175, 175), 20, (0, 0, 0), -1)
result = cv2.bitwise_and(img, mask)
result_location = 'Assets/mask.png'
cv2.imwrite(result_location, result)
remove_bg_url = useTemplate('REMOVING_BG_TEMPLATE_ID', result_location, 'trimmed')
downloadImage(remove_bg_url, remove_bg_location)
return remove_bg_url
Le début de notre fonction crée une image noire aux dimensions exactement identiques à celles de notre pochette d’album, puis crée deux cercles blancs pour produire un anneau, comme ceci :

Nous effectuons maintenant une opération ET bit à bit sur le masque et sur notre image d’origine : un pixel de l’image d’origine n’est donc affiché qu’aux endroits où le masque comporte un pixel blanc, ce qui nous donne le résultat suivant :

Enfin, nous utilisons notre Template précédent pour rendre transparents les pixels noirs.

Nous appelons notre fonction comme ceci :
trimmed_url = maskImage(resized_image_location)
Nous devons maintenant placer notre anneau sur le disque vinyle et le faire tourner ! Appliquons le filigrane à l’image à l’aide de notre Template de tout à l’heure :
vinyl_url = useTemplate('WATERMARK_IMAGE_TEMPLATE_ID', vinyl_path, 'watermark', True, {'url': trimmed_url})

En transmettant maintenant l’image fixe à notre Template de génération de GIF, avec une fréquence d’images et une durée que nous définissons dans notre script, nous pouvons la faire tourner !
frame_rate = 60
length = 2
final_gif_url = useTemplate('ROTATING_GIF_TEMPLATE_ID',
result_name='animated',
get_url=True,
fields={'url': vinyl_url, 'duration': length, 'framerate': frame_rate})
Enfin, nous pouvons télécharger le résultat en local.
final_gif_location = 'Assets/finished_gif.gif'
downloadImage(final_gif_url, final_gif_location)
Et voilà, nous y sommes enfin : notre propre disque vinyle animé en rotation !🎉

J’espère que vous avez pris plaisir à réaliser ce projet, et peut-être même appris quelques astuces sur l’API Transloadit que vous pourrez utiliser dans votre prochaine aventure ! Et si vous souhaitez aller encore plus loin, pourquoi ne pas en faire un MP4 et ajouter de la musique pour accompagner le disque, ou utiliser Uppy pour permettre aux utilisateurs d’envoyer leurs propres images à placer sur un disque vinyle en rotation ?
