Часто при решении задач, связанных с компьютерным зрением, недостаток данных становится большой проблемой. Это особенно актуально при работе с нейронными сетями.
Как было бы здорово, будь у нас безграничный источник новых оригинальных данных?
Эта мысль натолкнула меня на разработку предметно-ориентированного языка (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 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
Абстрактное синтаксическое дерево
Парсер преобразует исходный код в дерево объектов. Дерево объектов — это то, что компилятор использует в качестве источника данных. Чтобы получить АСД, сначала необходимо определить метамодель дерева.
> 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.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
Далее необходимо сопоставить класс с АСД:
> touch src/main/kotlin/com/example/shaper/ast/Mapping.kt
Mapping.kt используется для построения АСД с использованием классов, определенных в 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<<<
Будет преобразован к следующему АСД:

Компилятор
Компилятор — это последняя часть. Он использует АСД для получения конкретного результата, в данном случае, изображения.
> 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.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.
Читать ещё
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/512058/
Добавить комментарий