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.kt
contient 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.kt
est 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.
ShaperParserFacade
Est un wrapper sur le dessus ShaperAntlrParserFacade
qui construit l'AST réel à partir du code source fourni.
Shaper2Image
est 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 main
dans 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 .