Русский
Русский
English
Статистика
Реклама

Перевод Создание DSL для генерации изображений

Привет, Хабр! Считанные дни остаются до запуска нового курса от OTUS Backend-разработка на Kotlin. В преддверии старта курса мы подготовили для вас перевод еще одного интересного материала.



Часто при решении задач, связанных с компьютерным зрением, недостаток данных становится большой проблемой. Это особенно актуально при работе с нейронными сетями.

Как было бы здорово, будь у нас безграничный источник новых оригинальных данных?

Эта мысль натолкнула меня на разработку предметно-ориентированного языка (Domain Specific Language), который позволяет создавать изображения в различных конфигурациях. Эти изображения можно использовать для обучения и тестирования моделей машинного обучения. Как следует из названия, генерируемые DSL изображения обычно могут использоваться только в узко направленной области.

Требования к языку


В моем конкретном случае необходимо сосредоточиться на обнаружении объектов. Компилятор языка должен генерировать изображения, соответствующие следующим критериям:

  • изображения содержат различные формы (например, смайлики);
  • количество и положение отдельных фигур настраивается;
  • размер изображения и форм настраивается.

Сам язык должен быть максимально простым. Сначала я хочу определить размер выходного изображения, а затем размер фигур. После этого я хочу выразить фактическую конфигурацию изображения. Чтобы упростить задачу, я рассматриваю изображение как таблицу, где каждая фигура помещается в ячейку. Каждый новый ряд заполняется формами слева направо.

Реализация


Для создания DSL я выбрал комбинацию ANTLR, Kotlin и Gradle. 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 generateGrammarSourcejar {    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/antlrtouch 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

Абстрактное синтаксическое дерево


Парсер преобразует исходный код в дерево объектов. Дерево объектов это то, что компилятор использует в качестве источника данных. Чтобы получить АСД, сначала необходимо определить метамодель дерева.

> 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.astinterface Nodedata class Shaper(val img_dim: Int, val shp_dim: Int, val rows: List<Row>): Nodedata class Row(val shapes: List<Shape>): Nodedata class Shape(val type: String): Node

Далее необходимо сопоставить класс с АСД:

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

Mapping.kt используется для построения АСД с использованием классов, определенных в MetaModel.kt, используя данные от парсера.

package com.example.shaperdsl.astimport com.example.shaperdsl.ShaperDSLParserfun 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<<<

Будет преобразован к следующему АСД:



Компилятор


Компилятор это последняя часть. Он использует АСД для получения конкретного результата, в данном случае, изображения.

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

В этом файле много кода. Я постараюсь пояснить основные моменты.

ShaperParserFacade это оболочка поверх ShaperAntlrParserFacade, которая создает фактическое АСД из предоставленного исходного кода.

Shaper2Image является основным классом компилятора. После того, как он получает АСД от парсера, он проходит по всем объектам внутри него и создает графические объекты, которые затем вставляет в изображение. Затем он возвращает двоичное представление изображения. Также предусмотрена функция main в объекте-компаньоне класса, позволяющая проводить тестирование.

package com.example.shaperdsl.compilerimport com.audienceproject.util.cli.Argumentsimport com.example.shaperdsl.ShaperDSLLexerimport com.example.shaperdsl.ShaperDSLParserimport com.example.shaperdsl.ast.Shaperimport com.example.shaperdsl.ast.toAstimport org.antlr.v4.runtime.CharStreamsimport org.antlr.v4.runtime.CommonTokenStreamimport org.antlr.v4.runtime.TokenStreamimport java.awt.Colorimport java.awt.image.BufferedImageimport java.io.ByteArrayInputStreamimport java.io.ByteArrayOutputStreamimport java.io.Fileimport java.io.InputStreamimport javax.imageio.ImageIOobject 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/libsshaper-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.

Читать ещё


Источник: habr.com
К списку статей
Опубликовано: 22.07.2020 16:08:14
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании otus. онлайн-образование

Kotlin

Машинное обучение

Программирование

Dsl

Нейронные сети

Компьютерное зрение

Категории

Последние комментарии

© 2006-2021, personeltest.ru