Freqüentemente, ao resolver problemas relacionados à visão computacional, a falta de dados se torna um grande problema. Isso é especialmente verdadeiro ao trabalhar com redes neurais.
Não seria legal se tivéssemos uma fonte ilimitada de novos dados originais?
Esse pensamento me levou a desenvolver uma Linguagem Específica de Domínio, que permite criar imagens em várias configurações. Essas imagens podem ser usadas para treinar e testar modelos de aprendizado de máquina. Como o nome sugere, as imagens geradas por DSL geralmente só podem ser usadas em uma área com foco estreito.
Requisitos de idioma
No meu caso específico, preciso me concentrar na detecção de objetos. O compilador de linguagem deve gerar imagens que atendam aos seguintes critérios:
- as imagens contêm diferentes formas (por exemplo, emoticons);
- o número e a posição das figuras individuais são personalizáveis;
- o tamanho e as formas da imagem são personalizáveis.
A linguagem em si deve ser o mais simples possível. Quero determinar o tamanho da imagem de saída primeiro e depois o tamanho das formas. Em seguida, quero expressar a configuração real da imagem. Para manter as coisas simples, penso na imagem como uma mesa, onde cada forma se encaixa em uma célula. Cada nova linha é preenchida com formulários da esquerda para a direita.
Implementação
Escolhi uma combinação de ANTLR, Kotlin e Gradle para criar o DSL . ANTLR é um gerador de analisador. Kotlin é uma linguagem semelhante a JVM semelhante ao Scala. Gradle é um sistema de compilação semelhante ao
sbt
.
Ambiente necessário
Você precisará do Java 1.8 e do Gradle 4.6 para concluir as etapas descritas.
Configuração inicial
Crie uma pasta para conter o DSL.
> mkdir shaperdsl
> cd shaperdsl
Crie um arquivo
build.gradle
. Este arquivo é necessário para listar as dependências do projeto e configurar tarefas adicionais do Gradle. Se você quiser reutilizar este arquivo, você só precisa alterar os namespaces e a classe principal.
> touch build.gradle
Abaixo está o conteúdo do arquivo:
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
}
Analisador de linguagem
O analisador é construído como a gramática ANTLR .
mkdir -p src/main/antlr
touch src/main/antlr/ShaperDSL.g4
com o seguinte conteúdo:
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';
Agora você pode ver como a estrutura da linguagem fica mais clara. Para gerar o código-fonte da gramática, execute:
> gradle generateGrammarSource
Como resultado, você obterá o código gerado no
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
Árvore de sintaxe abstrata
O analisador converte o código-fonte em uma árvore de objetos. A árvore de objetos é o que o compilador usa como fonte de dados. Para obter o AST, primeiro você precisa definir o metamodelo da árvore.
> mkdir -p src/main/kotlin/com/example/shaperdsl/ast
> touch src/main/kotlin/com/example/shaper/ast/MetaModel.kt
MetaModel.kt
contém as definições das classes de objetos utilizadas na linguagem, a partir da raiz. Todos eles herdam do Nodo . A hierarquia da árvore é visível na definição da 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
Em seguida, você precisa combinar a classe com ASD:
> touch src/main/kotlin/com/example/shaper/ast/Mapping.kt
Mapping.kt
é usado para construir um AST usando as classes definidas em MetaModel.kt
, usando dados do analisador.
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)
O código em nosso DSL:
img_dim:100,shp_dim:8>>>square,square|circle|triangle,circle,square<<<
Será convertido para o seguinte ASD:
Compilador
O compilador é a última parte. Ele usa o ASD para obter um resultado específico, neste caso, uma imagem.
> mkdir -p src/main/kotlin/com/example/shaperdsl/compiler
> touch src/main/kotlin/com/example/shaper/compiler/Shaper2Image.kt
Há muito código neste arquivo. Vou tentar esclarecer os pontos principais.
ShaperParserFacade
É um invólucro superior ShaperAntlrParserFacade
que cria o AST real a partir do código-fonte fornecido.
Shaper2Image
é a principal classe do compilador. Depois de receber o AST do analisador, ele percorre todos os objetos dentro dele e cria objetos gráficos, que então insere na imagem. Em seguida, ele retorna a representação binária da imagem. Também há uma função main
no objeto complementar da classe para permitir o teste.
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)
}
}
}
Agora que tudo está pronto, vamos construir o projeto e obter um arquivo jar com todas as dependências ( uber jar ).
> gradle shadowJar
> ls build/libs
shaper-dsl-all.jar
Testando
Tudo o que precisamos fazer é verificar se tudo funciona, então tente inserir 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
Um arquivo será criado:
.png
que ficará assim:
Conclusão
Esta é uma DSL simples, não é segura e provavelmente quebrará se for usada incorretamente. No entanto, atende bem ao meu propósito e posso usá-lo para criar qualquer número de amostras de imagens exclusivas. Ele pode ser facilmente estendido para maior flexibilidade e pode ser usado como um modelo para outras DSLs.
Um exemplo completo de DSL pode ser encontrado em meu repositório GitHub: github.com/cosmincatalin/shaper .