Membuat DSL untuk menghasilkan gambar

Halo, Habr! Beberapa hari tersisa hingga peluncuran kursus baru dari OTUS "Pengembangan Backend di Kotlin" . Menjelang permulaan kursus, kami telah menyiapkan terjemahan dari materi menarik lainnya untuk Anda.












Seringkali saat menyelesaikan masalah yang berhubungan dengan computer vision, kekurangan data menjadi masalah besar. Ini terutama berlaku saat bekerja dengan jaringan neural.



Betapa hebatnya jika kita memiliki sumber data asli baru yang tak terbatas?



Pemikiran ini mendorong saya untuk mengembangkan Bahasa Khusus Domain yang memungkinkan Anda membuat gambar dalam berbagai konfigurasi. Gambar ini dapat digunakan untuk melatih dan menguji model pembelajaran mesin. Seperti namanya, gambar DSL yang dihasilkan biasanya hanya dapat digunakan di area dengan fokus yang sempit.



Persyaratan bahasa



Dalam kasus khusus saya, saya perlu fokus pada deteksi objek. Penyusun bahasa harus menghasilkan gambar yang memenuhi kriteria berikut:



  • gambar berisi berbagai bentuk (misalnya, emotikon);
  • jumlah dan posisi figur individu dapat disesuaikan;
  • ukuran dan bentuk gambar dapat disesuaikan.


Bahasanya sendiri harus sesederhana mungkin. Saya ingin menentukan ukuran gambar keluaran terlebih dahulu dan kemudian ukuran bentuknya. Kemudian saya ingin mengungkapkan konfigurasi gambar yang sebenarnya. Untuk menyederhanakannya, saya menganggap gambar sebagai tabel, di mana setiap bentuk dapat dimasukkan ke dalam sel. Setiap baris baru diisi dengan formulir dari kiri ke kanan.



Penerapan



Saya memilih kombinasi ANTLR, Kotlin dan Gradle untuk membuat DSL . ANTLR adalah generator parser. Kotlin adalah bahasa mirip JVM yang mirip dengan Scala. Gradle adalah sistem build yang mirip dengan sbt.



Lingkungan yang diperlukan



Anda memerlukan Java 1.8 dan Gradle 4.6 untuk menyelesaikan langkah-langkah yang dijelaskan.



Pengaturan awal



Buat folder untuk menampung DSL.



> mkdir shaperdsl
> cd shaperdsl


Buat file build.gradle. File ini diperlukan untuk membuat daftar dependensi project dan mengonfigurasi tugas Gradle tambahan. Jika Anda ingin menggunakan kembali file ini, Anda hanya perlu mengubah namespace dan kelas utama.



> touch build.gradle


Di bawah ini adalah isi dari file tersebut:



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
}


Pengurai bahasa



Parser dibangun seperti tata bahasa ANTLR .



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


dengan konten berikut:



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


Sekarang Anda bisa melihat bagaimana struktur bahasanya menjadi lebih jelas. Untuk menghasilkan kode sumber tata bahasa, jalankan:



> gradle generateGrammarSource


Hasilnya, Anda akan mendapatkan kode yang dihasilkan 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


Pohon sintaks abstrak



Parser mengubah kode sumber menjadi pohon objek. Pohon objek adalah apa yang digunakan kompilator sebagai sumber data. Untuk mendapatkan AST, pertama-tama Anda harus menentukan metamodel pohon.



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


MetaModel.ktberisi definisi kelas objek yang digunakan dalam bahasa, dimulai dari root. Mereka semua mewarisi dari Node . Hierarki pohon terlihat dalam definisi kelas.



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


Selanjutnya, Anda harus mencocokkan kelas dengan ASD:



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


Mapping.ktdigunakan untuk membuat AST menggunakan kelas yang ditentukan di MetaModel.kt, menggunakan data dari parser.



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)


Kode di DSL kami:



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


Akan dikonversi ke ASD berikut:







Penyusun



Kompiler adalah bagian terakhir. Ia menggunakan ASD untuk mendapatkan hasil tertentu, dalam hal ini berupa gambar.



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


Ada banyak kode di file ini. Saya akan mencoba menjelaskan poin-poin utama.



ShaperParserFacadeAdalah pembungkus di atas ShaperAntlrParserFacadeyang membangun AST sebenarnya dari kode sumber yang disediakan.



Shaper2Imageadalah kelas kompiler utama. Setelah menerima AST dari parser, ia melewati semua objek di dalamnya dan membuat objek grafis, yang kemudian dimasukkan ke dalam gambar. Kemudian mengembalikan representasi biner gambar. Ada juga fungsi maindi objek pendamping kelas untuk memungkinkan pengujian.



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


Sekarang semuanya sudah siap, mari buat proyek dan dapatkan file jar dengan semua dependensi ( uber jar ).



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


Menguji



Yang harus kita lakukan adalah memeriksa apakah semuanya berfungsi, jadi coba masukkan kode ini:



> 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


File akan dibuat:



.png


yang akan terlihat seperti ini:







Kesimpulan



Ini adalah DSL sederhana, tidak aman, dan mungkin akan rusak jika disalahgunakan. Namun, ini sangat sesuai dengan tujuan saya dan saya dapat menggunakannya untuk membuat sejumlah sampel gambar unik. Ini dapat dengan mudah diperpanjang untuk fleksibilitas lebih dan dapat digunakan sebagai template untuk DSL lainnya.



Contoh DSL lengkap dapat ditemukan di repositori GitHub saya: github.com/cosmincatalin/shaper .



Baca lebih banyak






All Articles