Skip to main content

Serialization of Ink

Ink Serializationモジュールは、インクコンテンツと関連メタデータのエンコードおよびデコードを行うアルゴリズムを提供します。 これらのアルゴリズムは最適化されて、と連動します。Universal Ink Model.

アルゴリズムは次の機能をサポートします。

  • 高速なエンコードおよびデコード
  • コンパクトなファイルサイズ
  • オペレーティングシステムやデバイス間での移植
  • 各種コンテナ形式に組み込むことができるバイナリ表現

エンコードスキームの詳細を確認するには、here.

モデルの処理

The following illustrates how to manage the data used within the Universal Ink Modelをクリックしてください。 当該使用事例で情報が必要とされる場合、インクモデル内では、複数のデータレポジトリを、インクツリー、ビュー、およびKnowledge Graphとともに管理する必要があります。

Logical Parts of Ink Model.

Ink Model

最初のステップは、InkModel.

inkModel = InkModel()

入力コンテキストリポジトリ

入力コンテキストはセンサーデータの取得方法を記述します。 この情報はアプリケーションによって追加する必要がありますが、アプリケーションの使用事例によっては不要な場合もあります。 オプションで、センサーデータの保存もできます。

以下のセクションでは、コンテキストの定義方法の例をいくつか示します。

使用環境

センサーデータを生成したenvironment(オペレーティングシステムなど)は、次のように定義することができます。

import com.wacom.ink.format.input.*
...

// Initialize environment
environment = Environment()
environment.putProperty("os.name", "android")
environment.putProperty("os.version.name", Build.VERSION.CODENAME)
environment.putProperty("os.version.code", Build.VERSION.SDK_INT.toString())
environment.putProperty("os.version.incremental", Build.VERSION.INCREMENTAL)
environment.putProperty("os.version.release", Build.VERSION.RELEASE)
environment.putProperty("wacom.ink.sdk.name", activity.getString(R.string.sdk_name))
environment.putProperty("wacom.ink.sdk.version", activity.getString(R.string.sdk_version))

InkInputProvider

ink input provider は一般的な入力データソースを表し、これによってデータの生成方法(タッチ入力、マウス、スタイラス、ハードウェアコントローラの使用など)が特定されます。 アプリケーションは、プラットフォームおよびデバイスごとに、アプリケーション内で使用される入力デバイスを決定し、インクモデル内でシリアル化する必要があります。

import com.wacom.ink.format.input.*
...
val toolType = when (event.getToolType(0)) {
MotionEvent.TOOL_TYPE_STYLUS -> InkInputType.PEN
MotionEvent.TOOL_TYPE_FINGER -> InkInputType.TOUCH
MotionEvent.TOOL_TYPE_MOUSE -> InkInputType.MOUSE
else -> InkInputType.PEN
}

val provider = InkInputProvider(toolType)

入力デバイス

センサーデータの生成に使用したinput device です。 プラットフォームにより、使用可能な入力デバイスは複数存在する場合があります。 一部では、既にスタイラスを備えたタブレット端末やアクセサリとして使用可能な端末もあります。 次の属性は、プラットフォーム自体などによって提供された場合に取得できます。

import com.wacom.ink.format.input.*
...

// Initialize InputDevice
inputDevice = InputDevice()
inputDevice.putProperty("dev.id", Build.ID)
inputDevice.putProperty("dev.manufacturer", Build.MANUFACTURER)
inputDevice.putProperty("dev.brand", Build.BRAND)
inputDevice.putProperty("dev.model", Build.MODEL)
inputDevice.putProperty("dev.board", Build.BOARD)
inputDevice.putProperty("dev.hardware", Build.HARDWARE)
inputDevice.putProperty("dev.codename", Build.DEVICE)
inputDevice.putProperty("dev.display", Build.DISPLAY)
...

SensorContext

sensor contextは、デジタルインク入力の取得に使用される、センサチャンネルコンテキストの一意の組み合わせです。 この情報は、データを正規化する必要のある機械学習においてセンサーデータが使用される場合に役立つことがあります。

// Define which channels are used for sensor data collection
var inkSensorTypes = listOf(InkSensorType.X, InkSensorType.Y, InkSensorType.TIMESTAMP)
...
// Register the channels which are used for sensor data collection
val channels = registerChannels(inkSensorTypeUris)

// Define the context for the sensor channels
val sensorChannelsContext = SensorChannelsContext(provider.id, inputDevice.id, channels)
val sensorContext = SensorContext()
sensorContext.addSensorChannelsContext(sensorChannelsContext)

// Maintain a map of contexts by id
sensorContexts[sensorContext.id] = sensorContext

// Input context is combining environment and sensor context
val inputContext = InputContext(environment.id, sensorContext.id)

// Maintain a map of input context
inputContexts[inputContext.id] = inputContext
inputProviderToInputContextMapping[provider.id] = inputContext.id

