A menudo, al resolver problemas relacionados con la visión por computadora, la falta de datos se convierte en un gran problema. Esto es especialmente cierto cuando se trabaja con redes neuronales.
¿Qué tan genial sería si tuviéramos una fuente ilimitada de nuevos datos originales?
Este pensamiento me impulsó a desarrollar un lenguaje específico de dominio, que le permite crear imágenes en varias configuraciones. Estas imágenes se pueden usar para entrenar y probar modelos de aprendizaje automático. Como sugiere el nombre, las imágenes DSL generadas generalmente solo se pueden usar en un área de enfoque limitado.
Requisitos de idioma
En mi caso particular, necesito centrarme en la detección de objetos. El compilador del lenguaje debe generar imágenes que cumplan con los siguientes criterios:
- las imágenes contienen diferentes formas (por ejemplo, emoticonos);
- el número y la posición de las figuras individuales es personalizable;
- el tamaño y las formas de la imagen son personalizables.
El lenguaje en sí debería ser lo más simple posible. Primero quiero determinar el tamaño de la imagen de salida y luego el tamaño de las formas. Entonces quiero expresar la configuración real de la imagen. Para simplificar las cosas, pienso en la imagen como una tabla, donde cada forma encaja en una celda. Cada nueva fila está llena de formularios de izquierda a derecha.
Implementación
Elegí una combinación de ANTLR, Kotlin y Gradle para crear la DSL . ANTLR es un generador de analizador sintáctico. Kotlin es un lenguaje similar a JVM similar a Scala. Gradle es un sistema de compilación similar a
sbt
.
Entorno necesario
Necesitará Java 1.8 y Gradle 4.6 para completar los pasos descritos.
Configuración inicial
Cree una carpeta para contener el DSL.
> mkdir shaperdsl
> cd shaperdsl
Crea un archivo
build.gradle
. Este archivo es necesario para enumerar las dependencias del proyecto y configurar tareas adicionales de Gradle. Si desea reutilizar este archivo, solo necesita cambiar los espacios de nombres y la clase principal.
> touch build.gradle
A continuación se muestra el contenido del archivo:
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
}
Analizador de idiomas
El analizador está construido como gramática ANTLR .
mkdir -p src/main/antlr
touch src/main/antlr/ShaperDSL.g4
con el siguiente contenido:
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';
Ahora puedes ver cómo la estructura del lenguaje se vuelve más clara. Para generar el código fuente de la gramática, ejecute:
> gradle generateGrammarSource
Como resultado, obtendrá el código generado en formato
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
Árbol de sintaxis abstracta
El analizador convierte el código fuente en un árbol de objetos. El árbol de objetos es lo que el compilador usa como fuente de datos. Para obtener el AST, primero debe definir el metamodelo del árbol.
> mkdir -p src/main/kotlin/com/example/shaperdsl/ast
> touch src/main/kotlin/com/example/shaper/ast/MetaModel.kt
MetaModel.kt
contiene las definiciones de las clases de objetos utilizadas en el lenguaje, comenzando por la raíz. Todos heredan del Node . La jerarquía del árbol es visible en la definición de clase.
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
A continuación, debe hacer coincidir la clase con ASD:
> touch src/main/kotlin/com/example/shaper/ast/Mapping.kt
Mapping.kt
se usa para construir un AST usando las clases definidas en MetaModel.kt
, usando datos del analizador.
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)
El código en nuestro DSL:
img_dim:100,shp_dim:8>>>square,square|circle|triangle,circle,square<<<
Se convertirá al siguiente ASD:
Compilador
El compilador es la última parte. Utiliza ASD para obtener un resultado específico, en este caso, una imagen.
> mkdir -p src/main/kotlin/com/example/shaperdsl/compiler
> touch src/main/kotlin/com/example/shaper/compiler/Shaper2Image.kt
Hay mucho código en este archivo. Intentaré aclarar los puntos principales.
ShaperParserFacade
Es un contenedor en la parte superior ShaperAntlrParserFacade
que crea el AST real a partir del código fuente proporcionado.
Shaper2Image
es la clase de compilador principal. Después de recibir el AST del analizador, pasa por todos los objetos que contiene y crea objetos gráficos, que luego inserta en la imagen. Luego devuelve la representación binaria de la imagen. También hay una función main
en el objeto complementario de la clase para permitir las pruebas.
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)
}
}
}
Ahora que todo está listo, construyamos el proyecto y obtengamos un archivo jar con todas las dependencias ( uber jar ).
> gradle shadowJar
> ls build/libs
shaper-dsl-all.jar
Pruebas
Todo lo que tenemos que hacer es verificar si todo funciona, así que intente ingresar este código:
> 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
Se creará un archivo:
.png
que se verá así:
Conclusión
Este es un DSL simple, no es seguro y probablemente se romperá si se usa incorrectamente. Sin embargo, se adapta bien a mi propósito y puedo usarlo para crear cualquier cantidad de muestras de imágenes únicas. Se puede ampliar fácilmente para obtener más flexibilidad y se puede utilizar como plantilla para otras DSL.
Se puede encontrar un ejemplo completo de DSL en mi repositorio de GitHub: github.com/cosmincatalin/shaper .