【コード解説】欠損値の線形補間処理・Tensorflow編

著者: Natsuki Takayama
作成日: 2023年10月04日(水) 00:00
最終更新日: 2023年11月02日(木) 11:20
カテゴリ: コンピュータビジョン

こんにちは.高山です.
以前の記事で,Numpyの行列計算を用いて線形補間を行う方法を紹介しました.
今回は,同様の処理をTensorflowを用いて実装する方法を紹介します.
今回解説するスクリプトはGitHub上に公開しています

本記事の実装方法は,Brent M. Spell氏のブログに記載の方法を参考にしています.
この度,本記事向けに拡張したコードを記載してよいかお伺いしたところ,快く許可してくださいました.
この場を借りて感謝申し上げます.


更新履歴 (大きな変更のみ記載しています)

  • 2023/10/30: 処理時間の計測方法を更新しました

1. モジュールのロード

まず最初に,下記のコードでモジュールをロードします.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Standard modules.
import gc
import sys
import time
from functools import partial

# CV/ML.
import numpy as np

import tensorflow as tf
【コード解説】
- 標準モジュール
  - gc: ガベージコレクション用ライブラリ
    処理時間計測クラスの内部処理で用います.
  - sys: Pythonシステムを扱うライブラリ
    今回はバージョン情報を取得するために使用しています.
  - time: 時刻データを取り扱うためのライブラリ
    今回は処理時間を計測するために使用しています.
  - functools: 関数オブジェクトを操作するためのライブラリ
    処理時間計測クラスに渡す関数オブジェクト作成に使用しています.
- 画像処理・機械学習向けモジュール
  - numpy: 行列演算ライブラリ
    今回はデータのロードに用います.
  - Tensorflow: ニューラルネットワークライブラリ
    今回は行列演算機能を用います.

記事執筆時点のPythonと主要モジュールのバージョンは下記のとおりです.

1
2
3
print(f"Python:{sys.version}")
print(f"Numpy:{np.__version__}")
print(f"Tensorflow:{tf.__version__}")
Python:3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0]
Numpy:1.23.5
Tensorflow:2.14.0

2. 入力データのロード

次に,下記の処理で補間前と補間後の追跡点データをダウンロードします.
補間後の追跡点データは以前の記事で紹介した処理で生成したデータです.
このデータは今回の処理で生成したデータとの比較に用います.

!wget https://github.com/takayama-rado/trado_samples/raw/main/test_data/finger_far0_non_static.npy
!wget https://github.com/takayama-rado/trado_samples/raw/main/test_data/finger_far0_non_static_interp.npy

ls コマンドでデータがダウンロードされているか確認します.

!ls
finger_far0_non_static_interp.npy  finger_far0_non_static.npy  sample_data

3. 実験用処理の実装

実験に先立って,処理時間計測用の関数と実験用定数を定義します.
次のコードは処理時間の値から,適切なSI接頭辞を設定し,文字列にして返します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def get_perf_str(val):
    token_si = ["", "m", "µ", "n", "p"]
    exp_si = [1, 1e3, 1e6, 1e9, 1e12]
    perf_str = f"{val:3g}s"
    si = ""
    sval = val
    for token, exp in zip(token_si, exp_si):
        if val * exp > 1.0:
            si = token
            sval = val * exp
            break
    perf_str = f"{sval:3g}{si}s"
    return perf_str
- 引数
  - val: 処理時間を示す値 (秒)
- 2-6行目: 変数の初期化
- 7-11行目: 接頭辞を選択して,表示値を調整
- 12行目: 表示文字列を作成

次のコードは,処理時間を格納した配列から統計量を求めて表示します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def print_perf_time(intervals, top_k=None):
    if top_k is not None:
        intervals = np.sort(intervals)[:top_k]
    min = intervals.min()
    max = intervals.max()
    mean = intervals.mean()
    std = intervals.std()

    smin = get_perf_str(min)
    smax = get_perf_str(max)
    mean = get_perf_str(mean)
    std = get_perf_str(std)
    if top_k:
        print(f"Top {top_k} summary: Max {smax}, Min {smin}, Mean +/- Std {mean} +/- {std}")
    else:
        print(f"Overall summary: Max {smax}, Min {smin}, Mean +/- Std {mean} +/- {std}")