...
private fun registerChannels(inkSensorTypeUris: List<String>): MutableList<SensorChannel> {
val precision = 2

val channels = mutableListOf<SensorChannel>()

val dimensions = getScreenDimensions()

for (type in inkSensorTypeUris) {
val channel = when (type) {
InkSensorType.X -> SensorChannel(
InkSensorType.X,
InkSensorMetricType.LENGTH,
ScalarUnit.INCH,
0.0f,
dimensions.x,
precision
)
InkSensorType.Y -> SensorChannel(
InkSensorType.Y,
InkSensorMetricType.LENGTH,
ScalarUnit.INCH,
0.0f,
dimensions.y,
precision
)
InkSensorType.Z -> SensorChannel(
InkSensorType.Z,
InkSensorMetricType.LENGTH,
ScalarUnit.DIP,
0.0f,
0.0f,
precision
)
InkSensorType.TIMESTAMP -> SensorChannel(
InkSensorType.TIMESTAMP,
InkSensorMetricType.TIME,
ScalarUnit.MILLISECOND,
0.0f,
0.0f,
precision
)
InkSensorType.PRESSURE -> SensorChannel(
InkSensorType.PRESSURE,
InkSensorMetricType.NORMALIZED,
ScalarUnit.NORMALIZED,
0.0f,
1.0f,
precision
)
InkSensorType.RADIUS_X -> SensorChannel(
InkSensorType.RADIUS_X,
InkSensorMetricType.LENGTH,
ScalarUnit.DIP,
0.0f,
0.0f,
precision
)
InkSensorType.RADIUS_Y -> SensorChannel(
InkSensorType.RADIUS_Y,
InkSensorMetricType.LENGTH,
ScalarUnit.DIP,
0.0f,
0.0f,
precision
)
InkSensorType.ALTITUDE -> SensorChannel(
InkSensorType.ALTITUDE,
InkSensorMetricType.ANGLE,
ScalarUnit.RADIAN,
0.0f,
(Math.PI/2).toFloat(),
precision
)
InkSensorType.AZIMUTH -> SensorChannel(
InkSensorType.AZIMUTH,
InkSensorMetricType.ANGLE,
ScalarUnit.RADIAN,
-(Math.PI/2).toFloat(),
(Math.PI/2).toFloat(),
precision
)
InkSensorType.ROTATION -> SensorChannel(
InkSensorType.ROTATION,
InkSensorMetricType.ANGLE,
ScalarUnit.RADIAN,
0.0f,
0.0f,
precision
)
else -> {
throw Exception("Unknown channel type.")
}
}
channels.add(channel)
}
return channels
}

SensorData リポジトリ

センサーデータを追跡するには、すべてのプラットフォーム上のInkInputProviderを知ることが重要ですが、これはイベントから推測することができます。 さらに、たとえばタッチ入力の場合とスタイラス入力の場合とでは、レンダリング動作が異なります。

...
val touchChannels = listOf(InkSensorType.X, InkSensorType.Y, InkSensorType.TIMESTAMP)
val penChannels = listOf(InkSensorType.X, InkSensorType.Y, InkSensorType.TIMESTAMP,
InkSensorType.PRESSURE, InkSensorType.ALTITUDE, InkSensorType.AZIMUTH)
...


fun createSensorData(event: MotionEvent): Pair<SensorData, List<SensorChannel>> {
// MotionEvent tool type helps to decide on the appropriate ink input provider
val toolType = when (event.getToolType(0)) {
MotionEvent.TOOL_TYPE_STYLUS -> InkInputType.PEN
MotionEvent.TOOL_TYPE_FINGER -> InkInputType.TOUCH
MotionEvent.TOOL_TYPE_MOUSE -> InkInputType.MOUSE
else -> InkInputType.PEN
}

var provider: InkInputProvider? = null
// First, check if exist an input provider of the desired type
for ((_, existingProvider) in inputProviders) {
existingProvider.type == toolType
provider = existingProvider
break
}

if (provider == null) {
provider = InkInputProvider(toolType)

// It is possible to add custom define properties to the input provider
// for example PenID in case exists
if (toolType == InkInputType.PEN) {
provider.putProperty("penType", "s-pen") // Assuming using Samsung S Pen
}
}

var inputContextId = if (!inputProviders.containsKey(provider.id)) {
inputProviders[provider.id] = provider

// Build the list of channels
val channels = registerChannels(if (toolType == InkInputType.PEN) penChannels else touchChannels)
channelsForInput.put(provider.id, channels)

val sensorChannelsContext = SensorChannelsContext(
provider.id, // Reference to input input provider
inputDevice.id, // Reference to input device
channels) // Channels for the registered channels

val sensorContext = SensorContext()
sensorContext.addSensorChannelsContext(sensorChannelsContext)

sensorContexts[sensorContext.id] = sensorContext

val inputContext = InputContext(
environment.id, // Reference to environment
sensorContext.id) // Reference to sensor context

inputContexts[inputContext.id] = inputContext

inputProviderToInputContextMapping[provider.id] = inputContext.id

inputContext.id
} else {
val provider = inputProviders[provider.id]!!
inputProviderToInputContextMapping[provider.id]!!
}
var channelList: List<SensorChannel>? = null
for ((id, channels) in channelsForInput) {
if (provider.id == id) {
channelList = channels
}
}

if (channelList == null) {
channelList = listOf() // empty list to avoid null pointer exceptions
}
// Create the SensorData object with a new unique ID and its input context reference id
return Pair<SensorData,
List<SensorChannel>>(SensorData(UUID.randomUUID().toString(), inputContextId, InkState.PLANE),
channelList)
}


