Challenge de programmation Instagram : Shredder
November 24, 2011 in Python
J’ai participé il y a deux semaines de cela à un petit challenge d’algorithmie lancé par Instagram, mais si vous savez, le réseau social aux teintes polaroids. Pour ceux qui ont la flemme d’aller sur leur blog, voici visuellement en quoi le challenge consiste.
Le but du jeu était à partir d’une image découpée en segments et réarrangée aléatoirement, de retrouver l’ordre original en régénérant l’image. L’analogie était faite avec un broyeur de documents qui découpe les feuilles de papier en des lamelles de tailles égales.
Il se trouve que j’ai été sélectionné dans le groupe A (le groupe des winners mesdames et monsieurs). Je vais donc recevoir un superbe t-shirt, et pour la peine ça mérite un article.
Voici comment je m’y suis pris.
Je choisis une lamelle de l’image (dans les faits, la première à gauche mais cela n’a pas d’impact sur l’algorithme). Je cherche la lamelle à sa droite parmi toutes les autres. Pour ce faire je compare le bord droit de ma première lamelle avec le bord gauche de l’autre. Comparer un bord veut dire comparer pour une même hauteur le pixel de gauche et de droite.
for y in range(self.source_image.size[1]):
# compare left pixel and right pixel with tolerance
pixel_left = shred_left.get_right_pixels(y)
pixel_right = shred_right.get_left_pixels(y)
# score is a flat number (clarity)
score += abs(pixel_left[0] - pixel_right[0])
+ abs(pixel_left[1] - pixel_right[1])
+ abs(pixel_left[2] - pixel_right[2])
+ abs(pixel_left[3] - pixel_right[3])
Dans les deux premières lignes, je récupère les pixels des bords. Je lis mon image en RGBA (Red, Green, Blue, Alpha). Ceci veut dire que chaque pixel est représenté par 4 valeurs, les 3 couleurs et sa transparence.
Ainsi, si deux pixels sont semblables, la soustraction de l’un avec l’autre devrait tendre vers 0.
Pour chaque pixel comparé, j’en retire un score (soustrayant chaque composante ensemble et en les sommant pour passer d’un vecteur à un scalaire). Je n’oublie pas d’en tirer à chaque fois la valeur absolue pour ne pas fausser le résultat.
Ainsi, si deux pixels sont proches, leur soustraction tend vers 0, donc leur score sera petit. Ayant comparé toutes les lamelles entre elles, la bonne voisine est celle qui a le score le plus bas. Et ainsi de suite.
Cette approche m’a permis de réunifier avec quelques erreurs le dessin du dessus. Lorsque dans le commentaire du code je précise “with tolerance”, cela veut dire que je n’ai pas réellement comparé deux pixels entre eux. Et oui j’ai menti. Regardons le code des “get_left_pixels” (le code pour le pixel de droite est identique).
def get_left_pixels(self, y):
"""
Given a height, get the average left pixel.
"""
pixels = []
for i in range(0, self.magic_average):
pixels.append(self.get_pixel_value(self.x + i, y))
average_pixel = reduce(self.get_average_pixel, pixels)
return average_pixel
Au lieu de récupérer un seul pixel, je moyenne un nombre de pixels donné sur la même ligne (le nombre de pixels étant symbolisé par magic_average). La moyenne est effectuée par “get_average_pixel” et aidée d’un reduce parce que moi aussi je connais la programmation fonctionnelle, non mais. J’ai déterminé ce magic number de manière très empirique et sa valeur est trois. Point. Plus, vous augmentez les erreurs, moins, ce n’est pas assez. Tout simplement.
Vous vous dites, tu es sympa coco, ton article bien long, il commence à me fatiguer (ou bien tu t’es déjà arrêté de lire mais tu programmes surement en Java). Et bien ce serait dommage de s’arrêter de lire maintenant car l’algorithme ne résout pas encore le problème. Paradoxalement c’est la partie sur laquelle j’ai passé le plus de temps.
Pour l’instant, nous sommes incapables de déterminer la première lamelle de la dernière !
Et oui, le début n’est pas au début et la fin n’est pas à la fin, comme si l’image avait été coupée en deux. Naïvement je pensais qu’en conservant tous les scores et en choisissant comme début et fin les deux lamelles qui avaient eu le score le plus improbable (donc le plus haut) seraient les deux bords. A priori, les bords n’ayant pas de voisin par définition, le côté trouvé doit vraiment avoir un score haut. Et bien pas du tout. Et oui j’étais aussi déçu que vous.
J’ai donc fait ça.
while(len(self.shred_ordered) < len(self.shred_list) and loop):
# search neighbor while all shred are not ordered
current_shred = local_find(shred)
# must not be possible
assert(current_shred not in self.shred_ordered)
if seek_right:
# check if it is a good fit with its left side
left_shred = self.find_left(current_shred)
if left_shred.x != shred.x:
# it is the right border
loop = False
Je lance l’algorithme pour rechercher tous les voisins de droite successifs des lamelles. Pour m’assurer que ce n’est pas un bord, je cherche aussi à voir si le voisin de gauche de la lamelle potentiellement voisine de droite (vous suivez ?) est bien la même. Sinon, c’est un bord. Dans le code cela se caractérise par la dernière condition où je compare les abscisses des deux lamelles (j’identifie les lamelles par leurs abscisses du point en haut à gauche). “local_find” est dans l’exemple un pointeur de fonction sur “find_right”, ceci m’a permis de factoriser le code pour permettre la recherche à gauche et à droite dans la même fonction (symbolisé par le booléen seek_right).
Juste pour la beauté du geste, j’utilise une liste pour stocker les lamelles ordonnées (“shred_ordered”). Voici l’algorithme à son plus haut niveau d’abstraction.
# peek the first shred to start self.shred_ordered.append(self.shred_list[VAR_START_WITH]) # order from left to right self.order_shred(True, self.shred_ordered[-1]) # reverse to order from right to left self.shred_ordered.reverse() self.order_shred(False, self.shred_ordered[-1]) # reverse again to get the original order self.shred_ordered.reverse()
Je renverse juste l’ordre de la liste avant de changer le sens de la recherche. Finalement je renverse une dernière fois cette liste afin qu’elle soit correctement lue par la fonction de sauvegarde de l’image ordonnée (sinon l’image aurait eu un effet miroir assez dérangeant).
Voilà, je me suis bien amusé et vous ?
Le code source de la chose est disponible sur mon github -> c’est par là et ça s’appelle Shreddator !


