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 の値を取得してみます。
最初のステップとして、単に 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 の値は、まず ひとつの stroke ごとに分割されていて、その stroke 内には x 座標のリスト(JSONArray)と y 座標のリスト(JSONArray)が入っています。
詳しくはこちらをご覧ください。
今取得できた drawing のタイプ(というかクラス)は org.json.JSONArray になっているので... 以下のようにして、strokeArray を strokeList に変換します。
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:
できました。