override fun onEvent(pointerData: PointerData, inkToolType: InkInputType) {
when (pointerData.phase) {
// Set the appropriate input provider
Phase.BEGIN -> {
if ((inputProvider.type != inkToolType)) {
inputProvider = InkInputProvider(inkToolType)
}
}
// Adding sensor data
Phase.UPDATE, Phase.END -> {
sensorData.add(sensorChannels[InkSensorType.X]!!, pointerData.x)
sensorData.add(sensorChannels[InkSensorType.Y]!!, pointerData.y)
sensorData.addTimestamp(sensorChannels[InkSensorType.TIMESTAMP]!!,
pointerData.timestamp)
}
}
}
...
}

Stroke リポジトリ

rendering pipelineの視覚的出力も、Universal Ink Modelの極めて重要な要素であるため、保存することが必要です。 この場合は、次の構造を追加する必要があります。

  • Stroke - インクストロークのジオメトリ
  • Style - ストロークのレンダリング用のカスタマイズパラメータ
  • StrokeNode - インクツリーの論理ノード
import com.wacom.ink.format.rendering.PathPointProperties
import com.wacom.ink.format.rendering.Style
import com.wacom.ink.format.tree.data.Stroke
import com.wacom.ink.format.tree.nodes.StrokeNode
import com.wacom.ink.model.IdentifiableImpl

...
fun surfaceTouch(event: MotionEvent) {
...
if ((pointerData.phase == Phase.END) &&
(rasterInkBuilder.splineProducer.allData != null)) {
addStroke(event)
}
...
}

...
private fun addStroke(event: MotionEvent) {
// Adding the style
val style = Style(
rasterTool.uri(), // Style URI
1, // Particle random seed
props = PathPointProperties( // Coloring path properties
red = defaults.red,
green = defaults.green,
blue = defaults.blue,
alpha = defaults.alpha
),
renderModeUri = rasterTool.getBlendMode().name
)
// Adding stroke to the Stroke Repository
val path = Stroke(
IdentifiableImpl.generateUUID(), // Generated UUID
rasterInkBuilder.splineProducer.allData!!.copy(), // Spline
style // Style
)
// Adding a node to the Ink tree
val node = StrokeNode(IdentifiableImpl.generateUUID(),
path)
...
strokeNodeList.add(node)
}

Brush リポジトリ

あらゆるブラシを組み込むことで、さまざまなプラットフォームやアプリケーションにおいてインクストロークを確実にレンダリングできるようにする必要があります。

fun setTool(view: View, tool: Tool) {
drawingTool = tool

if (drawingTool is VectorTool) {
vectorDrawingView.setTool(drawingTool as VectorTool)
} else {
val dt = drawingTool as RasterTool
val brush = dt.brush
if (inkModel.brushRepository.getBrush(brush.name) == null) {
// Adding a raster brush if it is not within the repository yet
inkModel.brushRepository.addRasterBrush(brush as RasterBrush)
}
rasterDrawingSurface.setTool(dt)
}
highlightTool(view)
}

持続性

インクモデルを持続させ、プラットフォーム間で共有するには、データストリーミングでエンコードする必要があります。

エンコード

モデルはバイナリ表現でエンコードされます。詳細についてはhere.

try {
val file = File(filePath)
fileOutputStream = FileOutputStream(file)
// Serialize encoded ink model to an output stream
Will3Codec.encode(inkModel, fileOutputStream)
} catch (e: Exception){
e.printStackTrace()
} finally {
fileOutputStream?.close()
}

デコード

ストリームをデコードするには、エンコードしたデータをデコーダの関数に受け渡す必要があります。 データ形式のバージョン番号はRIFFヘッダー内でエンコードされるため、適切なバージョンのコンテンツパーサーが自動的に選択されます。

// Loading file stream from path
val fileInputStream = FileInputStream(File(path))
// Read bytes array with UIM content
val bytes = fileInputStream.readBytes()
// Using the codec decoder to de-serialize the InkModel
val inkModel = Will3Codec.decode(bytes)