On me force à venir comment donc je vais faire des remarques critiques
Je trouve que tester tout les pixels sur la hauteur c’est de l’overkill et qu’une autre constante magique genre un pixel sur 3 aurait aidé à gagner en perf.
Sinon je me demande comment s’en sort ton truc sur une feuille avec juste du texte (donc majoritairement blanche ce qui fera des scores très petits tout le temps). Je suppose qu’un brute force de détection de texte serai plus simple dans ce cas.
En tout cas bon challenge, où python est un vrai atout par rapport à c++, même si le faire en Matlab aurai été plus noble.
C’est un problème de programmation dynamique. Au prix de quelques kilo-octets, on peut stocker dans une matrice les résultats de chaque comparaison de lamelle, on a ainsi pas à refaire les calculs un peu lourdingue à chaque fois.
Et sur du texte, j’ai testé hier soir, ça ne marche absolument pas. Tout ça pour dire que je n’ai pas cité la plus grosse limitation de l’algo, s’il se plante une fois, l’image sera déguelasse. J’en ai une version où je n’ai pas cet effet mais qui est bien moins élégante.
Matlab@lol ..
Sympa!
J’aime beaucoup le tag “star du rock”.
“ou bien tu t’es déjà arrêté de lire mais tu programmes surement en Java”
Mmmmh, vas-y laisse exprimer ton côté féminin, cette petite pointe de jalousie te soulagera certainement de ne pas comprendre “da Java powa” : write once, bug everywhere !
:-p
Je reste perplexe concernant le choix d’une algo glouton/greedy pour déterminer la ‘meilleur ligne voisine’.
Et j’imagine que c’est pour cela que tu as parfois des résultats dégelasses : tu n’as aucune tolérance à l’erreur dans le choix du voisin car si ton algo se plante une seul fois c’est tout le reste de l’image qui est certainement foiré.
Et dans le domaine de l’imagerie, tu sais à quel point c’est un monde avec beaucoup d’a-peu-près et de tolérance.
Bref c’est pas mal car c’est léger et performant.
Et j’aime bien le commentaire de ton assert : “must not be possible”
Bon sinon tu pourrais faire un filtre, genre Sobel si Jelena souvient bien, qu’une gros fait une dérivée de ton image. Du coup au lieu de comparer des composantes avec une tolérance, tu compares un taux de variation. Ça doit même pouvoir marcher avec du texte. Je laisse ça a ta sagacité, et si t’es vraiment gaillard tu me le fais en assembleur, parce que python c’est un langage à la noix ( ça n’engage que moi mais j’ai raison )
Je ne connais pas ces algos. Taux de variation ca peut être pas mal mais je ne suis pas convaincu que ça soit très applicable dans mon cas (je ne compare pas des images complètes mais des bords), néanmoins je vais me renseigner et si ça marche je m’abaisserai platement devant ton conseil.
Pour ce qui est du troll sur l’asm (réponse sans troll): le but du jeu est ici d’écrire un algorithme pour un concours. Je ne pense pas en Python mais en pseudo code. Python permet de faire une implémentation rapidement (et plus ou moins élégante). De plus, la pile logicielle d’Instagram est constituée en grande partie de soft écrit en Python (je t’encourage à lire le reste de leur blog technique), donc si tu veux les intéresser, écrire ton programme dans un langage utilisé par la boite est toujours une stratégie gagnante
Kikoolol,
En effet l’algo de Sobel a l’air excellent : http://fr.wikipedia.org/wiki/Algorithme_de_Sobel .
T’as plus qu’à t’abaisser platement
Désolé pour les conneries d’auto complétion de mon tel qui rend mon commentaire assez peu compréhensible