Créer un DSL pour générer des images

Bonjour, Habr! Il reste quelques jours avant le lancement d'un nouveau cours d'OTUS "Développement Backend sur Kotlin" . A la veille du début du cours, nous avons préparé pour vous une traduction d'un autre matériel intéressant.












Souvent, lors de la résolution de problèmes liés à la vision par ordinateur, le manque de données devient un gros problème. Cela est particulièrement vrai lorsque vous travaillez avec des réseaux de neurones.



Serait-ce cool si nous avions une source illimitée de nouvelles données originales?



Cette pensée m'a incité à développer un langage spécifique au domaine qui vous permet de créer des images dans diverses configurations. Ces images peuvent être utilisées pour former et tester des modèles d'apprentissage automatique. Comme son nom l'indique, les images DSL générées ne peuvent généralement être utilisées que dans une zone étroitement ciblée.



Exigences linguistiques



Dans mon cas particulier, je dois me concentrer sur la détection d'objets. Le compilateur de langage doit générer des images qui répondent aux critères suivants:



  • les images contiennent différentes formes (par exemple, des émoticônes);
  • le nombre et la position des figures individuelles sont personnalisables;
  • la taille et les formes de l'image sont personnalisables.


La langue elle-même doit être aussi simple que possible. Je veux d'abord déterminer la taille de l'image de sortie, puis la taille des formes. Ensuite, je veux exprimer la configuration réelle de l'image. Pour garder les choses simples, je considère l'image comme un tableau, où chaque forme s'insère dans une cellule. Chaque nouvelle ligne est remplie de formulaires de gauche à droite.



la mise en oeuvre



J'ai choisi une combinaison d' ANTLR, Kotlin et Gradle pour créer le DSL . ANTLR est un générateur d'analyseurs. Kotlin est un langage de type JVM similaire à Scala. Gradle est un système de construction similaire à sbt.



Environnement nécessaire



Vous aurez besoin de Java 1.8 et Gradle 4.6 pour effectuer les étapes décrites.



La configuration initiale



Créez un dossier pour contenir le DSL.



> mkdir shaperdsl
> cd shaperdsl


Créez un fichier build.gradle. Ce fichier est nécessaire pour répertorier les dépendances du projet et configurer des tâches Gradle supplémentaires. Si vous souhaitez réutiliser ce fichier, il vous suffit de modifier les espaces de noms et la classe principale.



> touch build.gradle


Ci-dessous le contenu du fichier:



buildscript {
   ext.kotlin_version = '1.2.21'
   ext.antlr_version = '4.7.1'
   ext.slf4j_version = '1.7.25'

   repositories {
     mavenCentral()
     maven {
        name 'JFrog OSS snapshot repo'
        url  'https://oss.jfrog.org/oss-snapshot-local/'
     }
     jcenter()
   }

   dependencies {
     classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
     classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.1'
   }
}

apply plugin: 'kotlin'
apply plugin: 'java'
apply plugin: 'antlr'
apply plugin: 'com.github.johnrengelman.shadow'

repositories {
  mavenLocal()
  mavenCentral()
  jcenter()
}

dependencies {
  antlr "org.antlr:antlr4:$antlr_version"
  compile "org.antlr:antlr4-runtime:$antlr_version"
  compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
  compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
  compile "org.apache.commons:commons-io:1.3.2"
  compile "org.slf4j:slf4j-api:$slf4j_version"
  compile "org.slf4j:slf4j-simple:$slf4j_version"
  compile "com.audienceproject:simple-arguments_2.12:1.0.1"
}

generateGrammarSource {
    maxHeapSize = "64m"
    arguments += ['-package', 'com.example.shaperdsl']
    outputDirectory = new File("build/generated-src/antlr/main/com/example/shaperdsl".toString())
}
compileJava.dependsOn generateGrammarSource