- 引数:
  - intervals: 処理時間のNumpy配列
  - top_k: intervalsから,処理時間が短いサンプルをk個取り出して統計量を算出
- 2-3行目: Top K個のサンプル抽出
- 4-7行目: 統計量算出
- 9-16行目: 表示文字列を作成して表示

次のクラスは,入力した関数を複数回呼び出して,処理時間の統計量を表示します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class PerfMeasure():
    def __init__(self,
                 trials=100,
                 top_k=10):
        self.trials = trials
        self.top_k = top_k

    def __call__(self, func):
        gc.collect()
        gc.disable()
        intervals = []
        for _ in range(self.trials):
            start = time.perf_counter()
            func()
            end = time.perf_counter()
            intervals.append(end - start)
        intervals = np.array(intervals)
        print_perf_time(intervals)
        if self.top_k:
            print_perf_time(intervals, self.top_k)
        gc.enable()
        gc.collect()
- 引数:
  - trials: 入力関数の実行回数
  - top_k: 値が定義されている場合,全体の処理時間配列から処理時間が短いサンプルをk個取り出して統計量を算出
- 9-10行目: 計測中はガベージコレクションをしないように設定
- 11-20行目: 処理時間計測処理
- 21-22行目: ガベージコレクションの設定を元に戻す

最後に,次のコードで実験用の定数を定義して処理時間計測クラスをインスタンス化しています.

1
2
3
TRIALS = 100
TOPK = 10
pmeasure = PerfMeasure(TRIALS, TOPK)

実際の所,Colabの様な複雑な処理環境で他のプロセスの影響を排除して純粋な処理時間を計測するのはかなり難しいです.
そこでここでは,処理の繰り返し数を \(100\) とし全体の統計量に加えて,処理時間の短い代表値 \(10\) 試行からも統計量を表示するようにしています.


4. 線形補間の実装・実行

ここから先は補間処理の実装・実行を行います.
Tensorflowは,プログラムの実行時に処理をコンパイル (計算グラフの作成) して実行します.
現在のTensorflowには,下記に示すとおり2種類の計算グラフ作成方法があります.

  • Eager execution mode (Define-by-Run): データを入力した際に処理のコンパイルと実行を同時に行う.
    Pythonの実行環境と親和性が高くインタラクティブな実行環境で利用しやすくなっています.
  • Graph mode (Define-and-Run): (C言語のように) 処理のコンパイルと実行が明確に別れて行われる.
    処理の最適化性能が高いため高速に動作します.

今回は,補間処理をそれぞれの動作モードで動かして処理時間を比較してみたいと思います.

4.1 Eager execution modeを用いた場合

本項ではまず,Eager execution mode を用いた場合の実装を紹介します.
と言っても特別なところはあまりありません(^^;).
現在のTensorflowは Eager execution mode が標準の動作モードになっているため,処理をそのまま書いていけばこちらの動作モードで実行されます.

補間処理の実装

次のコードでは,こちらの記事の第2節で説明した行列計算を用いた線形補間処理を実装しています.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def matrix_interp_tf_eager(track):
    orig_shape = tf.shape(track)
    tlength = orig_shape[0]
    mask = track[:, 0, -1] != 0
    valid = tf.reduce_sum(tf.cast(mask, dtype=tf.int32))
    if valid == tlength:
        y = track
    else:
        xs = tf.where(mask)
        xs = tf.reshape(xs, [valid])
        # determine the output data type
        ys = tf.reshape(track, [tlength, -1])
        ys = tf.gather(ys, xs, axis=0)
        x = tf.range(tlength)
        dtype_ys = ys.dtype

        # normalize data types
        xs = tf.cast(xs, dtype_ys)
        x = tf.cast(x, dtype_ys)

        # pad control points for extrapolation
        xs = tf.concat([[xs.dtype.min], xs, [xs.dtype.max]], axis=0)
        ys = tf.concat([ys[:1], ys, ys[-1:]], axis=0)

        # compute slopes, pad at the edges to flatten
        sloops = (ys[1:] - ys[:-1]) / tf.expand_dims((xs[1:] - xs[:-1]), axis=-1)
        sloops = tf.pad(sloops[:-1], [(1, 1), (0, 0)])

        # solve for intercepts
        intercepts = ys - sloops * tf.expand_dims(xs, axis=-1)

        # search for the line parameters at each input data point
        # create a grid of the inputs and piece breakpoints for thresholding
        # rely on argmax stopping on the first true when there are duplicates,
        # which gives us an index into the parameter vectors
        idx = tf.math.argmax(tf.expand_dims(xs, axis=-2) > tf.expand_dims(x, axis=-1), axis=-1)
        sloop = tf.gather(sloops, idx, axis=0)
        intercept = tf.gather(intercepts, idx, axis=0)

        # apply the linear mapping at each input data point
        y = sloop * tf.expand_dims(x, axis=-1) + intercept
        y = tf.cast(y, dtype_ys)
        y = tf.reshape(y, orig_shape)
    return y
