إنشاء DSL لتوليد الصور

مرحبا هبر! تبقى بضعة أيام حتى إطلاق دورة جديدة من OTUS "Backend development on Kotlin" . عشية بدء الدورة ، قمنا بإعداد ترجمة لمواد أخرى مثيرة للاهتمام.












في كثير من الأحيان عند حل المشكلات المتعلقة برؤية الكمبيوتر ، يصبح نقص البيانات مشكلة كبيرة. هذا صحيح بشكل خاص عند العمل مع الشبكات العصبية.



كم سيكون رائعًا إذا كان لدينا مصدر غير محدود للبيانات الأصلية الجديدة؟



دفعني هذا الفكر إلى تطوير لغة خاصة بالمجال تسمح لك بإنشاء صور في تكوينات مختلفة. يمكن استخدام هذه الصور لتدريب نماذج التعلم الآلي واختبارها. كما يوحي الاسم ، لا يمكن استخدام صور DSL التي تم إنشاؤها عادةً إلا في منطقة ذات تركيز ضيق.



متطلبات اللغة



في حالتي الخاصة ، أحتاج إلى التركيز على اكتشاف الكائن. يجب أن يقوم برنامج التحويل البرمجي للغة بإنشاء صور تستوفي المعايير التالية:



  • تحتوي الصور على أشكال مختلفة (على سبيل المثال ، الرموز) ؛
  • يمكن تخصيص عدد وموضع الشخصيات الفردية ؛
  • حجم الصورة والأشكال قابلة للتخصيص.


يجب أن تكون اللغة نفسها بسيطة قدر الإمكان. أريد تحديد حجم الصورة الناتجة أولاً ثم حجم الأشكال. ثم أريد أن أعبر عن التكوين الفعلي للصورة. لتبسيط الأمور ، أفكر في الصورة كجدول ، حيث يتناسب كل شكل في خلية. يتم ملء كل صف جديد بالنماذج من اليسار إلى اليمين.



التنفيذ



لقد اخترت مجموعة من ANTLR و Kotlin و Gradle لإنشاء DSL . ANTLR هو مولد محلل. Kotlin هي لغة تشبه JVM تشبه Scala. 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يحتوي على تعريفات لفئات الكائنات المستخدمة في اللغة ، بدءًا من الجذر. كلهم يرثون من العقدة . التسلسل الهرمي للشجرة مرئي في تعريف الفئة.



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يستخدم لبناء AST باستخدام الفئات المحددة في MetaModel.kt، باستخدام البيانات من المحلل اللغوي.



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


الآن بعد أن أصبح كل شيء جاهزًا ، فلنقم ببناء المشروع والحصول على ملف جرة بجميع التبعيات ( 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 بسيط وغير مؤمن ومن المحتمل أن ينكسر إذا أسيء استخدامه. ومع ذلك ، فهو يناسب هدفي جيدًا ويمكنني استخدامه لإنشاء أي عدد من عينات الصور الفريدة. يمكن تمديده بسهولة لمزيد من المرونة ويمكن استخدامه كنموذج لـ DSLs الأخرى.



يمكن العثور على مثال كامل لـ DSL في مستودع GitHub الخاص بي: github.com/cosmincatalin/shaper .



اقرأ أكثر






All Articles