Home About Contact
JSON , Kotlin , SVG , Image Manipulation

Quick, Draw! のデータをパースして SVG に変換

Quick, Draw! というプロジェクトがあります。 ここで描かれた落書きデータの入手方法がこちらで説明されているので、kotlin でパースして SVG に変換してみました。

ゆくゆく ss one のプロジェクトで、 これを何かに活用できないかとは思っている。

例によって kotlinc を使います。

$ kotlinc -version
info: kotlinc-jvm 1.8.10 (JRE 17.0.7+7-Ubuntu-0ubuntu122.04.2)

まず手書きデータの入手方法ですが、詳しくはこちらを見ていただくとして、とりあえず以下のようにすれば dog.ndjson を入手できます。

$ gsutil -m cp gs://quickdraw_dataset/full/simplified/dog.ndjson .

gsutil コマンドは snap で入れました。(Ubuntu 22.04 を使用)

dog.ndjson の一行目を見ると、以下のようになっています。

$ head -1 dog.ndjson | jq . | head -7
{
  "word": "dog",
  "countrycode": "US",
  "timestamp": "2017-03-01 21:44:26.60176 UTC",
  "recognized": true,
  "key_id": "6718004173733888",
  "drawing": [

ndjson なので、行ごとに json データになっています。

drawing を取得

まずは、この drawing の値を取得してみます。

最初のステップとして、単に dog.ndjson を読んで先頭一行だけを取り出すコード。

dog.main.kts

import java.io.File
import java.io.FileInputStream

val file = File("dog.ndjson")
val firstLine = FileInputStream(file).bufferedReader(Charsets.UTF_8).useLines { lineSequences->
    lineSequences.first()
}
println(firstLine)

実行します。

$ kotlinc -script dog.main.kts
{"word":"dog","countrycode":"US",...

うまくいきました。

一行目がJSONになっているので、org.json を使ってパースします。

@file:Repository("https://repo1.maven.org/maven2/")
@file:DependsOn("org.json:json:20230618")

import org.json.*
import java.io.File
import java.io.FileInputStream

val file = File("dog.ndjson")
val firstLine = FileInputStream(file).bufferedReader(Charsets.UTF_8).useLines { lineSequences->
    lineSequences.first()
}

val rootJsonObject = JSONObject(firstLine)
val drawing = rootJsonObject.getJSONArray("drawing")
println(drawing)

実行します。

$ kotlinc -script dog.main.kts
[[[115,110,111,130,132,125],[45,52,60,60,51,42]],[[177,173,172,181,201,200,192,173],[44,47,55,63,49,45,39,35]],[[136,155,158,158,155,141,141],[84,80,84,92,100,95,88]],[[149,147,140,125,110],[101,128,140,151,141]],[[149,150,155,163,175,181,180],[103,143,154,162,162,150,141]],[[133,139,144,153,162],[150,168,175,176,167]],[[113,100,84,74,66,65,75,87,126,153,191,204,220,254,255,248,238,226,190,138,98],[8,7,11,22,50,79,124,152,189,203,203,194,173,103,77,56,40,28,10,5,15]],[[82,57,31,17,4,0,0,6,42,55,67],[14,5,0,7,22,36,70,92,54,46,45]],[[235,242],[25,16]]]

drawings 値のパース

drawings の値は、まず ひとつの stroke ごとに分割されていて、その stroke 内には x 座標のリスト(JSONArray)と y 座標のリスト(JSONArray)が入っています。

詳しくはこちらをご覧ください。

今取得できた drawing のタイプ(というかクラス)は org.json.JSONArray になっているので... 以下のようにして、strokeArraystrokeList に変換します。

typealias Stroke = List<Pair<Int,Int>>

val strokeArray = 0.until(drawing.length()).map { drawing.getJSONArray(it) }
val strokeList: List<Stroke> = strokeArray.map {
    val xList: List<Int> = it.getJSONArray(0).toList()
    val yList: List<Int> = it.getJSONArray(1).toList()
    xList.zip(yList)
}

ただ、これを実行すると以下の部分でエラーになります。

val xList: List<Int> = it.getJSONArray(0).toList()

こちらは List<Int> を期待していますが、実際は List<Any!>! が返ることになるので、 kotlinc にだめだしされます。

そこで、(無理やり)意図通りに List<Int> が返るように修正します。

val xList: List<Int> = it.getJSONArray(0).toList().map { "${it}".toInt() }

これでOKです。

strokeList: List<Stroke> を得ることができたので、あとはこれを SVG の Path に変換すればよい。

Stroke を SVG の Path 要素(文字列)に変換する関数:

val toPath: (Stroke)->String = {stroke->
    val head = stroke.first()
    val tail = stroke.drop(1)
    val data = listOf(
        listOf("M ${head.first} ${head.second}"),
        tail.map { "L ${it.first} ${it.second}" }
    ).flatten().joinToString(" ")

    "<path d=\"${data}\"/>"
}

この toPath 関数を使って、strokeList を SVG に変換。

printlnn( strokeList.map { toPath(it) }.joinToString("\n") )

実行します。

$ kotlinc -script dog.main.kts
<path d="M 115 45 L 110 52 L 111 60 L 130 60 L 132 51 L 125 42"/>
<path d="M 177 44 L 173 47 L 172 55 L 181 63 L 201 49 L 200 45 L 192 39 L 173 35"/>
<path d="M 136 84 L 155 80 L 158 84 L 158 92 L 155 100 L 141 95 L 141 88"/>
<path d="M 149 101 L 147 128 L 140 140 L 125 151 L 110 141"/>
<path d="M 149 103 L 150 143 L 155 154 L 163 162 L 175 162 L 181 150 L 180 141"/>
<path d="M 133 150 L 139 168 L 144 175 L 153 176 L 162 167"/>
<path d="M 113 8 L 100 7 L 84 11 L 74 22 L 66 50 L 65 79 L 75 124 L 87 152 L 126 189 L 153 203 L 191 203 L 204 194 L 220 173 L 254 103 L 255 77 L 248 56 L 238 40 L 226 28 L 190 10 L 138 5 L 98 15"/>
<path d="M 82 14 L 57 5 L 31 0 L 17 7 L 4 22 L 0 36 L 0 70 L 6 92 L 42 54 L 55 46 L 67 45"/>
<path d="M 235 25 L 242 16"/>

できました。

あとは、SVG用のヘッダとフッダ要素を追加したら完成です。

完成した dog.main.kts:

@file:Repository("https://repo1.maven.org/maven2/")
@file:DependsOn("org.json:json:20230618")

import org.json.*
import java.io.File
import java.io.FileInputStream

typealias Stroke = List<Pair<Int,Int>>

val toPath: (Stroke)->String = {stroke->
    val head = stroke.first()
    val tail = stroke.drop(1)
    val data = listOf(
        listOf("M ${head.first} ${head.second}"),
        tail.map { "L ${it.first} ${it.second}" }
    ).flatten().joinToString(" ")

    "<path d=\"${data}\"/>"
}


val file = File("dog.ndjson")
val firstLine = FileInputStream(file).bufferedReader(Charsets.UTF_8).useLines { lineSequences->
    lineSequences.first()
}

val rootJsonObject = JSONObject(firstLine)
val drawing = rootJsonObject.getJSONArray("drawing")

val strokeArray = 0.until(drawing.length()).map { drawing.getJSONArray(it) }
val strokeList: List<Stroke> = strokeArray.map {
    val xList: List<Int> = it.getJSONArray(0).toList().map { "${it}".toInt() }
    val yList: List<Int> = it.getJSONArray(1).toList().map { "${it}".toInt() }
    xList.zip(yList)
}

val w = 256
val h = 256
val header = listOf(
    "<?xml version=\"1.0\" encoding=\"utf-8\"?>",
    "\n",
    "<!DOCTYPE svg PUBLIC ",
        "\"-//W3C//DTD SVG 1.1//EN\" ",
        "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">",
    "<svg ",
        "xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" ",
        "x=\"0px\" y=\"0px\" width=\"${w}px\" height=\"${h}px\" ",
        "viewBox=\"0.0 0.0 ${w} ${h}\">")

val g = listOf(
    "<g stroke=\"rgb(0, 0, 0)\" stroke-width=\"0.254\" fill=\"none\">",
    strokeList.map { toPath(it) }.joinToString(""),
    "</g>")
val footer = listOf("</svg>")

val svg = listOf(header , g , footer).flatten().joinToString("")

File("dog.svg").writer(Charsets.UTF_8).use { writer-> writer.write(svg) }

dog.svg:

a dog

できました。