【コード解説】
- 引数
  - track: `[T, J, C]` 形状の追跡点配列.欠損値はゼロ埋めされている必要があります.
    また,この関数は部位毎に追跡点配列が入力される (全追跡点,特徴量で欠損フレームが共通) ことを想定しています.
    - T: 動画フレームインデクス
    - J: 追跡点インデクス
    - C: 特徴量インデクス.今回は $(x, y, z, c)$ の4次元特徴量を用いています.
- 2-3行目: 内部処理で用いるため,`track` の形状情報を取得
- 4行目: 第0番目の追跡点の信頼度に基づいて,追跡成功フレームを示す `mask` を生成.
  この関数は全追跡点,特徴量で欠損フレームが共通であることを前提としています.
- 5-7行目: 5行目で追跡成功フレーム数を取得し,線形補間が必要かを判定.
  Numpyと違ってTensorflowでは暗黙的な型変換は行われないため,`tf.cast()` で明示的に型変換を行っています.
  7行目で即座に関数を出ずに `y = track` としている点に注意してください.
  Graph mode では `if` 文は `tf.cond()` 関数に変換されます.
  そのため,7行目で `return track` とした場合は `if` ブロックと `else` ブロックで同種の値を返していない,としてエラーになります.
  今回は動作モード毎にコードを変えたくないので,Graph mode で動作可能な実装方法をしています.
- 9-13行目: 追跡成功フレームのデータ点を取得
- 14行目: 補間箇所を含む,全フレームのインデクス配列を生成
- 15-19行目: 計算処理のために型を `ys` に合わせて変換
- 22-23行目: `xs` と `ys` の先頭と末尾にダミー値を追加
- 26-27行目: 直線の傾き行列を算出し,先頭にダミー値を追加
- 30行目: 直線の切片行列を算出
- 36-38行目: ルックアップテーブルを作成して,全フレームのインデクスに対する直線パラメータを取得
- 41-43行目: 線形補間を実行し,型と形状を入力に合わせて返す

全部位の補間処理

全部位の追跡処理は次の関数で実装してます.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def partsbased_interp_tf_eager(trackdata):
    num_joints = trackdata.shape[1]
    trackdata = tf.convert_to_tensor(trackdata)
    pose = tf.gather(trackdata, tf.range(0, 33), axis=1)
    lhand = tf.gather(trackdata, tf.range(33, 33+21), axis=1)
    rhand = tf.gather(trackdata, tf.range(33+21, 33+21+21), axis=1)
    face = tf.gather(trackdata, tf.range(33+21+21, num_joints), axis=1)

    pose = matrix_interp_tf_eager(pose)
    lhand = matrix_interp_tf_eager(lhand)
    rhand = matrix_interp_tf_eager(rhand)
    face = matrix_interp_tf_eager(face)
    return tf.concat([pose, lhand, rhand, face], axis=1)
【コード解説】
- 引数:
  - trackdata: `[T, J, C]` 形状の追跡点配列.欠損値はゼロ埋めされている必要があります.
    こちらの追跡点には全部位のデータが含まれていることを想定しています.
