Making a DSL for Generating Images

6 minute read

Very often when trying to work on computer vision problems, the lack of sufficient data becomes a big issue. This is especially true when working with neural networks.

Would it not be great if we could have a limitless source of new and original data?

This thought has led me to create a Domain Specific Language that allows the creation of images in various configurations. These images can then be used for training and testing machine learning models. Now, as the name suggests, the images DSL generate can usually be used only in a narrow domain.

Requirements

For my particular case, I focus on object detection. The compiler of the language must generate images that fulfill the following criteria:

  • images contain various shapes (think emoji).
  • the number and position of individual shapes are configurable.
  • size of the image and of the shapes is configurable.

The language itself must be as simple as possible. First I want to be able to define the size of the output image and then the size of the shapes. After that, I want to express the actual configuration of the image. To make things easier, I think of the image as being a table, so each shape can go in a cell. Each new row starts from the left and then it gets filled with shapes.

Implementation

To build my DSL I have chosen to use a combination of ANTLR, Kotlin and Gradle. ANTLR is a parser generator. Kotlin is a JVM based language similar to Scala. Gradle is build system similar to sbt.

Prerequisites

To follow the tutorial, you will need to have Java 1.8 and Gradle 4.6.

Initial set-up

Create a folder that will contain the DSL.

> mkdir shaperdsl
> cd shaperdsl

Create the build.gradle file. This file is used to list the dependencies of the project and to configure additional Gradle tasks. If you want to reuse this file you should typically modify only the namespaces and the main class.

> touch build.gradle

Here is the content of the file

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
}

The language parser

The parser is built as ANTLR grammar.

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

with the following content:

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

You can see how the language internals now become clear. To generate the grammar source code run:

> gradle generateGrammarSource

You will end up with the generated code in build/generated-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

Abstract Syntax Tree

The parser transforms the source code into a tree of objects. The tree of objects is what the compiler uses as the data source. To obtain the AST, a meta model of the tree needs to be defined first.

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

MetaModel.kt contains the definitions for the classes of objects used in the language, starting with the root. They all inherit from a Node interface. The tree hierarchy is visible in the class definition.

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

Next is the mapping to the AST

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

Mapping.kt is used to build the AST by using the classes defined in the MetaModel.kt along with the input from the 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)

Here is a a graphical representation of an AST:

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

translates to:

AST

The compiler

The compiler is the last part. It uses the AST to create a concrete representation in the desired format, in this case, an image.

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

There is a lot cramped down in this file. I will try to break it down.

ShaperParserFacade is a wrapper on top of ShaperAntlrParserFacade which constructs the actual AST from the source code provided. Shaper2Image is the main compiler class. After it receives the AST from the parser, it goes through the all the objects inside it and creates graphical objects that it then inserts them inside a container image. It then finally returns the binary representation of the image. A main function in the class’ companion is provided as well to allow testing.

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

Now that everything is ready, the project can be built. An uber jar is created.

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

Testing

All we have to do now is test if everything works, so try feeding some code like this:

> 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

A .png file is created that will look like this:

Test

Conclusion

This is a simple DSL, it is not hardened and will probably break if used outside the way it is intended. However, it fits my purpose very well and I can use it to generate any desired number of unique sample images. It can easily be extended to be more configurable and can be used as a template for other DSL.

A complete example of the DSL can be found in my GitHub repository: https://github.com/cosmincatalin/shaper.

Updated:

Comments