画像を生成するためのDSLの作成

こんにちは、Habr!OTUS 「Kotlinのバックエンド開発」から新しいコースが開始されるまで、数日かかります。コース開始の前夜に、別の興味深い資料の翻訳を用意しました。












多くの場合、コンピュータービジョンに関連する問題を解決するとき、データの不足が大きな問題になります。これは、ニューラルネットワークを使用する場合に特に当てはまります。



新しいオリジナルデータの無限のソースがあったら、どれほどクールでしょうか?



この考えから、さまざまな構成で画像を作成できるドメイン固有の言語を開発するようになりました。これらの画像は、機械学習モデルのトレーニングとテストに使用できます。名前が示すように、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)にあります。



続きを読む






All Articles