- 2行目: 内部処理のために,追跡点数を取得
- 3行目: `trackdata` を `numpy.ndarray` から `tensorflow.Tensor` に変換
- 4-7行目: 追跡点を部位毎に分割.Tensorflowでは配列の一部分を取得するのに `tf.gather()` 関数を用います.
- 9-12行目: 部位毎に線形補間を実行
- 13行目: 部位毎の追跡点を結合して返す

補間処理の実行

線形補間に必要な処理が実装できましたので,上でダウンロードしたデータを用いて処理を実行します.

次のコードでは,追跡点をロードして,関数の仕様に合わせて形状を変更しています.
追跡点データは[P, T, J, C]形状 (Pは人物インデクス) の配列ですが,人物は1名だけですのでP軸は除去しています.

1
2
3
4
5
trackdata = np.load("finger_far0_non_static.npy")
reftrack = np.load("finger_far0_non_static_interp.npy")
# Remove person axis.
trackdata = trackdata[0]
reftrack = reftrack[0]

次のコードは先程実装したpartsbased_interp_tf_eagertrackdataを入力して,補間後の追跡点newtrackを得ています.
1回目の処理呼び出しでは,処理のコンパイルが行われるため個別に時間を計測し,参照用の補間後追跡点との誤差を計測しています.
その後,処理時間計測クラスを用いて平均処理時間を表示しています.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Tensorflow.
# The 1st call may be slow because of the computation graph construction.
print(f"Time of first call.")
start = time.perf_counter()
newtrack = partsbased_interp_tf_eager(trackdata)
interval = time.perf_counter() - start
print_perf_time(np.array([interval]))

diff = (reftrack - newtrack.numpy()).sum()
print(f"Sum of error:{diff}")

print("Time after second call.")
target_fn = partial(partsbased_interp_tf_eager, trackdata=trackdata)
pmeasure(target_fn)

print 処理の結果を示します.
1回目の処理時間は121.0ミリ秒程度で,誤差はほぼありませんでした.
2回目以降の処理では,(他のプロセスの影響が少ない場合は) 11.7ミリ秒程度で処理が完了しています.

Time of first call.
Overall summary: Max 120.998ms, Min 120.998ms, Mean +/- Std 120.998ms +/-   0s
Sum of error:-6.935119145623503e-12
Time after second call.
Overall summary: Max 61.8187ms, Min 11.2679ms, Mean +/- Std 20.8628ms +/- 8.64964ms
Top 10 summary: Max 12.2807ms, Min 11.2679ms, Mean +/- Std 11.7345ms +/- 329.671µs

入力データ形状が変わった場合の挙動

Tensorflowで Graph mode を用いた場合の典型的な問題として,処理の再トレーシングが挙げられます.
これは,入力データの型や形状が変わった場合に処理のコンパイルが再度行われてしまう問題を指します.
Eager execution mode ではこの問題は起きませんが,Graph mode との比較のために下記のコードで挙動を確認します.
処理の再トレーシングについての詳細は,公式ドキュメントをご参照ください.

次のコードは先程の線形補間の実行処理とほぼ同じですが,入力データの時間長が1フレーム分短くなっています.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Tensorflow.
# The 1st call may be slow because of the computation graph construction.
print(f"Time of first call.")
start = time.perf_counter()
newtrack = partsbased_interp_tf_eager(trackdata[:-1])
interval = time.perf_counter() - start
print_perf_time(np.array([interval]))

diff = (reftrack[:-1] - newtrack.numpy()).sum()
print(f"Sum of error:{diff}")

print("Time after second call.")
target_fn = partial(partsbased_interp_tf_eager, trackdata=trackdata[:-1])
pmeasure(target_fn)

print 処理の結果を示します.
1回目と2回目以降の処理時間は大きくは変わらず,Eager execution mode では処理の再トレーシングは起きないことが分かります.

Time of first call.
Overall summary: Max 30.9785ms, Min 30.9785ms, Mean +/- Std 30.9785ms +/-   0s
Sum of error:-6.935119145623503e-12
Time after second call.
Overall summary: Max 75.0154ms, Min 15.6051ms, Mean +/- Std 23.0584ms +/- 10.6375ms
Top 10 summary: Max 17.3485ms, Min 15.6051ms, Mean +/- Std 16.8033ms +/- 521.555µs

