Utilisateur:Pierreetheve
Page de Pierre ETHEVE
B3Q2
Le but de ce projet est de créer une base de données de séquences d'animation dans le but d'en faire une base d'expérimentation de différentes approches de génération d'animations par deep learning. Constatant mon manque de données, j'ai décidé de consacrer mon semestre à la mise en place d'une structure solide de collecte de données.
BG_detector
Je suis parti du principe qu'en arrivant à identifier et à extraire les parties animées d'un épisode d'animation en laissant de coté le décor, je pourrais constituer assez facilement un corpus de données intéressant. J'ai donc écrit un programme utilisant les capacités d'OpenCV pour tenter de localiser les parties mouvantes d'une scène, et de l'isoler de l'arrière plan, même lors de mouvement de caméra faibles.
J'ai pour cela suivi un protocole qui revient dans les grandes lignes à cela :
1) Faire une première analyse grossière du mouvement, basée sur la différence pixel à pixel entre les images.
2) Utiliser une technique de seuillage du taux de changement frame à frame pour détecter les changements de scène.
Puis pour chaque scène :
3) Faire un tracking de points d'intérêts grâce à l'implémentation du flux optique d'OpenCV.
4) Trier les points en fonction de leur trajectoire : des points avec une trajectoire linéaires dans le temps (qui restent immobiles ou se déplacent en ligne droite) sont considérés comme potentiellement dûs au mouvement de caméra, et exclus de l'analyse.
5) Répéter les étapes 3 et 4 avec un défilement inverse des images (permet de tracker des points qui ne seraient pas présents dans la première image).
6) Agréger tous les points issus de trajectoires irrégulières (donc probalement dûs à un mouvement animé), et en déduire une zone rectangulaire d'intérêt.
Pour chaque zone d'intérêt :
7) Faire un nouveau tracking des points, et repérer les frames où ils sont en mouvement. Eliminer toutes les images où le personnage ne fait aucun mouvement notable. Cela élimine les pauses, et permet de n'enregistrer que l'information importante (~60% de réduction de la masse de donnée après cette étape).
8) Enregistrer la zone pour chaque instant où un mouvement a été détecté, sous forme de séquence de PNGs.
Un aperçu de l'exécution du processus (sans l'étape 5, ajoutée après) : https://youtu.be/p8s0ym5ERck
Code source du programme disponible ici : https://drive.google.com/drive/folders/140W08E-Op49OVjXbV0cBRNIwxJX2ryoZ?usp=sharing
Labellisation
Une fois le script exécuté, on obtient une grosse masse de données brutes : une série de dossiers contenant les séquences d'images pour chaque zones d'intérêt. Il faut maintenant les labelliser pour pouvoir trier les données. J'ai opté pour une labellisation par tags qui décrivent l'ensemble de la séquence sans entrer dans un tracking de chaque élément, extrêmement fastidieux.
Comme j'avais des besoins à la fois simples et assez spécifiques, j'ai choisi de construire moi-même mon outil de labellisation sous forme de page web. Une fois la labellisation lancée, chaque séquence est jouée, le labellisateur peut alors sélectionner les tags qui correspondent ou en rajouter. Il note ainsi la qualité de la séquence, les différents parties du personnages présentes dans le champs, ainsi que le nom de chacun des personnages présents.
Une fois la labelisation finie, l'outil renvoie un fichier JSON contenant toutes les informations entrées.
Code source de la page disponible ici : https://drive.google.com/drive/folders/1Nfc7nOwmaL2_qYbuc57tlSu1-1_1Cb6u?usp=sharing
Base de donnée
Il ne nous reste maintenant plus qu'à les intégrer dans la base de donnée.
J'ai opté pour une base de donnée MySQL, avec une architecture réduite : chaque séquence est reliée à un ou plusieurs tags, un ou plusieurs personnages, et à un épisode faisant partie d'une série.
La structure exacte est représentée sur ce schéma :
Pour intégrer les informations dans la base de donnée, un script python va dépaqueter le fichier JSON, puis ajouter chaque entrée à la base de donnée en créant au fur et à mesure les tags et les personnages supplémentaires nécessaires.
Les informations retenues sont simples, mais permettent néanmoins de faire des requêtes puissantes à la base de donnée comme par exemple construire un corpus d'animation pour IA contenant uniquement :
- des mains de personnages d'animation japonaise
- le visage d'un personnage spécifique
- toutes les animations labelisées comment étant de bonne qualité d'un film spécifique
- etc.
Futurs développements
Si le projet est maintenant fini, il est encore quasiment vide, il reste en effet à l'utiliser à plus grande échelle pour collecter et classer les animations. Même si je n'ai pu fournir aucune utilisation concrète de ce travail cette année, c'est un outil puissant, qui me servira notamment l'année prochaine à tester différentes approches d'aide à l'animation par IA, très gourmandes en données sur lesquelles apprendre. Il me paraît donc très peu probable que ce projet ne justifie pas abondamment de son utilité dans les années qui viennent.
B3Q1
Sources
Mon but est de collecter une grande quantité d'animations schématiques afin de pouvoir s'en servir pour de l'apprentissage automatique. Plusieurs sources sont possibles, sous forme de sites d'hébergements soit de vidéos (Vimeo, Youtube), soit de GIFs (Pinterest, Tumblr). L'avantage des sites de GIFS est que les répertoires sont plus fournis, et plus propres à l'herbergement de fragments video courts. La petite définition n'est pour le moment pas un problème, mais elle pourrait le devenir par la suite.
Pinterest : - https://www.pinterest.fr/chars7793/rough-animation/
Tumblr : - https://2dtraditionalanimation.tumblr.com/archive
Structure
Extracteur d'URL
Le but du projet est simple : pouvoir télécharger en bloc tout le contenu d'un board pinterest pour constituer des bases de données d'images homogènes (les boards pinterest sont très efficace pour laisser aux utilisateurs le soin de trier les informations, et donc effectivement de les labeliser).
Méthode traditionnelle
La méthode classique consistant à charger la page via Mechanize, puis parse les données récupérées via CSSSelect ne marche que pour des sites relativement statiques. Or Pinterest demande qu'on se connecte pour pouvoir fournir des choix personnalisés.
Deux options sont donc possibles : essayer de continuer dans une voie 100% automatique avec un navigateur plus avancé comme Selenium, ou bien chercher une méthode plus spécifique. En l'occurence, on peut tirer partie du fait que la collecte de donnée se fait une fois, sur une seule page, et n'a pas besoin d'être répétée automatiquement. On peut donc ruser en altérant manuellement la page déjà chargée dans le navigateur pour qu'elle renvoie d'elle-même les données qui nous intéresse.
Méthode locale
J'ai donc fini par me rabattre sur une méthode que j'avais déjà expérimentée auparavant, consistant à aller modifier dans la page chargée dans le navigateur certaines fonctions de bases pour ménager un flux d'informations pertinentes vers le log, qui sera ensuite copié à la main.
Ici le but va être de modifier la fonction setAttribute de chacune des éléments de la page. Cette fonction est appelée à la création de nouveaux éléments graphiques pour remplir les différents attributs de la balise, dont un champ 'src' qui contient l'adresse exacte de l'image stockée sur Pinterest en haute définition. Ainsi, une fois modifiée, cette fonction remplira ses usages normaux, mais en prime renverra toute chaine de caractère qui contient le pattern "pinimg", présent dans toutes les adresses des images.
Pour cela, on va imbriquer des wrapping de fonction, qui permettent d'adjoindre de nouvelles fonctionnalités à une fonction existante.
Basiquement, on va donner à document.createElement une nouvelle valeur. Si createElement avait été une simple variable, on aurait donc une syntaxe comme celle là.
// valeur originale de x
var x = 2
// on exécute une fonction anonyme qui va appliquer une opération sur x, puis retourne le résultat
var x = function(old_x){
return x+2;
}(x);
Or createElement est une fonction, notre fonction anonyme devra donc elle-même renvoyer une fonction :
document.createElement = function(old){
return function(){
// la version original de document.createELement est exécutée
old.apply(this, arguments);
// on peut ici faire ce qu'on veut d'autre avec ses arguments
};
}(document.createElement);
//la fonction anonyme est exécutée instantanément, prenant pour paramètre (nommé old) la version originale de document.createElement
Ici on fait un double wrapping : on laisse createElement faire ce qu'elle veut pour créer la balise, puis on remplace sa fonction setAttribute par une fonction maison qui va simplement renvoyer le contenu de l'attribut si il contient pinimg.
Toute cette explication nous mène donc au code final assez peu lisible, mais efficace :
document.createElement = function(old_create) {
return function() {
// la version originale de createDocument est exécutée
var ret = old_create.apply(this, arguments);
// après l'exécution de createDocument et la création de la balise,
// on utilise la même méthode de wrapping pour changer le contenu
// de setAttribute
ret.setAttribute = function(old_setattribute){
return function(){
// l'ancienne version n'est même pas exécutée, on se contente
// de récupérer les infos nécessaires et les exporter dans le log
if (arguments[1].includes("pinimg")){
console.log(arguments[1])
}
}
}(ret.setattribute)
return ret;
};
}(document.createElement)
Téléchargement des images
Dans mon projet original, le log copié devait être manuellement transformé par une suite de rechercher-remplacer pour obtenir une liste d'adresses précédées d'un WGET, qui était ensuite exécuté en ligne de commande. J'ai pris cette fois ci là peine de faire un vrai script qui s'occupe de toutes ces étapes tout seul, en isolant la bonne adresse, puis en utilisant la librairie urlretrieve pour aller télécharger chacune des images et les stocker dans un dossier. Voici la version finale du script :
#! /usr/bin/python3
# -*- coding: utf-8 -*-
__author__ = "Pierre Etheve"
__email__ = "pierre.etheve.lfdv@gmail.com"
import urllib.request as rq
import sys
unique_pins = []
start = 0
folder = "pins"
source = "estampe_pinimg.txt"
# Processing des options, peu intéressant, à la reflexion j'aurais eu une
# meilleure syntaxe en utilisant la librairie opt
def getOptions() :
global folder, start, source
args = sys.argv
option = ""
for el in sys.argv :
if el[0] != "-" :
if option == "path" :
folder = el
print("Setting path to %s"%folder)
elif option == "start" :
start = int(el)
print("Setting start to %d"%start)
elif option == "source" :
source = el
print("Setting start to %s"%source)
option = ""
else :
option = el[1:]
# télécharger le pin à partir de l'url et le stocker dans le dossier 'folder'
def getPin(url, i) :
rq.urlretrieve(url, "%s/%05d.jpg"%(folder, i))
print("Saving to %s/%05d.jpg"%(folder,i))
# si aucune option n'est montrée, print l'aide et clore le programme
if len(sys.argv) == 1 :
print("./pin_dl.py -path [DESTINATION FOLDER] -start [SKIP TO # PIN] -source [xxx_pinimg.txt]")
sys.exit()
# process les options
getOptions()
# trier les url pour ne garder que celles en HD
with open(source) as infile :
for line in infile :
if "4x" in line :
link = line.split(", ")[-1][:-3]
if link not in unique_pins :
unique_pins.append(link)
print("Found %d unique pins"%len(unique_pins))
# pour chaque URL HD, lancer un télécharger grace à get_pin()
for i in range(len(unique_pins)) :
link = unique_pins[i]
getPin(link, i);