创建用于生成图像的DSL

哈Ha!距离OTUS推出新课程“科特林后端开发”还有几天在课程开始前夕,我们已为您准备了另一本有趣的材料的翻译。












通常,在解决与计算机视觉有关的问题时,数据的缺乏成为一个大问题。在使用神经网络时尤其如此。



如果我们有无限的新原始数据源,那会多么酷?



这种想法促使我开发一种域特定语言,该语言可让您以各种配置创建图像。这些图像可用于训练和测试机器学习模型。顾名思义,生成的DSL图像通常只能在狭窄的区域中使用。



语言要求



在我的特定情况下,我需要专注于对象检测。语言编译器必须生成满足以下条件的图像:



  • 图像包含不同形式(例如,表情符号);
  • 个人数字的数量和位置是可定制的;
  • 图像大小和形状是可定制的。


语言本身应尽可能简单。我想先确定输出图像的大小,然后再确定形状的大小。然后,我想表达图像的实际配置。为简单起见,我将图像视为一张桌子,其中每种形状都适合一个单元格。每个新行都从左到右填充表格。



实作



我选择了ANTLR,Kotlin和Gradle的组合来创建DSL ANTLR是解析器生成器。Kotlin是类似于Scala的类似于JVM的语言。Gradle是一个类似于的构建系统sbt



必要的环境



您将需要Java 1.8和Gradle 4.6来完成所描述的步骤。



初始设置



创建一个文件夹以包含DSL。



> mkdir shaperdsl
> cd shaperdsl


创建一个文件build.gradle需要此文件来列出项目依赖项并配置其他Gradle任务。如果要重用此文件,则只需更改名称空间和主类。



> touch build.gradle


以下是文件的内容:



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
}


语言解析器



解析器的构建类似于ANTLR语法



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


具有以下内容:



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';


现在您可以看到语言的结构如何变得更加清晰。要生成语法源代码,请运行:



> gradle generateGrammarSource


结果,您将在中获得生成的代码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


抽象语法树



解析器将源代码转换为对象树。对象树是编译器用作数据源的对象。要获取AST,您首先需要定义树元模型。



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


MetaModel.kt从根开始包含该语言中使用的对象类的定义。它们都继承自Node树的层次结构在类定义中可见。



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


接下来,您需要将类别与ASD相匹配:



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


Mapping.kt用于MetaModel.kt使用解析器中的数据使用中定义的类来构建AST



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)


DSL上的代码:



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


将转换为以下ASD:







编译器



编译器是最后一部分。他使用ASD获得特定结果,在这种情况下为图像。



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


该文件中有很多代码。我将尝试澄清要点。



ShaperParserFacade是位于顶部的包装程序ShaperAntlrParserFacade,可根据提供的源代码构建实际的AST。



Shaper2Image是主要的编译器类。从解析器收到AST之后,它将遍历其中的所有对象并创建图形对象,然后将其插入图像中。然后,它返回图像的二进制表示形式。main该类的伴随对象中还有一个函数可以进行测试。



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)
        }
    }
}


现在一切就绪,让我们构建项目并获取具有所有依赖项的jar文件(uber jar)。



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


测试中



我们要做的就是检查是否一切正常,因此请尝试输入以下代码:



> 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


将创建一个文件:



.png


看起来像这样:







结论



这是一个简单的DSL,不安全,如果使用不当,可能会损坏。但是,它非常适合我的目的,并且我可以使用它来创建任意数量的唯一图像样本。它可以轻松地扩展以获得更大的灵活性,并且可以用作其他DSL的模板。



可以在我的GitHub存储库中找到完整的DSL示例:github.com/cosmincatalin/shaper



阅读更多






All Articles