4.2 Graph mode (tf.function) を"型指定無しで"用いた場合

補間処理の実装

ここから先は,Tensorflowの Graph mode を用いた場合の挙動について見ていきます.

Tensorflowでは @tf.function デコレータで修飾した関数は Graph mode で動作します.
今回は Graph mode で動作するように線形補間処理を実装しているので,コードの変更はほとんどありません.

次のコードでは,先程紹介した線形補間処理関数を Graph mode で動作するようにしています.
中身のコードは完全に同じなのでここでは説明を省かさせていただきます.

1
2
3
4
# If input_signature is omitted, the re-tracing is performed when a tensor's shape is changed.
@tf.function
def matrix_interp_tf(track):
    ...

補間処理の実行

次のコードで補間処理を実行しています.
先程と同じく,1回目の処理呼び出しでは,処理のコンパイルが行われるため個別に時間を計測し,2回目以降に処理時間計測クラスを用いて平均時間を表示しています.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Tensorflow.
# The 1st call may be slow because of the computation graph construction.
print(f"Time of first call.")
start = time.perf_counter()
newtrack = partsbased_interp_tf(trackdata)
interval = time.perf_counter() - start
print_perf_time(np.array([interval]))

diff = (reftrack - newtrack.numpy()).sum()
print(f"Sum of error:{diff}")

print("Time after second call.")
target_fn = partial(partsbased_interp_tf, trackdata=trackdata)
pmeasure(target_fn)

print 処理の結果を示します.
1回目の処理では処理のコンパイルの影響で1.6秒程度かかっていますが,2回目以降の処理では9.2ミリ秒程度で処理が完了しています.

Time of first call.
Overall summary: Max 1.5834s, Min 1.5834s, Mean +/- Std 1.5834s +/-   0s
Sum of error:-6.935119145623503e-12
Time after second call.
Overall summary: Max 70.1215ms, Min 8.67015ms, Mean +/- Std 21.3508ms +/- 10.4774ms
Top 10 summary: Max 9.71076ms, Min 8.67015ms, Mean +/- Std 9.19527ms +/- 332.419µs

入力データ形状が変わった場合の挙動

次に,先程と同じく入力データの形状が変わった場合の挙動を示します.
次のコードは先程の線形補間の実行処理とほぼ同じですが,入力データの時間長が1フレーム分短くなっています.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Tensorflow.
# The 1st call may be slow because of the computation graph construction.
print(f"Time of first call.")
start = time.perf_counter()
newtrack = partsbased_interp_tf(trackdata[:-1])
interval = time.perf_counter() - start
print_perf_time(np.array([interval]))

diff = (reftrack[:-1] - newtrack.numpy()).sum()
print(f"Sum of error:{diff}")

print("Time after second call.")
target_fn = partial(partsbased_interp_tf, trackdata=trackdata[:-1])
pmeasure(target_fn)

print 処理の結果を示します.
Eager execution mode の時とは異なり,1回目の処理で1.7秒程度かかり,2回目以降の処理では8.0ミリ秒程度で処理が完了しています.
ここから,Graph mode で入力データの形状が変わった場合は,再トレーシングの影響で実行時間が長くなってしまうことが分かります.

Time of first call.
Overall summary: Max 1.73487s, Min 1.73487s, Mean +/- Std 1.73487s +/-   0s
Sum of error:-6.935119145623503e-12
Time after second call.
Overall summary: Max 36.0208ms, Min 7.17009ms, Mean +/- Std 14.598ms +/- 5.84049ms
Top 10 summary: Max 8.39623ms, Min 7.17009ms, Mean +/- Std 7.99962ms +/- 385.734µs

4.3 Graph mode (tf.function) を"型指定有りで"用いた場合

補間処理の実装

最後に,Graph mode 利用時に処理の再トレーシングを抑制する方法を紹介します.
再トレーシングを抑制するには,@tf.function デコレータの引数として型情報を明示的に与えてあげればよいです.

