Reconnaissance de caractères avec Google Vision
Depuis l’arrivée des PC capable d’exécuter des réseaux neuronaux, l’apprentissage profond est devenu un terrain de jeu accessible à un grand nombre de gens. Cet événement a permis des percées majeures en intelligence artificielle, notamment tout ce qui touche à la classification comme la reconnaissance d’objets ou, comme c’est le sujet aujourd’hui, de la reconnaissance de caractères.
Dans quel contexte ça peut être utile?
La reconnaissance de caractères a plusieurs utilités qui peuvent accélérer le travail manuel de lecture. On peut par exemple penser pouvoir rechercher dans des notes manuscrites, lire les informations bancaires sur un chèque ou même lire des plaques d’immatriculation de voitures.
Le principe ici, c’est d’automatiser une tâche : la lecture et la recherche d’information. Et… aujourd’hui, on va tester une librairie de Google !
Rendez-vous ici pour créer votre projet dans Google Cloud Platform si vous n’en avez pas!
Préparation du projet
Cet article sera en Node, mais sachez que la documentation de Google contient des exemples dans plusieurs langages.
Installation de l’API Vision
Depuis un répertoire vide :
1 2 |
npm init -y npm install @google-cloud/vision |
Puis, prenez une photo d’un objet sur lequel se trouve du texte que vous voulez analyser. À titre d’exemple, je vais mettre les performances de l’API de Google à l’épreuve avec une photo d’une cassette de SNES.
Espérons que l’algorithme ait plus de facilité à lire le titre du jeu que moi pour le passer 😛
Installation de string-similarity
Cette librairie permet de comparer une chaîne de caractères avec une liste afin de sortir l’entrée de la liste qui a le plus de ressemblance. La librairie nous servira a rechercher le texte analysé par Vision parmi une base de données de titres de SNES.
1 |
npm i string-similarity |
Création du main
À la racine du projet, créer un fichier index.js pour y taper le code suivant :
1 2 3 4 5 6 7 8 9 10 11 |
async function main() { const snesData = loadSnesData(); const imagesToScan = fs.readdirSync('./images') .map(fileName => `./images/${fileName}`); for (let currentImage of imagesToScan) { console.log(`Scanning file ${currentImage}`); // ... } } |
Ici, la fonction loadSnesData() récupère la liste des titres de snes depuis un fichier sur mon disque. Sachez que la fonction retourne une collection d’objets contenant le titre, la compagnie et l’année de publication du jeu.
Par la suite, on boucle sur les fichiers du répertoire images où j’ai préalablement déposé quelques photos. L’objectif maintenant est de voir ce que l’API Vision peut nous offrir.
Reconnaissance de logos
Google nous offre une API afin de retrouver les logos sur une image. Nous allons simplement essayer de voir si l’image contient le logo du snes. Dans un répertoire functions, créer le fichier vision-api.js pour y mettre le code suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const vision = require('@google-cloud/vision'); const client = new vision.ImageAnnotatorClient({ keyFile: './Blog-82a748f14fb8.json' }); async function findLogos(imagePath) { const [logoResult] = await client.logoDetection(imagePath); return logoResult.logoAnnotations .filter(annotation => annotation.score > .5) .map(annotation => annotation.description); } module.exports = { findLogos } |
Dans un premier temps, on crée une instance du client de Vision en lui passant notre jeton JSON d’accès. Assurez-vous de modifier la ligne pour y mettre le vôtre. Puis, on déclare une fonction pour récupérer les logos via la méthode logoDetection().
Pour faire simple, disons que le résultat de l’API contient les logos trouvés sous la propriété logoAnnotations, puis le nom du logo (compagnie) est sous description. Chaque logo trouvé vient avec un score compris entre 0 et 1 indiquant le niveau de confiance de Google d’avoir trouvé ce logo dans l’image. Ici, je ne garde que ceux dont le score est > 0.5, puis je retourne la description.
Dans notre main, faisons l’appel pour valider si l’image contient le logo de snes :
1 2 3 4 |
async function isSnesLogoPresent(imagePath) { const logos = await findLogos(imagePath); return logos.indexOf('Super Nintendo Entertainment System') > -1; } |
Récupération des textes
Dans le fichier vision-api.js, ajouter la fonction suivante, puis ajouter la fonction à l’export :
1 2 3 4 5 6 7 8 9 10 |
async function findTexts(imagePath) { const [textResult] = await client.textDetection(imagePath); return textResult.fullTextAnnotation.text .replace(/\n/g, ' '); } module.exports = { findLogos, findTexts } |
Ici, Google nous retourne un agrégat de tous les textes trouvés dans fullTextAnnotation.text. Les textes qui sont à des positions différentes dans l’image seront séparés par des retours de chariot, d’où l’appel au remplacement de ces derniers par des espaces.
Maintenant, dans notre main, je dois trouver une façon de corriger les erreurs faites par Google Vision. Pour ce faire, je charge la totalité des mots compris dans les noms des jeux dans un Set. Ainsi, j’aurai une liste de tous les mots valident pour un titre de jeu de snes. Dans un fichier extract-valid-word.js :
1 2 3 4 5 6 7 |
module.exports = function extractvalidWords(games) { const validWords = new Set(); games.forEach(game => game.name.split(' ').forEach(word => validWords.add(word.trim().toLowerCase()))); return Array.from(validWords); } |
Puis l’objectif ici est de jetonifier (nope, ça n’existe pas, mais tokenize) les mots trouvés par Google Vision en séparant la chaîne sur les espaces. Ensuite on peut comparer chaque mot avec un mot valide pour tenter de réparer les erreurs (ex. : pour Super Ghouls ‘n Ghosts, Google Vision voit ghostc au lieu de ghosts). Dans un fichier find-best-valid-word-for.js , taper :
1 2 3 4 5 6 7 8 9 10 |
const stringSimilarity = require('string-similarity'); const invalidWords = ['capcom', 'license', 'usa', 'system'] module.exports = function findBestValidWordFor(word, validWords) { word = word.trim().toLowerCase(); if (invalidWords.indexOf(word) > -1) return null; const result = stringSimilarity.findBestMatch(word, validWords); const bestMatch = result.bestMatch; return bestMatch.rating > .75 ? bestMatch.target : null; } |
La fonction findBestMatch() de stringSimilarity nous retourne une entrée du tableau qui a le score le plus élevé de similitude. Notez que la le retour contient aussi la liste de tous les éléments avec leur score respectif. Le principe ici est de retirer les mots qui ne correspondent à rien et aussi de retirer les mots qui peuvent se retrouver ailleurs sur l’image (comme le nom de la compagnie, etc.)
Essayons de nettoyer ce que Google nous retourne, dans le main :
1 2 3 4 5 6 7 8 |
let allTexts = await findTexts(currentImage); allTexts = allTexts .split(' ') .map(word => findBestValidWordFor(word, validWords)) .filter(word => word != null) .join(' ') .toLowerCase(); console.log(` Cleaned text : ${allTexts}`) |
Voici un exemple de la chaîne avant et après son nettoyage :
1 2 3 4 |
Scanning file ./images/ghoulsnghosts.jpg Found SNES logo in image. LICENSED BY Nintendo CAPCOM USA 十十 SUPER GHOULS N GHOSTC Nintendo Seelef Quality SUPER NINTENDO SNS-CM-USA MADE IN JAPAN EMTERTAI MENT STSTEM it Cleaned text : license super ghouls ghosts super in it |
Essayons maintenant de retrouver le bon jeu :
1 2 3 4 5 6 7 8 |
const gamesWithRating = snesData.map(game => ({ game, score: stringSimilarity .compareTwoStrings(allTexts, game.name.toLowerCase()) })); const gamesFound = gamesWithRating .sort((game1, game2) => game2.score - game1.score) .filter(x => x.score > toleranceInTitle); |
Ici, toleranceInTitle est une constante que j’ai initialisée à 0.4. Donc, on récupère les scores pour chaque titre, puis on retire tous ceux qui ne sont pas assez précis. En triant par score décroissant et en ne conservant que le premier résultat, on devrait avoir le jeu parmi toute la collection que Google Vision a pu retrouver :
1 2 3 4 5 6 |
if (gamesFound.length === 0) { console.log(' Game was not found.'); } else { const gameFound = gamesFound[0]; console.log(` ${gameFound.game.name} made by ${gameFound.game.company} in ${gameFound.game.year} (${gameFound.score})`); } |
Pour le plaisir, voici le résultat de 4 images :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Scanning file ./images/battletoads.jpg Found SNES logo in image. Cleaned text : license double dragon ultimate in the ultimate game the in super Super Double Dragon made by Tradewest in 1992 (0.43478260869565216) Scanning file ./images/ghoulsnghosts.jpg Found SNES logo in image. Cleaned text : license super ghouls ghosts super in it Super Ghouls 'n Ghosts made by Capcom in 1991 (0.6) Scanning file ./images/mariorpg.jpg Found SNES logo in image. Cleaned text : kids to super legend of the seven stars ages 6 in exertainment Super Mario RPG: Legend of the Seven Stars made by Nintendo in 1996 (0.5714285714285714) Scanning file ./images/zelda.jpg Found SNES logo in image. Cleaned text : the legend of a link to the past in super Legend of Zelda: A Link to the Past, The made by Nintendo in 1992 (0.7419354838709677) |
Boom! 75% de succès 😬 Le premier était Battletoads & Double Dragon…
Vous pouvez voir les sources finales ici.
Conclusion
Aujourd’hui, nous avons vu comment appeler Google Vision, notamment son service de reconnaissance de logos et de texte pour retrouver quel jeu de snes se trouve sur une photo. Mon algo n’est pas parfait, mais il fonctionne assez bien! Vous auriez des améliorations? Je veux les savoir en commentaires!
Cheers! Bonne semaine.
Commentaires
Laisser un commentaire