通常,在解决与计算机视觉有关的问题时,数据的缺乏成为一个大问题。在使用神经网络时尤其如此。
如果我们有无限的新原始数据源,那会多么酷?
这种想法促使我开发一种域特定语言,该语言可让您以各种配置创建图像。这些图像可用于训练和测试机器学习模型。顾名思义,生成的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。