多くの場合、コンピュータービジョンに関連する問題を解決するとき、データの不足が大きな問題になります。これは、ニューラルネットワークを使用する場合に特に当てはまります。
新しいオリジナルデータの無限のソースがあったら、どれほどクールでしょうか?
この考えから、さまざまな構成で画像を作成できるドメイン固有の言語を開発するようになりました。これらの画像は、機械学習モデルのトレーニングとテストに使用できます。名前が示すように、DSLで生成された画像は通常、焦点の狭い領域でのみ使用できます。
言語要件
私の特定のケースでは、オブジェクトの検出に焦点を当てる必要があります。言語コンパイラは、次の条件を満たす画像を生成する必要があります。
- 画像にはさまざまな形式(たとえば、絵文字)が含まれています。
- 個々の図の数と位置はカスタマイズ可能です。
- 画像のサイズと形状はカスタマイズ可能です。
言語自体はできるだけ単純にする必要があります。最初に出力画像のサイズを決定し、次に形状のサイズを決定したいと思います。次に、画像の実際の構成を表現したいと思います。簡単にするために、画像は各形状がセルに収まるテーブルと考えています。新しい各行には、左から右にフォームが入力されます。
実装
DSLを作成するために、ANTLR、Kotlin、およびGradleの組み合わせを選択しました。ANTLRはパーサージェネレーターです。Kotlinは、Scalaに似たJVMに似た言語です。Gradleは、に似たビルドシステム
sbt
です。
必要な環境
説明されている手順を完了するには、Java1.8とGradle4.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
ルートから始めて、言語で使用されるオブジェクトクラスの定義が含まれています。それらはすべてノードから継承します。ツリー階層は、クラス定義に表示されます。
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
このファイルにはたくさんのコードがあります。要点を明確にしようと思います。提供されたソースコードから実際のASTを構築
ShaperParserFacade
するラッパーShaperAntlrParserFacade
です。
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のテンプレートとして使用できます。
完全なDSLの例は、私のGitHubリポジトリ(github.com/cosmincatalin/shaper)にあります。