Bei der Lösung von Problemen im Zusammenhang mit der Bildverarbeitung wird der Mangel an Daten häufig zu einem großen Problem. Dies gilt insbesondere für die Arbeit mit neuronalen Netzen.
Wie cool wäre es, wenn wir eine unbegrenzte Quelle für neue Originaldaten hätten?
Dieser Gedanke veranlasste mich, eine domänenspezifische Sprache zu entwickeln, mit der Sie Bilder in verschiedenen Konfigurationen erstellen können. Diese Bilder können zum Trainieren und Testen von Modellen für maschinelles Lernen verwendet werden. Wie der Name schon sagt, können DSL-generierte Bilder normalerweise nur in einem eng fokussierten Bereich verwendet werden.
Sprachanforderungen
In meinem speziellen Fall muss ich mich auf die Objekterkennung konzentrieren. Der Sprachcompiler muss Bilder generieren, die die folgenden Kriterien erfüllen:
- Bilder enthalten verschiedene Formen (z. B. Emoticons);
- Die Anzahl und Position der einzelnen Figuren ist anpassbar.
- Bildgröße und -formen sind anpassbar.
Die Sprache selbst sollte so einfach wie möglich sein. Ich möchte zuerst die Größe des Ausgabebildes und dann die Größe der Formen bestimmen. Dann möchte ich die tatsächliche Konfiguration des Bildes ausdrücken. Um die Dinge einfach zu halten, stelle ich mir das Bild als Tabelle vor, in der jede Form in eine Zelle passt. Jede neue Zeile wird von links nach rechts mit Formularen gefüllt.
Implementierung
Ich habe eine Kombination aus ANTLR, Kotlin und Gradle gewählt , um das DSL zu erstellen . ANTLR ist ein Parser-Generator. Kotlin ist eine JVM-ähnliche Sprache ähnlich wie Scala. Gradle ist ein ähnliches Build-System wie
sbt
.
Notwendiges Umfeld
Sie benötigen Java 1.8 und Gradle 4.6, um die beschriebenen Schritte auszuführen.
Ersteinrichtung
Erstellen Sie einen Ordner mit DSL.
> mkdir shaperdsl
> cd shaperdsl
Erstellen Sie eine Datei
build.gradle
. Diese Datei wird benötigt, um Projektabhängigkeiten aufzulisten und zusätzliche Gradle-Aufgaben zu konfigurieren. Wenn Sie diese Datei wiederverwenden möchten, müssen Sie nur die Namespaces und die Hauptklasse ändern.
> touch build.gradle
Unten ist der Inhalt der Datei:
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
}
Sprachparser
Der Parser ist wie eine ANTLR- Grammatik aufgebaut .
mkdir -p src/main/antlr
touch src/main/antlr/ShaperDSL.g4
mit folgendem Inhalt:
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';
Jetzt können Sie sehen, wie die Struktur der Sprache klarer wird. Führen Sie Folgendes aus, um den Grammatik-Quellcode zu generieren:
> gradle generateGrammarSource
Als Ergebnis erhalten Sie den generierten Code in
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
Abstrakter Syntaxbaum
Der Parser konvertiert den Quellcode in einen Objektbaum. Der Objektbaum wird vom Compiler als Datenquelle verwendet. Um den AST zu erhalten, müssen Sie zuerst das Baummetamodell definieren.
> mkdir -p src/main/kotlin/com/example/shaperdsl/ast
> touch src/main/kotlin/com/example/shaper/ast/MetaModel.kt
MetaModel.kt
enthält die Definitionen der in der Sprache verwendeten Objektklassen, beginnend mit der Wurzel. Sie alle erben vom Knoten . Die Baumhierarchie ist in der Klassendefinition sichtbar.
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
Als nächstes müssen Sie die Klasse mit ASD abgleichen:
> touch src/main/kotlin/com/example/shaper/ast/Mapping.kt
Mapping.kt
wird verwendet, um einen AST unter Verwendung der in definierten Klassen MetaModel.kt
unter Verwendung von Daten aus dem Parser zu erstellen.
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)
Der Code auf unserer DSL:
img_dim:100,shp_dim:8>>>square,square|circle|triangle,circle,square<<<
Wird in die folgende ASD konvertiert:
Compiler
Der Compiler ist der letzte Teil. Er verwendet ASD, um ein bestimmtes Ergebnis zu erhalten, in diesem Fall ein Bild.
> mkdir -p src/main/kotlin/com/example/shaperdsl/compiler
> touch src/main/kotlin/com/example/shaper/compiler/Shaper2Image.kt
Diese Datei enthält viel Code. Ich werde versuchen, die wichtigsten Punkte zu klären.
ShaperParserFacade
Ist ein Wrapper oben ShaperAntlrParserFacade
, der den tatsächlichen AST aus dem bereitgestellten Quellcode erstellt.
Shaper2Image
ist die Haupt-Compiler-Klasse. Nachdem es den AST vom Parser empfangen hat, durchläuft es alle darin enthaltenen Objekte und erstellt grafische Objekte, die es dann in das Bild einfügt. Dann wird die binäre Darstellung des Bildes zurückgegeben. Es gibt auch eine Funktion main
im Begleitobjekt der Klasse, um das Testen zu ermöglichen.
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)
}
}
}
Nachdem alles fertig ist, erstellen wir das Projekt und erhalten eine JAR-Datei mit allen Abhängigkeiten ( über JAR ).
> gradle shadowJar
> ls build/libs
shaper-dsl-all.jar
Testen
Wir müssen nur überprüfen, ob alles funktioniert. Geben Sie also diesen Code ein:
> 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
Eine Datei wird erstellt:
.png
was so aussehen wird:
Fazit
Es ist ein einfaches DSL, ungesichert und wird wahrscheinlich kaputt gehen, wenn es missbraucht wird. Es passt jedoch gut zu meinem Zweck und ich kann damit beliebig viele einzigartige Bildbeispiele erstellen. Es kann für mehr Flexibilität einfach erweitert und als Vorlage für andere DSLs verwendet werden.
Ein vollständiges DSL-Beispiel finden Sie in meinem GitHub-Repository: github.com/cosmincatalin/shaper .