jar {
    manifest {
        attributes "Main-Class": "com.example.shaperdsl.compiler.Shaper2Image"
    }

    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

task customFatJar(type: Jar) {
    manifest {
        attributes 'Main-Class': 'com.example.shaperdsl.compiler.Shaper2Image'
    }
    baseName = 'shaperdsl'
    from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
    with jar
}


Analyseur de langue



L'analyseur est construit comme la grammaire ANTLR .



mkdir -p src/main/antlr
touch src/main/antlr/ShaperDSL.g4


avec le contenu suivant:



grammar ShaperDSL;

shaper      : 'img_dim:' img_dim ',shp_dim:' shp_dim '>>>' ( row ROW_SEP)* row '<<<' NEWLINE* EOF;
row       : ( shape COL_SEP )* shape ;
shape     : 'square' | 'circle' | 'triangle';
img_dim   : NUM ;
shp_dim   : NUM ;

NUM       : [1-9]+ [0-9]* ;
ROW_SEP   : '|' ;
COL_SEP   : ',' ;

NEWLINE   : '\r\n' | 'r' | '\n';


Vous pouvez maintenant voir comment la structure de la langue devient plus claire. Pour générer le code source de la grammaire, exécutez:



> gradle generateGrammarSource


En conséquence, vous obtiendrez le code généré au format build/generate-src/antlr.



> ls build/generated-src/antlr/main/com/example/shaperdsl/
ShaperDSL.interp  ShaperDSL.tokens  ShaperDSLBaseListener.java  ShaperDSLLexer.interp  ShaperDSLLexer.java  ShaperDSLLexer.tokens  ShaperDSLListener.java  ShaperDSLParser.java


Arbre de syntaxe abstraite



L'analyseur convertit le code source en une arborescence d'objets. L'arborescence d'objets est ce que le compilateur utilise comme source de données. Pour obtenir l'AST, vous devez d'abord définir le métamodèle de l'arborescence.



> mkdir -p src/main/kotlin/com/example/shaperdsl/ast
> touch src/main/kotlin/com/example/shaper/ast/MetaModel.kt


MetaModel.ktcontient les définitions des classes d'objets utilisées dans le langage, à partir de la racine. Ils héritent tous du Node . La hiérarchie de l'arborescence est visible dans la définition de classe.



package com.example.shaperdsl.ast

interface Node

data class Shaper(val img_dim: Int, val shp_dim: Int, val rows: List<Row>): Node

data class Row(val shapes: List<Shape>): Node

data class Shape(val type: String): Node


Ensuite, vous devez faire correspondre la classe avec ASD:



> touch src/main/kotlin/com/example/shaper/ast/Mapping.kt


Mapping.ktest utilisé pour construire un AST en utilisant les classes définies dans MetaModel.kt, en utilisant les données de l'analyseur.



package com.example.shaperdsl.ast

import com.example.shaperdsl.ShaperDSLParser

fun ShaperDSLParser.ShaperContext.toAst(): Shaper = Shaper(this.img_dim().text.toInt(), this.shp_dim().text.toInt(), this.row().map { it.toAst() })

fun ShaperDSLParser.RowContext.toAst(): Row = Row(this.shape().map { it.toAst() })

fun ShaperDSLParser.ShapeContext.toAst(): Shape = Shape(text)


Le code sur notre DSL:



img_dim:100,shp_dim:8>>>square,square|circle|triangle,circle,square<<<


Sera converti en ASD suivant:







Compilateur



Le compilateur est la dernière partie. Il utilise ASD pour obtenir un résultat spécifique, dans ce cas, une image.



> mkdir -p src/main/kotlin/com/example/shaperdsl/compiler
> touch src/main/kotlin/com/example/shaper/compiler/Shaper2Image.kt


Il y a beaucoup de code dans ce fichier. Je vais essayer de clarifier les principaux points.



ShaperParserFacadeEst un wrapper sur le dessus ShaperAntlrParserFacadequi construit l'AST réel à partir du code source fourni.



Shaper2Imageest la classe principale du compilateur. Après avoir reçu l'AST de l'analyseur, il parcourt tous les objets qu'il contient et crée des objets graphiques, qu'il insère ensuite dans l'image. Ensuite, il renvoie la représentation binaire de l'image. Il existe également une fonction maindans l'objet compagnon de la classe pour permettre les tests.



package com.example.shaperdsl.compiler

import com.audienceproject.util.cli.Arguments
import com.example.shaperdsl.ShaperDSLLexer
import com.example.shaperdsl.ShaperDSLParser
import com.example.shaperdsl.ast.Shaper
import com.example.shaperdsl.ast.toAst
import org.antlr.v4.runtime.CharStreams
import org.antlr.v4.runtime.CommonTokenStream
import org.antlr.v4.runtime.TokenStream
import java.awt.Color
import java.awt.image.BufferedImage
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import javax.imageio.ImageIO

object ShaperParserFacade {

    fun parse(inputStream: InputStream) : Shaper {
        val lexer = ShaperDSLLexer(CharStreams.fromStream(inputStream))
        val parser = ShaperDSLParser(CommonTokenStream(lexer) as TokenStream)
        val antlrParsingResult = parser.shaper()
        return antlrParsingResult.toAst()
    }

}


class Shaper2Image {

    fun compile(input: InputStream): ByteArray {
        val root = ShaperParserFacade.parse(input)
        val img_dim = root.img_dim
        val shp_dim = root.shp_dim

        val bufferedImage = BufferedImage(img_dim, img_dim, BufferedImage.TYPE_INT_RGB)
        val g2d = bufferedImage.createGraphics()
        g2d.color = Color.white
        g2d.fillRect(0, 0, img_dim, img_dim)

        g2d.color = Color.black
        var j = 0
        root.rows.forEach{
            var i = 0
            it.shapes.forEach {
                when(it.type) {
                    "square" -> {
                        g2d.fillRect(i * (shp_dim + 1), j * (shp_dim + 1), shp_dim, shp_dim)
                    }
                    "circle" -> {
                        g2d.fillOval(i * (shp_dim + 1), j * (shp_dim + 1), shp_dim, shp_dim)
                    }
                    "triangle" -> {
                        val x = intArrayOf(i * (shp_dim + 1), i * (shp_dim + 1) + shp_dim / 2, i * (shp_dim + 1) + shp_dim)
                        val y = intArrayOf(j * (shp_dim + 1) + shp_dim, j * (shp_dim + 1), j * (shp_dim + 1) + shp_dim)
                        g2d.fillPolygon(x, y, 3)
                    }
                }
                i++
            }
            j++
        }

        g2d.dispose()
        val baos = ByteArrayOutputStream()
        ImageIO.write(bufferedImage, "png", baos)
        baos.flush()
        val imageInByte = baos.toByteArray()
        baos.close()
        return imageInByte

    }

    companion object {

        @JvmStatic
        fun main(args: Array<String>) {
            val arguments = Arguments(args)
            val code = ByteArrayInputStream(arguments.arguments()["source-code"].get().get().toByteArray())
            val res = Shaper2Image().compile(code)
            val img = ImageIO.read(ByteArrayInputStream(res))
            val outputfile = File(arguments.arguments()["out-filename"].get().get())
            ImageIO.write(img, "png", outputfile)
        }
    }
}


Maintenant que tout est prêt, construisons le projet et obtenons un fichier jar avec toutes les dépendances ( uber jar ).



> gradle shadowJar
> ls build/libs
shaper-dsl-all.jar


Essai



Tout ce que nous avons à faire est de vérifier si tout fonctionne, alors essayez d'entrer ce code:



> java -cp build/libs/shaper-dsl-all.jar com.example.shaperdsl.compiler.Shaper2Image \
--source-code "img_dim:100,shp_dim:8>>>circle,square,square,triangle,triangle|triangle,circle|square,circle,triangle,square|circle,circle,circle|triangle<<<" \
--out-filename test.png


Un fichier sera créé:



.png


qui ressemblera à ceci:







Conclusion



Il s'agit d'un simple DSL, il n'est pas sécurisé et se cassera probablement s'il est mal utilisé. Cependant, cela convient bien à mon objectif et je peux l'utiliser pour créer un nombre illimité d'échantillons d'images uniques. Il peut être facilement étendu pour plus de flexibilité et peut être utilisé comme modèle pour d'autres DSL.



Un exemple DSL complet peut être trouvé dans mon référentiel GitHub: github.com/cosmincatalin/shaper .



Lire la suite






All Articles