次のコードでは,@tf.function デコレータの input_signature 引数に対して, tf.TensorSpec() を与えています.
tf.TensorSpec() は入力のメタ情報を記述するためのクラスで,ここでは入力配列の形状と型を記述しています.
shape=[None, None, 4]None は入力毎にサイズが変化する次元を示しています.
今回は時間軸と追跡点軸に対して None を指定しています.

1
2
3
4
# If input_signature is omitted, the re-tracing is performed when a tensor's shape is changed.
@tf.function(input_signature=(tf.TensorSpec(shape=[None, None, 4], dtype=tf.float64),))
def matrix_interp_tf(track):
    ...

補間処理の実行

次のコードで補間処理を実行しています.
先程と同じく,1回目の処理呼び出しでは,処理のコンパイルが行われるため個別に時間を計測し,2回目以降に処理時間計測クラスを用いて平均時間を表示しています.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Tensorflow.
# The 1st call may be slow because of the computation graph construction.
print(f"Time of first call.")
start = time.perf_counter()
newtrack = partsbased_interp_tf(trackdata)
interval = time.perf_counter() - start
print_perf_time(np.array([interval]))

diff = (reftrack - newtrack.numpy()).sum()
print(f"Sum of error:{diff}")

print("Time after second call.")
target_fn = partial(partsbased_interp_tf, trackdata=trackdata)
pmeasure(target_fn)

print 処理の結果を示します.
1回目の処理では処理のコンパイルの影響で328.6ミリ秒程度かかっていますが,2回目以降の処理では8.6ミリ秒程度で処理が完了していることが分かります.

Time of first call.
Overall summary: Max 328.611ms, Min 328.611ms, Mean +/- Std 328.611ms +/-   0s
Sum of error:-6.935119145623503e-12
Time after second call.
Overall summary: Max 33.595ms, Min 8.34474ms, Mean +/- Std 10.9117ms +/- 3.95531ms
Top 10 summary: Max 8.8418ms, Min 8.34474ms, Mean +/- Std 8.63372ms +/- 153.86µs

入力データ形状が変わった場合の挙動

次に,先程と同じく入力データの形状が変わった場合の挙動を示します.
次のコードは先程の線形補間の実行処理とほぼ同じですが,入力データの時間長が1フレーム分短くなっています.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Tensorflow.
# The 1st call may be slow because of the computation graph construction.
print(f"Time of first call.")
start = time.perf_counter()
newtrack = partsbased_interp_tf(trackdata[:-1])
interval = time.perf_counter() - start
print_perf_time(np.array([interval]))

diff = (reftrack[:-1] - newtrack.numpy()).sum()
print(f"Sum of error:{diff}")

print("Time after second call.")
target_fn = partial(partsbased_interp_tf, trackdata=trackdata[:-1])
pmeasure(target_fn)

print 処理の結果を示します.
前項の結果と異なり,1回目の処理では19.2ミリ秒程度,2回目以降の処理では5.9ミリ秒程度で処理が完了しています.
ここから,Graph mode で実行する際も入力データの型情報を明記することで再トレーシングを抑制することができることが分かります.

Time of first call.
Overall summary: Max 19.1748ms, Min 19.1748ms, Mean +/- Std 19.1748ms +/-   0s
Sum of error:-6.935119145623503e-12
Time after second call.
Overall summary: Max 29.0962ms, Min 5.7848ms, Mean +/- Std 7.69065ms +/- 4.21244ms
Top 10 summary: Max 5.92441ms, Min 5.7848ms, Mean +/- Std 5.87683ms +/- 44.1624µs

今回は線形補間処理をTensorflowの行列計算で実装する方法を紹介しましたが,如何でしょうか?
最初はさらっと書くつもりだったのですが,Tensorflowの実装でハマったところなどをちょこちょこ入れていったら結局長くなってしまいました(^^;).

今回実装した関数はTensorflow Liteにコンバートして動かすことも可能です.
使い道があれば,組み込み機器に実装して動かしてみるのも面白そうです.

今回紹介した話が,補間処理などでお悩みの方に何か参考になれば幸いです.